From 8ecb4a7173a666234b7ac19a63fc28732fbc31fe Mon Sep 17 00:00:00 2001 From: Ben Visness Date: Thu, 29 Jul 2021 22:40:47 -0500 Subject: [PATCH] Rework the category/thread data model Threads can stand alone now. Threads can be attached to resources directly without requiring a category. In addition, a lot of wiki stuff and library discussion stuff was deleted because we're not gonna port it. --- public/js/showcase.js | 12 +- public/style.css | 55 +--- public/themes/dark/theme.css | 2 - public/themes/light/theme.css | 2 - src/db/db.go | 2 +- src/hmnurl/hmnurl_test.go | 150 ++------- src/hmnurl/urls.go | 297 ++---------------- src/migration/migrationTemplate.txt | 1 - .../2021-07-28T020000Z_ReworkThreads.go | 160 ++++++++++ ...-28T033604Z_AddThreadAndPostConstraints.go | 88 ++++++ ...-07-28T040000Z_RenameCategoryToSubforum.go | 60 ++++ src/migration/migrations/migrations.go | 19 +- src/models/category.go | 155 --------- src/models/library_resource.go | 3 +- src/models/post.go | 13 +- src/models/subforum.go | 136 ++++++++ src/models/thread.go | 17 +- src/parsing/wasm/parsingmain.go | 2 + src/rawdata/scss/_forum.scss | 76 +---- src/rawdata/scss/_timeline.scss | 3 - src/rawdata/scss/themes/dark/_variables.scss | 3 - src/rawdata/scss/themes/light/_variables.scss | 3 - src/templates/mapping.go | 1 - .../src/{forum_category.html => forum.html} | 8 +- src/templates/src/forum_thread.html | 4 +- src/templates/src/include/header.html | 3 - src/templates/src/landing.html | 160 ---------- src/templates/src/user_profile.html | 8 +- src/templates/templates.go | 6 +- src/templates/types.go | 12 - src/website/category_helper.go | 14 - src/website/feed.go | 51 ++- src/website/forums.go | 265 ++++++++-------- src/website/landing.go | 42 ++- src/website/post_helper.go | 86 ++--- src/website/projects.go | 7 +- src/website/routes.go | 9 +- src/website/subforum_helper.go | 10 + src/website/timeline_helper.go | 53 +--- src/website/urls.go | 12 +- src/website/user.go | 72 +---- 41 files changed, 788 insertions(+), 1294 deletions(-) create mode 100644 src/migration/migrations/2021-07-28T020000Z_ReworkThreads.go create mode 100644 src/migration/migrations/2021-07-28T033604Z_AddThreadAndPostConstraints.go create mode 100644 src/migration/migrations/2021-07-28T040000Z_RenameCategoryToSubforum.go delete mode 100644 src/models/category.go create mode 100644 src/models/subforum.go rename src/templates/src/{forum_category.html => forum.html} (89%) delete mode 100644 src/website/category_helper.go create mode 100644 src/website/subforum_helper.go diff --git a/public/js/showcase.js b/public/js/showcase.js index fad6369..122e9ea 100644 --- a/public/js/showcase.js +++ b/public/js/showcase.js @@ -4,14 +4,10 @@ const TimelineTypes = { FORUM_REPLY: 2, BLOG_POST: 3, BLOG_COMMENT: 4, - WIKI_CREATE: 5, - WIKI_EDIT: 6, - WIKI_TALK: 7, - LIBRARY_COMMENT: 8, - SNIPPET_IMAGE: 9, - SNIPPET_VIDEO: 10, - SNIPPET_AUDIO: 11, - SNIPPET_YOUTUBE: 12 + SNIPPET_IMAGE: 5, + SNIPPET_VIDEO: 6, + SNIPPET_AUDIO: 7, + SNIPPET_YOUTUBE: 8 }; const showcaseItemTemplate = makeTemplateCloner("showcase_item"); diff --git a/public/style.css b/public/style.css index 9fea678..41b17fa 100644 --- a/public/style.css +++ b/public/style.css @@ -8634,11 +8634,6 @@ input[type=submit] { padding-left: 10px; max-width: 80em; } -.wiki .post { - padding: 0; - margin: auto; - max-width: 70em; } - .post .contents h1, .post .contents h2 { margin: 20px 0px; } @@ -8750,56 +8745,11 @@ input[type=submit] { .blog .post-list .post:nth-child(even) { background-color: transparent; } -.wiki .post p { - margin: 10px 0px; } - -.wiki .toc { - border-color: #aaa; - border-color: var(--wiki-border-color); - border-left-width: 1px; } - .wiki .toc .toc-number { - color: #333; - color: var(--wiki-toc-number-color); } - .wiki .toc ul { - list-style-type: none; - margin-left: 10px; - margin-bottom: 5px; } - .wiki .toc li { - margin-left: 0px; } - -.wiki .aside { - border-color: #aaa; - border-color: var(--wiki-border-color); - border-left-width: 1px; - margin-left: 20px; } - .wiki .aside::before { - margin-left: -20px; - margin-right: 15px; - display: inline-block; - width: 10px; - content: "\21b4 "; } - .wiki .aside .aside-heading { - padding: 2px; - margin: 1px; - border-radius: 3px; - border-width: 0px; - cursor: pointer; - display: inline; - background-color: transparent; } - .wiki .aside > .aside-body { - overflow: hidden; - padding-left: 10px; } - .wiki .aside.folded::before { - content: "\2192 "; } - .wiki .aside.folded > .aside-body { - max-height: 0px; } - .featured-post .meta .avatar-icon { left: -60px; bottom: -5px; } -.blog .body blockquote, -.wiki .body blockquote { +.blog .body blockquote { padding-top: 1px; padding-bottom: 1px; } @@ -9446,9 +9396,6 @@ span.icon-rss::before { .timeline.no-blogs .blogs { display: none; } -.timeline.no-wiki .wiki { - display: none; } - .timeline.no-library .library { display: none; } diff --git a/public/themes/dark/theme.css b/public/themes/dark/theme.css index fac0109..f94cc9e 100644 --- a/public/themes/dark/theme.css +++ b/public/themes/dark/theme.css @@ -300,8 +300,6 @@ will throw an error. --irc-users-popout-background: #181818; --irc-users-popout-border-color-left: #444; --irc-users-popout-border-color-right: #333; - --wiki-border-color: #444; - --wiki-toc-number-color: #bbb; --code-line-number-color: #444; --library-star-btn-background: #252525; --library-star-btn-border-color: #bbb; diff --git a/public/themes/light/theme.css b/public/themes/light/theme.css index 0e55f35..846c094 100644 --- a/public/themes/light/theme.css +++ b/public/themes/light/theme.css @@ -318,8 +318,6 @@ will throw an error. --irc-users-popout-background: #fff; --irc-users-popout-border-color-left: #bbb; --irc-users-popout-border-color-right: #ccc; - --wiki-border-color: #aaa; - --wiki-toc-number-color: #333; --code-line-number-color: #777; --library-star-btn-background: #fff; --library-star-btn-border-color: #999; diff --git a/src/db/db.go b/src/db/db.go index 0d036d1..6359f0e 100644 --- a/src/db/db.go +++ b/src/db/db.go @@ -22,7 +22,7 @@ import ( Values of these kinds are ok to query even if they are not directly understood by pgtype. This is common for custom types like: - type CategoryKind int + type ThreadType int */ var queryableKinds = []reflect.Kind{ reflect.Int, diff --git a/src/hmnurl/hmnurl_test.go b/src/hmnurl/hmnurl_test.go index d704b67..99f8e46 100644 --- a/src/hmnurl/hmnurl_test.go +++ b/src/hmnurl/hmnurl_test.go @@ -145,22 +145,22 @@ func TestPodcastRSS(t *testing.T) { AssertRegexMatch(t, BuildPodcastRSS(""), RegexPodcastRSS, nil) } -func TestForumCategory(t *testing.T) { - AssertRegexMatch(t, BuildForumCategory("", nil, 1), RegexForumCategory, nil) - AssertRegexMatch(t, BuildForumCategory("", []string{"wip"}, 2), RegexForumCategory, map[string]string{"cats": "wip", "page": "2"}) - AssertRegexMatch(t, BuildForumCategory("", []string{"sub", "wip"}, 2), RegexForumCategory, map[string]string{"cats": "sub/wip", "page": "2"}) - AssertSubdomain(t, BuildForumCategory("hmn", nil, 1), "") - AssertSubdomain(t, BuildForumCategory("", nil, 1), "") - AssertSubdomain(t, BuildForumCategory("hero", nil, 1), "hero") - assert.Panics(t, func() { BuildForumCategory("", nil, 0) }) - assert.Panics(t, func() { BuildForumCategory("", []string{"", "wip"}, 1) }) - assert.Panics(t, func() { BuildForumCategory("", []string{" ", "wip"}, 1) }) - assert.Panics(t, func() { BuildForumCategory("", []string{"wip/jobs"}, 1) }) +func TestForum(t *testing.T) { + AssertRegexMatch(t, BuildForum("", nil, 1), RegexForum, nil) + AssertRegexMatch(t, BuildForum("", []string{"wip"}, 2), RegexForum, map[string]string{"subforums": "wip", "page": "2"}) + AssertRegexMatch(t, BuildForum("", []string{"sub", "wip"}, 2), RegexForum, map[string]string{"subforums": "sub/wip", "page": "2"}) + AssertSubdomain(t, BuildForum("hmn", nil, 1), "") + AssertSubdomain(t, BuildForum("", nil, 1), "") + AssertSubdomain(t, BuildForum("hero", nil, 1), "hero") + assert.Panics(t, func() { BuildForum("", nil, 0) }) + assert.Panics(t, func() { BuildForum("", []string{"", "wip"}, 1) }) + assert.Panics(t, func() { BuildForum("", []string{" ", "wip"}, 1) }) + assert.Panics(t, func() { BuildForum("", []string{"wip/jobs"}, 1) }) } func TestForumNewThread(t *testing.T) { - AssertRegexMatch(t, BuildForumNewThread("", []string{"sub", "wip"}, false), RegexForumNewThread, map[string]string{"cats": "sub/wip"}) - AssertRegexMatch(t, BuildForumNewThread("", []string{"sub", "wip"}, true), RegexForumNewThreadSubmit, map[string]string{"cats": "sub/wip"}) + AssertRegexMatch(t, BuildForumNewThread("", []string{"sub", "wip"}, false), RegexForumNewThread, map[string]string{"subforums": "sub/wip"}) + AssertRegexMatch(t, BuildForumNewThread("", []string{"sub", "wip"}, true), RegexForumNewThreadSubmit, map[string]string{"subforums": "sub/wip"}) } func TestForumThread(t *testing.T) { @@ -197,12 +197,6 @@ func TestForumPostReply(t *testing.T) { AssertSubdomain(t, BuildForumPostReply("hero", nil, 1, 2), "hero") } -func TestForumPostQuote(t *testing.T) { - AssertRegexMatch(t, BuildForumPostQuote("", nil, 1, 2), RegexForumPostQuote, map[string]string{"threadid": "1", "postid": "2"}) - AssertRegexNoMatch(t, BuildForumPostQuote("", nil, 1, 2), RegexForumPost) - AssertSubdomain(t, BuildForumPostQuote("hero", nil, 1, 2), "hero") -} - func TestBlog(t *testing.T) { AssertRegexMatch(t, BuildBlog("", 1), RegexBlog, nil) AssertRegexMatch(t, BuildBlog("", 2), RegexBlog, map[string]string{"page": "2"}) @@ -248,82 +242,6 @@ func TestBlogPostQuote(t *testing.T) { AssertSubdomain(t, BuildBlogPostQuote("hero", 1, 2), "hero") } -func TestWiki(t *testing.T) { - AssertRegexMatch(t, BuildWiki(""), RegexWiki, nil) - AssertSubdomain(t, BuildWiki("hero"), "hero") -} - -func TestWikiIndex(t *testing.T) { - AssertRegexMatch(t, BuildWikiIndex(""), RegexWikiIndex, nil) - AssertSubdomain(t, BuildWikiIndex("hero"), "hero") -} - -func TestWikiArticle(t *testing.T) { - AssertRegexMatch(t, BuildWikiArticle("", 1, ""), RegexWikiArticle, map[string]string{"articleid": "1"}) - AssertRegexMatch(t, BuildWikiArticle("", 1, "wiki/title/--"), RegexWikiArticle, map[string]string{"articleid": "1"}) - AssertRegexMatch(t, BuildWikiArticleWithSectionName("", 1, "wiki/title/--", "Hello world"), RegexWikiArticle, map[string]string{"articleid": "1"}) - AssertSubdomain(t, BuildWikiArticle("hero", 1, ""), "hero") -} - -func TestWikiArticleEdit(t *testing.T) { - AssertRegexMatch(t, BuildWikiArticleEdit("", 1), RegexWikiArticleEdit, map[string]string{"articleid": "1"}) - AssertSubdomain(t, BuildWikiArticleEdit("hero", 1), "hero") -} - -func TestWikiArticleDelete(t *testing.T) { - AssertRegexMatch(t, BuildWikiArticleDelete("", 1), RegexWikiArticleDelete, map[string]string{"articleid": "1"}) - AssertSubdomain(t, BuildWikiArticleDelete("hero", 1), "hero") -} - -func TestWikiArticleHistory(t *testing.T) { - AssertRegexMatch(t, BuildWikiArticleHistory("", 1, ""), RegexWikiArticleHistory, map[string]string{"articleid": "1"}) - AssertRegexMatch(t, BuildWikiArticleHistory("", 1, "wiki/title/--"), RegexWikiArticleHistory, map[string]string{"articleid": "1"}) - AssertSubdomain(t, BuildWikiArticleHistory("hero", 1, ""), "hero") -} - -func TestWikiTalk(t *testing.T) { - AssertRegexMatch(t, BuildWikiTalk("", 1, ""), RegexWikiTalk, map[string]string{"articleid": "1"}) - AssertRegexMatch(t, BuildWikiTalk("", 1, "wiki/title/--"), RegexWikiTalk, map[string]string{"articleid": "1"}) - AssertSubdomain(t, BuildWikiTalk("hero", 1, ""), "hero") -} - -func TestWikiRevision(t *testing.T) { - AssertRegexMatch(t, BuildWikiRevision("", 1, "", 2), RegexWikiRevision, map[string]string{"articleid": "1", "revisionid": "2"}) - AssertRegexMatch(t, BuildWikiRevision("", 1, "wiki/title/--", 2), RegexWikiRevision, map[string]string{"articleid": "1", "revisionid": "2"}) - AssertSubdomain(t, BuildWikiRevision("hero", 1, "", 2), "hero") -} - -func TestWikiDiff(t *testing.T) { - AssertRegexMatch(t, BuildWikiDiff("", 1, "", 2, 3), RegexWikiDiff, map[string]string{"articleid": "1", "revisionidold": "2", "revisionidnew": "3"}) - AssertRegexMatch(t, BuildWikiDiff("", 1, "wiki/title", 2, 3), RegexWikiDiff, map[string]string{"articleid": "1", "revisionidold": "2", "revisionidnew": "3"}) - AssertSubdomain(t, BuildWikiDiff("hero", 1, "wiki/title", 2, 3), "hero") -} - -func TestWikiTalkPost(t *testing.T) { - AssertRegexMatch(t, BuildWikiTalkPost("", 1, 2), RegexWikiTalkPost, map[string]string{"articleid": "1", "postid": "2"}) - AssertSubdomain(t, BuildWikiTalkPost("hero", 1, 2), "hero") -} - -func TestWikiTalkPostDelete(t *testing.T) { - AssertRegexMatch(t, BuildWikiTalkPostDelete("", 1, 2), RegexWikiTalkPostDelete, map[string]string{"articleid": "1", "postid": "2"}) - AssertSubdomain(t, BuildWikiTalkPostDelete("hero", 1, 2), "hero") -} - -func TestWikiTalkPostEdit(t *testing.T) { - AssertRegexMatch(t, BuildWikiTalkPostEdit("", 1, 2), RegexWikiTalkPostEdit, map[string]string{"articleid": "1", "postid": "2"}) - AssertSubdomain(t, BuildWikiTalkPostEdit("hero", 1, 2), "hero") -} - -func TestWikiTalkPostReply(t *testing.T) { - AssertRegexMatch(t, BuildWikiTalkPostReply("", 1, 2), RegexWikiTalkPostReply, map[string]string{"articleid": "1", "postid": "2"}) - AssertSubdomain(t, BuildWikiTalkPostReply("hero", 1, 2), "hero") -} - -func TestWikiTalkPostQuote(t *testing.T) { - AssertRegexMatch(t, BuildWikiTalkPostQuote("", 1, 2), RegexWikiTalkPostQuote, map[string]string{"articleid": "1", "postid": "2"}) - AssertSubdomain(t, BuildWikiTalkPostQuote("hero", 1, 2), "hero") -} - func TestLibrary(t *testing.T) { AssertRegexMatch(t, BuildLibrary(""), RegexLibrary, nil) AssertSubdomain(t, BuildLibrary("hero"), "hero") @@ -344,38 +262,6 @@ func TestLibraryResource(t *testing.T) { AssertSubdomain(t, BuildLibraryResource("hero", 1), "hero") } -func TestLibraryDiscussion(t *testing.T) { - AssertRegexMatch(t, BuildLibraryDiscussion("", 1, 2, 1), RegexLibraryDiscussion, map[string]string{"resourceid": "1", "threadid": "2"}) - AssertRegexMatch(t, BuildLibraryDiscussion("", 1, 2, 3), RegexLibraryDiscussion, map[string]string{"resourceid": "1", "threadid": "2", "page": "3"}) - AssertRegexMatch(t, BuildLibraryDiscussionWithPostHash("", 1, 2, 3, 123), RegexLibraryDiscussion, map[string]string{"resourceid": "1", "threadid": "2", "page": "3"}) - AssertSubdomain(t, BuildLibraryDiscussion("hero", 1, 2, 3), "hero") -} - -func TestLibraryPost(t *testing.T) { - AssertRegexMatch(t, BuildLibraryPost("", 1, 2, 3), RegexLibraryPost, map[string]string{"resourceid": "1", "threadid": "2", "postid": "3"}) - AssertSubdomain(t, BuildLibraryPost("hero", 1, 2, 3), "hero") -} - -func TestLibraryPostDelete(t *testing.T) { - AssertRegexMatch(t, BuildLibraryPostDelete("", 1, 2, 3), RegexLibraryPostDelete, map[string]string{"resourceid": "1", "threadid": "2", "postid": "3"}) - AssertSubdomain(t, BuildLibraryPostDelete("hero", 1, 2, 3), "hero") -} - -func TestLibraryPostEdit(t *testing.T) { - AssertRegexMatch(t, BuildLibraryPostEdit("", 1, 2, 3), RegexLibraryPostEdit, map[string]string{"resourceid": "1", "threadid": "2", "postid": "3"}) - AssertSubdomain(t, BuildLibraryPostEdit("hero", 1, 2, 3), "hero") -} - -func TestLibraryPostReply(t *testing.T) { - AssertRegexMatch(t, BuildLibraryPostReply("", 1, 2, 3), RegexLibraryPostReply, map[string]string{"resourceid": "1", "threadid": "2", "postid": "3"}) - AssertSubdomain(t, BuildLibraryPostReply("hero", 1, 2, 3), "hero") -} - -func TestLibraryPostQuote(t *testing.T) { - AssertRegexMatch(t, BuildLibraryPostQuote("", 1, 2, 3), RegexLibraryPostQuote, map[string]string{"resourceid": "1", "threadid": "2", "postid": "3"}) - AssertSubdomain(t, BuildLibraryPostQuote("hero", 1, 2, 3), "hero") -} - func TestProjectCSS(t *testing.T) { AssertRegexMatch(t, BuildProjectCSS("000000"), RegexProjectCSS, nil) } @@ -395,8 +281,8 @@ func TestPublic(t *testing.T) { AssertRegexMatch(t, BuildUserFile("mylogo.png"), RegexPublic, nil) } -func TestForumCategoryMarkRead(t *testing.T) { - AssertRegexMatch(t, BuildForumCategoryMarkRead(5), RegexForumCategoryMarkRead, map[string]string{"catid": "5"}) +func TestForumMarkRead(t *testing.T) { + AssertRegexMatch(t, BuildForumMarkRead(5), RegexForumMarkRead, map[string]string{"sfid": "5"}) } func AssertSubdomain(t *testing.T, fullUrl string, expectedSubdomain string) { @@ -468,3 +354,9 @@ func AssertRegexNoMatch(t *testing.T, fullUrl string, regex *regexp.Regexp) { match := regex.FindStringSubmatch(requestPath) assert.Nilf(t, match, "Url matched regex: [%s] vs [%s]", requestPath, regex.String()) } + +func TestThingsThatDontNeedCoverage(t *testing.T) { + // look the other way ಠ_ಠ + BuildPodcastEpisodeFile("foo", "bar") + BuildS3Asset("ha ha") +} diff --git a/src/hmnurl/urls.go b/src/hmnurl/urls.go index 74e0939..3876cd4 100644 --- a/src/hmnurl/urls.go +++ b/src/hmnurl/urls.go @@ -305,17 +305,17 @@ func BuildPodcastEpisodeFile(projectSlug string, filename string) string { * Forums */ -// TODO(asaf): This also matches urls generated by BuildForumThread (/t/ is identified as a cat, and the threadid as a page) -// This shouldn't be a problem since we will match Thread before Category in the router, but should we enforce it here? -var RegexForumCategory = regexp.MustCompile(`^/forums(/(?P[^\d/]+(/[^\d]+)*))?(/(?P\d+))?$`) +// TODO(asaf): This also matches urls generated by BuildForumThread (/t/ is identified as a subforum, and the threadid as a page) +// This shouldn't be a problem since we will match Thread before Subforum in the router, but should we enforce it here? +var RegexForum = regexp.MustCompile(`^/forums(/(?P[^\d/]+(/[^\d]+)*))?(/(?P\d+))?$`) -func BuildForumCategory(projectSlug string, subforums []string, page int) string { +func BuildForum(projectSlug string, subforums []string, page int) string { defer CatchPanic() if page < 1 { panic(oops.New(nil, "Invalid forum thread page (%d), must be >= 1", page)) } - builder := buildForumCategoryPath(subforums) + builder := buildSubforumPath(subforums) if page > 1 { builder.WriteRune('/') @@ -325,12 +325,12 @@ func BuildForumCategory(projectSlug string, subforums []string, page int) string return ProjectUrl(builder.String(), nil, projectSlug) } -var RegexForumNewThread = regexp.MustCompile(`^/forums(/(?P[^\d/]+(/[^\d]+)*))?/t/new$`) -var RegexForumNewThreadSubmit = regexp.MustCompile(`^/forums(/(?P[^\d/]+(/[^\d]+)*))?/t/new/submit$`) +var RegexForumNewThread = regexp.MustCompile(`^/forums(/(?P[^\d/]+(/[^\d]+)*))?/t/new$`) +var RegexForumNewThreadSubmit = regexp.MustCompile(`^/forums(/(?P[^\d/]+(/[^\d]+)*))?/t/new/submit$`) func BuildForumNewThread(projectSlug string, subforums []string, submit bool) string { defer CatchPanic() - builder := buildForumCategoryPath(subforums) + builder := buildSubforumPath(subforums) builder.WriteString("/t/new") if submit { builder.WriteString("/submit") @@ -339,7 +339,7 @@ func BuildForumNewThread(projectSlug string, subforums []string, submit bool) st return ProjectUrl(builder.String(), nil, projectSlug) } -var RegexForumThread = regexp.MustCompile(`^/forums(/(?P[^\d/]+(/[^\d]+)*))?/t/(?P\d+)(-([^/]+))?(/(?P\d+))?$`) +var RegexForumThread = regexp.MustCompile(`^/forums(/(?P[^\d/]+(/[^\d]+)*))?/t/(?P\d+)(-([^/]+))?(/(?P\d+))?$`) func BuildForumThread(projectSlug string, subforums []string, threadId int, title string, page int) string { defer CatchPanic() @@ -355,7 +355,7 @@ func BuildForumThreadWithPostHash(projectSlug string, subforums []string, thread return ProjectUrlWithFragment(builder.String(), nil, projectSlug, strconv.Itoa(postId)) } -var RegexForumPost = regexp.MustCompile(`^/forums(/(?P[^\d/]+(/[^\d]+)*))?/t/(?P\d+)/p/(?P\d+)$`) +var RegexForumPost = regexp.MustCompile(`^/forums(/(?P[^\d/]+(/[^\d]+)*))?/t/(?P\d+)/p/(?P\d+)$`) func BuildForumPost(projectSlug string, subforums []string, threadId int, postId int) string { defer CatchPanic() @@ -364,7 +364,7 @@ func BuildForumPost(projectSlug string, subforums []string, threadId int, postId return ProjectUrl(builder.String(), nil, projectSlug) } -var RegexForumPostDelete = regexp.MustCompile(`^/forums(/(?P[^\d/]+(/[^\d]+)*))?/t/(?P\d+)/p/(?P\d+)/delete$`) +var RegexForumPostDelete = regexp.MustCompile(`^/forums(/(?P[^\d/]+(/[^\d]+)*))?/t/(?P\d+)/p/(?P\d+)/delete$`) func BuildForumPostDelete(projectSlug string, subforums []string, threadId int, postId int) string { defer CatchPanic() @@ -373,7 +373,7 @@ func BuildForumPostDelete(projectSlug string, subforums []string, threadId int, return ProjectUrl(builder.String(), nil, projectSlug) } -var RegexForumPostEdit = regexp.MustCompile(`^/forums(/(?P[^\d/]+(/[^\d]+)*))?/t/(?P\d+)/p/(?P\d+)/edit$`) +var RegexForumPostEdit = regexp.MustCompile(`^/forums(/(?P[^\d/]+(/[^\d]+)*))?/t/(?P\d+)/p/(?P\d+)/edit$`) func BuildForumPostEdit(projectSlug string, subforums []string, threadId int, postId int) string { defer CatchPanic() @@ -382,7 +382,7 @@ func BuildForumPostEdit(projectSlug string, subforums []string, threadId int, po return ProjectUrl(builder.String(), nil, projectSlug) } -var RegexForumPostReply = regexp.MustCompile(`^/forums(/(?P[^\d/]+(/[^\d]+)*))?/t/(?P\d+)/p/(?P\d+)/reply$`) +var RegexForumPostReply = regexp.MustCompile(`^/forums(/(?P[^\d/]+(/[^\d]+)*))?/t/(?P\d+)/p/(?P\d+)/reply$`) // TODO: It's kinda weird that we have "replies" to a specific post. That's not how the data works. I guess this just affects what you see as the "post you're replying to" on the post composer page? func BuildForumPostReply(projectSlug string, subforums []string, threadId int, postId int) string { @@ -472,163 +472,6 @@ func BuildBlogPostQuote(projectSlug string, threadId int, postId int) string { return ProjectUrl(builder.String(), nil, projectSlug) } -/* -* Wiki - */ - -var RegexWiki = regexp.MustCompile(`^/wiki$`) - -func BuildWiki(projectSlug string) string { - defer CatchPanic() - return ProjectUrl("/wiki", nil, projectSlug) -} - -var RegexWikiIndex = regexp.MustCompile(`^/wiki/index$`) - -func BuildWikiIndex(projectSlug string) string { - defer CatchPanic() - return ProjectUrl("/wiki/index", nil, projectSlug) -} - -var RegexWikiArticle = regexp.MustCompile(`^/wiki/(?P\d+)(-([^/])+)?$`) - -func BuildWikiArticle(projectSlug string, articleId int, title string) string { - defer CatchPanic() - builder := buildWikiArticlePath(articleId, title) - - return ProjectUrl(builder.String(), nil, projectSlug) -} - -func BuildWikiArticleWithSectionName(projectSlug string, articleId int, title string, sectionName string) string { - defer CatchPanic() - builder := buildWikiArticlePath(articleId, title) - - return ProjectUrlWithFragment(builder.String(), nil, projectSlug, sectionName) -} - -var RegexWikiArticleEdit = regexp.MustCompile(`^/wiki/(?P\d+)/edit$`) - -func BuildWikiArticleEdit(projectSlug string, articleId int) string { - defer CatchPanic() - builder := buildWikiArticlePath(articleId, "") - builder.WriteString("/edit") - - return ProjectUrl(builder.String(), nil, projectSlug) -} - -var RegexWikiArticleDelete = regexp.MustCompile(`^/wiki/(?P\d+)/delete$`) - -func BuildWikiArticleDelete(projectSlug string, articleId int) string { - defer CatchPanic() - builder := buildWikiArticlePath(articleId, "") - builder.WriteString("/delete") - - return ProjectUrl(builder.String(), nil, projectSlug) -} - -var RegexWikiArticleHistory = regexp.MustCompile(`^/wiki/(?P\d+)(-([^/])+)?/history$`) - -func BuildWikiArticleHistory(projectSlug string, articleId int, title string) string { - defer CatchPanic() - builder := buildWikiArticlePath(articleId, title) - builder.WriteString("/history") - - return ProjectUrl(builder.String(), nil, projectSlug) -} - -var RegexWikiTalk = regexp.MustCompile(`^/wiki/(?P\d+)(-([^/])+)?/talk$`) - -func BuildWikiTalk(projectSlug string, articleId int, title string) string { - defer CatchPanic() - builder := buildWikiArticlePath(articleId, title) - builder.WriteString("/talk") - - return ProjectUrl(builder.String(), nil, projectSlug) -} - -var RegexWikiRevision = regexp.MustCompile(`^/wiki/(?P\d+)(-([^/])+)?/(?P\d+)$`) - -func BuildWikiRevision(projectSlug string, articleId int, title string, revisionId int) string { - defer CatchPanic() - if revisionId < 1 { - panic(oops.New(nil, "Invalid wiki revision id (%d), must be >= 1", revisionId)) - } - builder := buildWikiArticlePath(articleId, title) - builder.WriteRune('/') - builder.WriteString(strconv.Itoa(revisionId)) - - return ProjectUrl(builder.String(), nil, projectSlug) -} - -var RegexWikiDiff = regexp.MustCompile(`^/wiki/(?P\d+)(-([^/])+)?/diff/(?P\d+)/(?P\d+)$`) - -func BuildWikiDiff(projectSlug string, articleId int, title string, revisionIdOld int, revisionIdNew int) string { - defer CatchPanic() - if revisionIdOld < 1 { - panic(oops.New(nil, "Invalid wiki revision id (%d), must be >= 1", revisionIdOld)) - } - - if revisionIdNew < 1 { - panic(oops.New(nil, "Invalid wiki revision id (%d), must be >= 1", revisionIdNew)) - } - builder := buildWikiArticlePath(articleId, title) - builder.WriteString("/diff/") - builder.WriteString(strconv.Itoa(revisionIdOld)) - builder.WriteRune('/') - builder.WriteString(strconv.Itoa(revisionIdNew)) - - return ProjectUrl(builder.String(), nil, projectSlug) -} - -var RegexWikiTalkPost = regexp.MustCompile(`^/wiki/(?P\d+)/talk/(?P\d+)$`) - -func BuildWikiTalkPost(projectSlug string, articleId int, postId int) string { - defer CatchPanic() - builder := buildWikiTalkPath(articleId, postId) - - return ProjectUrl(builder.String(), nil, projectSlug) -} - -var RegexWikiTalkPostDelete = regexp.MustCompile(`^/wiki/(?P\d+)/talk/(?P\d+)/delete$`) - -func BuildWikiTalkPostDelete(projectSlug string, articleId int, postId int) string { - defer CatchPanic() - builder := buildWikiTalkPath(articleId, postId) - builder.WriteString("/delete") - - return ProjectUrl(builder.String(), nil, projectSlug) -} - -var RegexWikiTalkPostEdit = regexp.MustCompile(`^/wiki/(?P\d+)/talk/(?P\d+)/edit$`) - -func BuildWikiTalkPostEdit(projectSlug string, articleId int, postId int) string { - defer CatchPanic() - builder := buildWikiTalkPath(articleId, postId) - builder.WriteString("/edit") - - return ProjectUrl(builder.String(), nil, projectSlug) -} - -var RegexWikiTalkPostReply = regexp.MustCompile(`^/wiki/(?P\d+)/talk/(?P\d+)/reply$`) - -func BuildWikiTalkPostReply(projectSlug string, articleId int, postId int) string { - defer CatchPanic() - builder := buildWikiTalkPath(articleId, postId) - builder.WriteString("/reply") - - return ProjectUrl(builder.String(), nil, projectSlug) -} - -var RegexWikiTalkPostQuote = regexp.MustCompile(`^/wiki/(?P\d+)/talk/(?P\d+)/quote$`) - -func BuildWikiTalkPostQuote(projectSlug string, articleId int, postId int) string { - defer CatchPanic() - builder := buildWikiTalkPath(articleId, postId) - builder.WriteString("/quote") - - return ProjectUrl(builder.String(), nil, projectSlug) -} - /* * Library */ @@ -671,74 +514,6 @@ func BuildLibraryResource(projectSlug string, resourceId int) string { return ProjectUrl(builder.String(), nil, projectSlug) } -var RegexLibraryDiscussion = regexp.MustCompile(`^/library/resource/(?P\d+)/d/(?P\d+)(/(?P\d+))?$`) - -func BuildLibraryDiscussion(projectSlug string, resourceId int, threadId int, page int) string { - defer CatchPanic() - builder := buildLibraryDiscussionPath(resourceId, threadId, page) - - return ProjectUrl(builder.String(), nil, projectSlug) -} - -func BuildLibraryDiscussionWithPostHash(projectSlug string, resourceId int, threadId int, page int, postId int) string { - defer CatchPanic() - if postId < 1 { - panic(oops.New(nil, "Invalid library post ID (%d), must be >= 1", postId)) - } - builder := buildLibraryDiscussionPath(resourceId, threadId, page) - - return ProjectUrlWithFragment(builder.String(), nil, projectSlug, strconv.Itoa(postId)) -} - -var RegexLibraryPost = regexp.MustCompile(`^/library/resource/(?P\d+)/d/(?P\d+)/p/(?P\d+)$`) - -func BuildLibraryPost(projectSlug string, resourceId int, threadId int, postId int) string { - defer CatchPanic() - builder := buildLibraryPostPath(resourceId, threadId, postId) - - return ProjectUrl(builder.String(), nil, projectSlug) -} - -var RegexLibraryPostDelete = regexp.MustCompile(`^/library/resource/(?P\d+)/d/(?P\d+)/p/(?P\d+)/delete$`) - -func BuildLibraryPostDelete(projectSlug string, resourceId int, threadId int, postId int) string { - defer CatchPanic() - builder := buildLibraryPostPath(resourceId, threadId, postId) - builder.WriteString("/delete") - - return ProjectUrl(builder.String(), nil, projectSlug) -} - -var RegexLibraryPostEdit = regexp.MustCompile(`^/library/resource/(?P\d+)/d/(?P\d+)/p/(?P\d+)/edit$`) - -func BuildLibraryPostEdit(projectSlug string, resourceId int, threadId int, postId int) string { - defer CatchPanic() - builder := buildLibraryPostPath(resourceId, threadId, postId) - builder.WriteString("/edit") - - return ProjectUrl(builder.String(), nil, projectSlug) -} - -var RegexLibraryPostReply = regexp.MustCompile(`^/library/resource/(?P\d+)/d/(?P\d+)/p/(?P\d+)/reply$`) - -func BuildLibraryPostReply(projectSlug string, resourceId int, threadId int, postId int) string { - defer CatchPanic() - builder := buildLibraryPostPath(resourceId, threadId, postId) - builder.WriteString("/reply") - - return ProjectUrl(builder.String(), nil, projectSlug) -} - -var RegexLibraryPostQuote = regexp.MustCompile(`^/library/resource/(?P\d+)/d/(?P\d+)/p/(?P\d+)/quote$`) - -func BuildLibraryPostQuote(projectSlug string, resourceId int, threadId int, postId int) string { - defer CatchPanic() - builder := buildLibraryPostPath(resourceId, threadId, postId) - builder.WriteString("/quote") - - return ProjectUrl(builder.String(), nil, projectSlug) -} - /* * Assets */ @@ -803,18 +578,18 @@ func BuildUserFile(filepath string) string { * Other */ -var RegexForumCategoryMarkRead = regexp.MustCompile(`^/markread/(?P\d+)$`) +var RegexForumMarkRead = regexp.MustCompile(`^/markread/(?P\d+)$`) -// NOTE(asaf): categoryId == 0 means ALL CATEGORIES -func BuildForumCategoryMarkRead(categoryId int) string { +// NOTE(asaf): subforumId == 0 means ALL SUBFORUMS +func BuildForumMarkRead(subforumId int) string { defer CatchPanic() - if categoryId < 0 { - panic(oops.New(nil, "Invalid category ID (%d), must be >= 0", categoryId)) + if subforumId < 0 { + panic(oops.New(nil, "Invalid subforum ID (%d), must be >= 0", subforumId)) } var builder strings.Builder builder.WriteString("/markread/") - builder.WriteString(strconv.Itoa(categoryId)) + builder.WriteString(strconv.Itoa(subforumId)) return Url(builder.String(), nil) } @@ -825,7 +600,7 @@ var RegexCatchAll = regexp.MustCompile("") * Helper functions */ -func buildForumCategoryPath(subforums []string) *strings.Builder { +func buildSubforumPath(subforums []string) *strings.Builder { for _, subforum := range subforums { if strings.Contains(subforum, "/") { panic(oops.New(nil, "Tried building forum url with / in subforum name")) @@ -855,7 +630,7 @@ func buildForumThreadPath(subforums []string, threadId int, title string, page i panic(oops.New(nil, "Invalid forum thread ID (%d), must be >= 1", threadId)) } - builder := buildForumCategoryPath(subforums) + builder := buildSubforumPath(subforums) builder.WriteString("/t/") builder.WriteString(strconv.Itoa(threadId)) @@ -880,7 +655,7 @@ func buildForumPostPath(subforums []string, threadId int, postId int) *strings.B panic(oops.New(nil, "Invalid forum post ID (%d), must be >= 1", postId)) } - builder := buildForumCategoryPath(subforums) + builder := buildSubforumPath(subforums) builder.WriteString("/t/") builder.WriteString(strconv.Itoa(threadId)) @@ -934,34 +709,6 @@ func buildBlogPostPath(threadId int, postId int) *strings.Builder { return &builder } -func buildWikiArticlePath(articleId int, title string) *strings.Builder { - if articleId < 1 { - panic(oops.New(nil, "Invalid wiki article ID (%d), must be >= 1", articleId)) - } - - var builder strings.Builder - - builder.WriteString("/wiki/") - builder.WriteString(strconv.Itoa(articleId)) - if len(title) > 0 { - builder.WriteRune('-') - builder.WriteString(PathSafeTitle(title)) - } - - return &builder -} - -func buildWikiTalkPath(articleId int, postId int) *strings.Builder { - if postId < 1 { - panic(oops.New(nil, "Invalid wiki post ID (%d), must be >= 1", postId)) - } - - builder := buildWikiArticlePath(articleId, "") - builder.WriteString("/talk/") - builder.WriteString(strconv.Itoa(postId)) - return builder -} - func buildLibraryResourcePath(resourceId int) *strings.Builder { if resourceId < 1 { panic(oops.New(nil, "Invalid library resource ID (%d), must be >= 1", resourceId)) diff --git a/src/migration/migrationTemplate.txt b/src/migration/migrationTemplate.txt index 7656e2f..53b4e35 100644 --- a/src/migration/migrationTemplate.txt +++ b/src/migration/migrationTemplate.txt @@ -5,7 +5,6 @@ import ( "time" "git.handmade.network/hmn/hmn/src/migration/types" - "git.handmade.network/hmn/hmn/src/oops" "github.com/jackc/pgx/v4" ) diff --git a/src/migration/migrations/2021-07-28T020000Z_ReworkThreads.go b/src/migration/migrations/2021-07-28T020000Z_ReworkThreads.go new file mode 100644 index 0000000..ee4fea4 --- /dev/null +++ b/src/migration/migrations/2021-07-28T020000Z_ReworkThreads.go @@ -0,0 +1,160 @@ +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(ReworkThreads{}) +} + +type ReworkThreads struct{} + +func (m ReworkThreads) Version() types.MigrationVersion { + return types.MigrationVersion(time.Date(2021, 7, 28, 2, 0, 0, 0, time.UTC)) +} + +func (m ReworkThreads) Name() string { + return "ReworkThreads" +} + +func (m ReworkThreads) Description() string { + return "Detach threads from categories and make them more independent" +} + +func (m ReworkThreads) Up(ctx context.Context, tx pgx.Tx) error { + // add and rename columns + _, err := tx.Exec(ctx, ` + ALTER TABLE handmade_thread + ADD type INT, + ADD project_id INT REFERENCES handmade_project (id) ON DELETE RESTRICT, -- used to associate project articles + ALTER category_id DROP NOT NULL, + ADD personal_article_user_id INT REFERENCES auth_user (id) ON DELETE RESTRICT; -- used to associate personal articles + ALTER TABLE handmade_thread + RENAME category_id TO subforum_id; -- preemptive, we're renaming categories next + + ALTER TABLE handmade_post + RENAME category_kind TO thread_type; + ALTER TABLE handmade_post + DROP category_id, + DROP CONSTRAINT post_category_kind_from_category, + DROP CONSTRAINT post_project_id_from_category; + + DROP FUNCTION category_id_for_thread(int); + DROP FUNCTION category_kind_for_post(int); + DROP FUNCTION project_id_for_post(int); + `) + if err != nil { + return oops.New(err, "failed to add and rename columns") + } + + // fill out null thread fields + _, err = tx.Exec(ctx, ` + UPDATE handmade_thread AS thread + SET (type, project_id, subforum_id) = ( + SELECT kind, project_id, CASE WHEN cat.kind = 2 THEN cat.id ELSE NULL END + FROM handmade_category AS cat + WHERE cat.id = thread.subforum_id + ); + + ALTER TABLE handmade_thread + ALTER type SET NOT NULL, + ALTER project_id SET NOT NULL; + `) + if err != nil { + return oops.New(err, "failed to copy category kind to thread type") + } + + // move wiki posts to personal articles + _, err = tx.Exec(ctx, ` + -- turn wiki threads into personal articles + UPDATE handmade_thread + SET + type = 7, -- new "personal article" type + personal_article_user_id = 1979 -- assign to Ben for now + WHERE type = 5; + + -- update the denormalized field on posts + UPDATE handmade_post + SET thread_type = 7 + WHERE thread_type = 5; + `) + if err != nil { + return oops.New(err, "failed to turn wiki posts into personal articles") + } + + // delete talk pages + _, err = tx.Exec(ctx, ` + DELETE FROM handmade_post + WHERE + thread_type = 7 -- personal articles, see above + AND parent_id IS NOT NULL; + + UPDATE handmade_thread + SET last_id = first_id + WHERE type = 7; + `) + if err != nil { + return oops.New(err, "failed to delete wiki talk pages") + } + + // delete library discussions + _, err = tx.Exec(ctx, ` + DELETE FROM handmade_threadlastreadinfo + WHERE thread_id IN ( + SELECT id + FROM handmade_thread + WHERE type = 6 + ); + + DELETE FROM handmade_thread + WHERE type = 6; + + DELETE FROM handmade_post + WHERE thread_type = 6; + + ALTER TABLE handmade_libraryresource + DROP category_id; + `) + if err != nil { + return oops.New(err, "failed to delete library discussions") + } + + // delete references to weirdo categories + _, err = tx.Exec(ctx, ` + ALTER TABLE handmade_project + DROP blog_id, + DROP annotation_id, + DROP wiki_id; + `) + if err != nil { + return oops.New(err, "failed to delete references to categories from projects") + } + + // delete categories we no longer need + _, err = tx.Exec(ctx, ` + DELETE FROM handmade_categorylastreadinfo + WHERE category_id IN ( + SELECT id + FROM handmade_category + WHERE kind != 2 + ); + + DELETE FROM handmade_category + WHERE kind != 2; + `) + if err != nil { + return oops.New(err, "failed to delete categories") + } + + return nil +} + +func (m ReworkThreads) Down(ctx context.Context, tx pgx.Tx) error { + panic("Implement me") +} diff --git a/src/migration/migrations/2021-07-28T033604Z_AddThreadAndPostConstraints.go b/src/migration/migrations/2021-07-28T033604Z_AddThreadAndPostConstraints.go new file mode 100644 index 0000000..cfe0a2b --- /dev/null +++ b/src/migration/migrations/2021-07-28T033604Z_AddThreadAndPostConstraints.go @@ -0,0 +1,88 @@ +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(AddThreadAndPostConstraints{}) +} + +type AddThreadAndPostConstraints struct{} + +func (m AddThreadAndPostConstraints) Version() types.MigrationVersion { + return types.MigrationVersion(time.Date(2021, 7, 28, 3, 36, 4, 0, time.UTC)) +} + +func (m AddThreadAndPostConstraints) Name() string { + return "AddThreadAndPostConstraints" +} + +func (m AddThreadAndPostConstraints) Description() string { + return "Add back appropriate check constraints for the new thread model" +} + +func (m AddThreadAndPostConstraints) Up(ctx context.Context, tx pgx.Tx) error { + // create null check constraints for threads + _, err := tx.Exec(ctx, ` + ALTER TABLE handmade_thread + ADD CONSTRAINT thread_has_field_for_type CHECK ( + CASE + WHEN type = 1 THEN + subforum_id IS NULL + AND personal_article_user_id IS NULL + WHEN type = 2 THEN + subforum_id IS NOT NULL + AND personal_article_user_id IS NULL + WHEN type = 7 THEN + subforum_id IS NULL + AND personal_article_user_id IS NOT NULL + ELSE TRUE + END + ); + `) + if err != nil { + return oops.New(err, "failed to add constraint to threads") + } + + // add constraints to posts + _, err = tx.Exec(ctx, ` + CREATE FUNCTION thread_type_for_post(int) RETURNS int AS $$ + SELECT thread.type + FROM + handmade_post AS post + JOIN handmade_thread AS thread ON post.thread_id = thread.id + WHERE post.id = $1 + $$ LANGUAGE SQL; + + CREATE FUNCTION project_id_for_post(int) RETURNS int AS $$ + SELECT thread.project_id + FROM + handmade_post AS post + JOIN handmade_thread AS thread ON post.thread_id = thread.id + WHERE post.id = $1 + $$ LANGUAGE SQL; + + ALTER TABLE handmade_post + ADD CONSTRAINT post_thread_type_from_thread CHECK ( + thread_type_for_post(id) = thread_type + ), + ADD CONSTRAINT post_project_id_from_thread CHECK ( + project_id_for_post(id) = project_id + ); + `) + if err != nil { + return oops.New(err, "failed to add post constraints") + } + + return nil +} + +func (m AddThreadAndPostConstraints) Down(ctx context.Context, tx pgx.Tx) error { + panic("Implement me") +} diff --git a/src/migration/migrations/2021-07-28T040000Z_RenameCategoryToSubforum.go b/src/migration/migrations/2021-07-28T040000Z_RenameCategoryToSubforum.go new file mode 100644 index 0000000..a9b4b4b --- /dev/null +++ b/src/migration/migrations/2021-07-28T040000Z_RenameCategoryToSubforum.go @@ -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(RenameCategoryToSubforum{}) +} + +type RenameCategoryToSubforum struct{} + +func (m RenameCategoryToSubforum) Version() types.MigrationVersion { + return types.MigrationVersion(time.Date(2021, 7, 28, 4, 0, 0, 0, time.UTC)) +} + +func (m RenameCategoryToSubforum) Name() string { + return "RenameCategoryToSubforum" +} + +func (m RenameCategoryToSubforum) Description() string { + return "Rename categories to subforums" +} + +func (m RenameCategoryToSubforum) Up(ctx context.Context, tx pgx.Tx) error { + _, err := tx.Exec(ctx, ` + ALTER TABLE handmade_category + RENAME TO handmade_subforum; + + ALTER TABLE handmade_subforum + ALTER project_id SET NOT NULL, + ALTER slug SET NOT NULL, + ALTER name SET NOT NULL, + ALTER blurb SET NOT NULL, + ALTER blurb SET DEFAULT '', + DROP kind, + DROP depth, + DROP color_1, + DROP color_2; + + ALTER TABLE handmade_categorylastreadinfo + RENAME TO handmade_subforumlastreadinfo; + ALTER TABLE handmade_subforumlastreadinfo + RENAME category_id TO subforum_id; + `) + if err != nil { + return oops.New(err, "failed to rename stuff") + } + + return nil +} + +func (m RenameCategoryToSubforum) Down(ctx context.Context, tx pgx.Tx) error { + panic("Implement me") +} diff --git a/src/migration/migrations/migrations.go b/src/migration/migrations/migrations.go index c11c9e7..ca33045 100644 --- a/src/migration/migrations/migrations.go +++ b/src/migration/migrations/migrations.go @@ -1,9 +1,26 @@ package migrations -import "git.handmade.network/hmn/hmn/src/migration/types" +import ( + "context" + "fmt" + + "git.handmade.network/hmn/hmn/src/migration/types" + "github.com/jackc/pgx/v4" +) var All map[types.MigrationVersion]types.Migration = make(map[types.MigrationVersion]types.Migration) func registerMigration(m types.Migration) { All[m.Version()] = m } + +func debugQuery(ctx context.Context, tx pgx.Tx, sql string) { + rows, err := tx.Query(ctx, sql) + if err != nil { + panic(err) + } + for rows.Next() { + vals, _ := rows.Values() + fmt.Println(vals) + } +} diff --git a/src/models/category.go b/src/models/category.go deleted file mode 100644 index d6bce87..0000000 --- a/src/models/category.go +++ /dev/null @@ -1,155 +0,0 @@ -package models - -import ( - "context" - - "git.handmade.network/hmn/hmn/src/db" - "git.handmade.network/hmn/hmn/src/oops" - "github.com/jackc/pgx/v4/pgxpool" -) - -type CategoryKind int - -const ( - CatKindBlog CategoryKind = iota + 1 - CatKindForum - CatKindStatic - CatKindAnnotation - CatKindWiki - CatKindLibraryResource -) - -type Category struct { - ID int `db:"id"` - - ParentID *int `db:"parent_id"` - ProjectID *int `db:"project_id"` // TODO: Make not null - - Slug *string `db:"slug"` // TODO: Make not null - Name *string `db:"name"` // TODO: Make not null - Blurb *string `db:"blurb"` // TODO: Make not null - Kind CategoryKind `db:"kind"` - Color1 string `db:"color_1"` - Color2 string `db:"color_2"` - Depth int `db:"depth"` // TODO: What is this? -} - -type CategoryTree map[int]*CategoryTreeNode - -type CategoryTreeNode struct { - Category - Parent *CategoryTreeNode - Children []*CategoryTreeNode -} - -func (node *CategoryTreeNode) GetLineage() []*Category { - current := node - length := 0 - for current != nil { - current = current.Parent - length += 1 - } - result := make([]*Category, length) - current = node - for i := length - 1; i >= 0; i -= 1 { - result[i] = ¤t.Category - current = current.Parent - } - return result -} - -func GetFullCategoryTree(ctx context.Context, conn *pgxpool.Pool) CategoryTree { - type categoryRow struct { - Cat Category `db:"cat"` - } - rows, err := db.Query(ctx, conn, categoryRow{}, - ` - SELECT $columns - FROM - handmade_category as cat - ORDER BY id ASC - `, - ) - if err != nil { - panic(oops.New(err, "Failed to fetch category tree")) - } - - rowsSlice := rows.ToSlice() - catTreeMap := make(map[int]*CategoryTreeNode, len(rowsSlice)) - for _, row := range rowsSlice { - cat := row.(*categoryRow).Cat - catTreeMap[cat.ID] = &CategoryTreeNode{Category: cat} - } - - for _, node := range catTreeMap { - if node.ParentID != nil { - node.Parent = catTreeMap[*node.ParentID] - } - } - - for _, row := range rowsSlice { - // NOTE(asaf): Doing this in a separate loop over rowsSlice to ensure that Children are in db order. - cat := row.(*categoryRow).Cat - node := catTreeMap[cat.ID] - if node.Parent != nil { - node.Parent.Children = append(node.Parent.Children, node) - } - } - return catTreeMap -} - -type CategoryLineageBuilder struct { - Tree CategoryTree - CategoryCache map[int][]*Category - SlugCache map[int][]string -} - -func MakeCategoryLineageBuilder(fullCategoryTree CategoryTree) *CategoryLineageBuilder { - return &CategoryLineageBuilder{ - Tree: fullCategoryTree, - CategoryCache: make(map[int][]*Category), - SlugCache: make(map[int][]string), - } -} - -func (cl *CategoryLineageBuilder) GetLineage(catId int) []*Category { - _, ok := cl.CategoryCache[catId] - if !ok { - cl.CategoryCache[catId] = cl.Tree[catId].GetLineage() - } - return cl.CategoryCache[catId] -} - -func (cl *CategoryLineageBuilder) GetSubforumLineage(catId int) []*Category { - return cl.GetLineage(catId)[1:] -} - -func (cl *CategoryLineageBuilder) GetLineageSlugs(catId int) []string { - _, ok := cl.SlugCache[catId] - if !ok { - lineage := cl.GetLineage(catId) - result := make([]string, 0, len(lineage)) - for _, cat := range lineage { - name := "" - if cat.Slug != nil { - name = *cat.Slug - } - result = append(result, name) - } - cl.SlugCache[catId] = result - } - return cl.SlugCache[catId] -} - -func (cl *CategoryLineageBuilder) GetSubforumLineageSlugs(catId int) []string { - return cl.GetLineageSlugs(catId)[1:] -} - -func (cl *CategoryLineageBuilder) FindIdBySlug(projectId int, slug string) int { - for _, node := range cl.Tree { - if node.Slug != nil && *node.Slug == slug && node.ProjectID != nil && *node.ProjectID == projectId { - return node.ID - } - } - return -1 -} diff --git a/src/models/library_resource.go b/src/models/library_resource.go index 1f0d13e..8fd6de2 100644 --- a/src/models/library_resource.go +++ b/src/models/library_resource.go @@ -3,8 +3,7 @@ package models type LibraryResource struct { ID int `db:"id"` - CategoryID int `db:"category_id"` - ProjectID *int `db:"project_id"` + ProjectID *int `db:"project_id"` Name string `db:"name"` Description string `db:"description"` diff --git a/src/models/post.go b/src/models/post.go index 4c476e2..64bf147 100644 --- a/src/models/post.go +++ b/src/models/post.go @@ -9,14 +9,13 @@ type Post struct { ID int `db:"id"` // TODO: Document each of these - AuthorID *int `db:"author_id"` - CategoryID int `db:"category_id"` - ParentID *int `db:"parent_id"` - ThreadID int `db:"thread_id"` - CurrentID int `db:"current_id"` - ProjectID int `db:"project_id"` + AuthorID *int `db:"author_id"` + ParentID *int `db:"parent_id"` + ThreadID int `db:"thread_id"` + CurrentID int `db:"current_id"` + ProjectID int `db:"project_id"` - CategoryKind CategoryKind `db:"category_kind"` + ThreadType ThreadType `db:"thread_type"` PostDate time.Time `db:"postdate"` Deleted bool `db:"deleted"` diff --git a/src/models/subforum.go b/src/models/subforum.go new file mode 100644 index 0000000..f65570e --- /dev/null +++ b/src/models/subforum.go @@ -0,0 +1,136 @@ +package models + +import ( + "context" + + "git.handmade.network/hmn/hmn/src/db" + "git.handmade.network/hmn/hmn/src/oops" + "github.com/jackc/pgx/v4/pgxpool" +) + +type Subforum struct { + ID int `db:"id"` + + ParentID *int `db:"parent_id"` + ProjectID int `db:"project_id"` + + Slug string `db:"slug"` + Name string `db:"name"` + Blurb string `db:"blurb"` +} + +type SubforumTree map[int]*SubforumTreeNode + +type SubforumTreeNode struct { + Subforum + Parent *SubforumTreeNode + Children []*SubforumTreeNode +} + +func (node *SubforumTreeNode) GetLineage() []*Subforum { + current := node + length := 0 + for current != nil { + current = current.Parent + length += 1 + } + result := make([]*Subforum, length) + current = node + for i := length - 1; i >= 0; i -= 1 { + result[i] = ¤t.Subforum + current = current.Parent + } + return result +} + +func GetFullSubforumTree(ctx context.Context, conn *pgxpool.Pool) SubforumTree { + type subforumRow struct { + Subforum Subforum `db:"sf"` + } + rows, err := db.Query(ctx, conn, subforumRow{}, + ` + SELECT $columns + FROM + handmade_subforum as sf + ORDER BY id ASC + `, + ) + if err != nil { + panic(oops.New(err, "failed to fetch subforum tree")) + } + + rowsSlice := rows.ToSlice() + sfTreeMap := make(map[int]*SubforumTreeNode, len(rowsSlice)) + for _, row := range rowsSlice { + sf := row.(*subforumRow).Subforum + sfTreeMap[sf.ID] = &SubforumTreeNode{Subforum: sf} + } + + for _, node := range sfTreeMap { + if node.ParentID != nil { + node.Parent = sfTreeMap[*node.ParentID] + } + } + + for _, row := range rowsSlice { + // NOTE(asaf): Doing this in a separate loop over rowsSlice to ensure that Children are in db order. + cat := row.(*subforumRow).Subforum + node := sfTreeMap[cat.ID] + if node.Parent != nil { + node.Parent.Children = append(node.Parent.Children, node) + } + } + return sfTreeMap +} + +type SubforumLineageBuilder struct { + Tree SubforumTree + SubforumCache map[int][]*Subforum + SlugCache map[int][]string +} + +func MakeSubforumLineageBuilder(fullSubforumTree SubforumTree) *SubforumLineageBuilder { + return &SubforumLineageBuilder{ + Tree: fullSubforumTree, + SubforumCache: make(map[int][]*Subforum), + SlugCache: make(map[int][]string), + } +} + +func (cl *SubforumLineageBuilder) GetLineage(sfId int) []*Subforum { + _, ok := cl.SubforumCache[sfId] + if !ok { + cl.SubforumCache[sfId] = cl.Tree[sfId].GetLineage() + } + return cl.SubforumCache[sfId] +} + +func (cl *SubforumLineageBuilder) GetSubforumLineage(sfId int) []*Subforum { + return cl.GetLineage(sfId)[1:] +} + +func (cl *SubforumLineageBuilder) GetLineageSlugs(sfId int) []string { + _, ok := cl.SlugCache[sfId] + if !ok { + lineage := cl.GetLineage(sfId) + result := make([]string, 0, len(lineage)) + for _, cat := range lineage { + result = append(result, cat.Slug) + } + cl.SlugCache[sfId] = result + } + return cl.SlugCache[sfId] +} + +func (cl *SubforumLineageBuilder) GetSubforumLineageSlugs(sfId int) []string { + return cl.GetLineageSlugs(sfId)[1:] +} + +func (cl *SubforumLineageBuilder) FindIdBySlug(projectId int, slug string) int { + for _, node := range cl.Tree { + if node.Slug == slug && node.ProjectID == projectId { + return node.ID + } + } + return -1 +} diff --git a/src/models/thread.go b/src/models/thread.go index 1d47246..5e7bac9 100644 --- a/src/models/thread.go +++ b/src/models/thread.go @@ -1,9 +1,24 @@ package models +type ThreadType int + +const ( + ThreadTypeProjectArticle ThreadType = iota + 1 + ThreadTypeForumPost + _ // formerly occupied by static pages, RIP + _ // formerly occupied by who the hell knows what, RIP + _ // formerly occupied by the wiki, RIP + _ // formerly occupied by library discussions, RIP + ThreadTypePersonalArticle +) + type Thread struct { ID int `db:"id"` - CategoryID int `db:"category_id"` + Type ThreadType `db:"type"` + ProjectID int `db:"project_id"` + SubforumID *int `db:"subforum_id"` + PersonalArticleUserID *int `db:"personal_article_user_id"` Title string `db:"title"` Sticky bool `db:"sticky"` diff --git a/src/parsing/wasm/parsingmain.go b/src/parsing/wasm/parsingmain.go index 3adc6ba..3d6b067 100644 --- a/src/parsing/wasm/parsingmain.go +++ b/src/parsing/wasm/parsingmain.go @@ -1,3 +1,5 @@ +// +build js + package main import ( diff --git a/src/rawdata/scss/_forum.scss b/src/rawdata/scss/_forum.scss index 84fb307..14daab4 100644 --- a/src/rawdata/scss/_forum.scss +++ b/src/rawdata/scss/_forum.scss @@ -157,13 +157,6 @@ } .post { - .wiki &, - { - padding: 0; - margin: auto; - max-width: 70em; - } - .contents { h1, h2 { margin: 20px 0px; @@ -332,79 +325,12 @@ } } -.wiki { - .post p { - margin: 10px 0px; - } - - .toc { - @include usevar(border-color, 'wiki-border-color'); - - border-left-width: 1px; - - .toc-number { - @include usevar(color, 'wiki-toc-number-color'); - } - - ul { - list-style-type: none; - margin-left: 10px; - margin-bottom: 5px; - } - - li { - margin-left: 0px; - } - } - - .aside { - @include usevar(border-color, 'wiki-border-color'); - - border-left-width: 1px; - margin-left:20px; - - &::before { - margin-left:-20px; - margin-right:15px; - display:inline-block; - width:10px; - content:"\21b4 "; - } - - .aside-heading { - padding:2px; - margin:1px; - border-radius:3px; - border-width:0px; - cursor:pointer; - display:inline; - background-color:transparent; - } - - > .aside-body { - overflow:hidden; - padding-left:10px; - } - - &.folded { - &::before { - content:"\2192 "; - } - - > .aside-body { - max-height: 0px; - } - } - } -} - .featured-post .meta .avatar-icon { left:-60px; bottom:-5px; } -.blog .body blockquote, -.wiki .body blockquote { +.blog .body blockquote { padding-top:1px; padding-bottom:1px; } diff --git a/src/rawdata/scss/_timeline.scss b/src/rawdata/scss/_timeline.scss index e9012e2..8264514 100644 --- a/src/rawdata/scss/_timeline.scss +++ b/src/rawdata/scss/_timeline.scss @@ -5,9 +5,6 @@ &.no-blogs .blogs { display: none; } - &.no-wiki .wiki { - display: none; - } &.no-library .library { display: none; } diff --git a/src/rawdata/scss/themes/dark/_variables.scss b/src/rawdata/scss/themes/dark/_variables.scss index 8c567d9..e4c7404 100644 --- a/src/rawdata/scss/themes/dark/_variables.scss +++ b/src/rawdata/scss/themes/dark/_variables.scss @@ -112,9 +112,6 @@ $vars: ( irc-users-popout-border-color-left: #444, irc-users-popout-border-color-right: #333, - wiki-border-color: #444, - wiki-toc-number-color: #bbb, - code-line-number-color: #444, library-star-btn-background: #252525, diff --git a/src/rawdata/scss/themes/light/_variables.scss b/src/rawdata/scss/themes/light/_variables.scss index 634f368..520f14e 100644 --- a/src/rawdata/scss/themes/light/_variables.scss +++ b/src/rawdata/scss/themes/light/_variables.scss @@ -112,9 +112,6 @@ $vars: ( irc-users-popout-border-color-left: #bbb, irc-users-popout-border-color-right: #ccc, - wiki-border-color: #aaa, - wiki-toc-number-color: #333, - code-line-number-color: #777, library-star-btn-background: #fff, diff --git a/src/templates/mapping.go b/src/templates/mapping.go index e07b8cc..eed5634 100644 --- a/src/templates/mapping.go +++ b/src/templates/mapping.go @@ -99,7 +99,6 @@ func ProjectToTemplate(p *models.Project, theme string) Project { HasBlog: true, // TODO: Check flag sets or whatever HasForum: true, - HasWiki: true, HasLibrary: true, DateApproved: p.DateApproved, diff --git a/src/templates/src/forum_category.html b/src/templates/src/forum.html similarity index 89% rename from src/templates/src/forum_category.html rename to src/templates/src/forum.html index e9f6aad..dc0fe6a 100644 --- a/src/templates/src/forum_category.html +++ b/src/templates/src/forum.html @@ -2,7 +2,7 @@ {{ define "content" }} {{ end }} -{{ define "forum_category_options" }} +{{ define "subforum_options" }}
{{ if .User }} + New Thread diff --git a/src/templates/src/forum_thread.html b/src/templates/src/forum_thread.html index df996c2..553f06d 100644 --- a/src/templates/src/forum_thread.html +++ b/src/templates/src/forum_thread.html @@ -4,7 +4,7 @@
{{ template "pagination.html" .Pagination }} @@ -118,7 +118,7 @@ {{ end }}
- ← Back to index + ← Back to index {{ if .Thread.Locked }} Thread is locked. {{ else if .User }} diff --git a/src/templates/src/include/header.html b/src/templates/src/include/header.html index db46fdb..6e61d64 100644 --- a/src/templates/src/include/header.html +++ b/src/templates/src/include/header.html @@ -51,9 +51,6 @@ {{ if .Project.HasForum }} Forums {{ end }} - {{ if .Project.HasWiki }} - Wiki - {{ end }} {{ if .Project.HasLibrary }} Library {{ end }} diff --git a/src/templates/src/landing.html b/src/templates/src/landing.html index faf9dc5..49c83d1 100644 --- a/src/templates/src/landing.html +++ b/src/templates/src/landing.html @@ -172,166 +172,6 @@
{{ end }} -{{/* TODO(asaf): Delete this section once we're done with the landing page -{% block columns %} -{% include "showcase/js_templates.html" %} -{% include "timeline/js_templates.html" %} -
-
-

Community Showcase

- -
- - - -
- This is a selection of recent work done by community members. Want to participate? Join us on Discord. -
-
- - -
-
-
-

Around the Network

- -
-
-
-{% spaceless %} -
- {% for col in recent_post_columns %} -
-
- {% if forloop.counter == 1 %} -
- {% include "blog_index_thread_list_entry.html" with post=featured_post align_top=True %} -
- {% endif %} - - {% for entry in col %} - {% with proj=entry.project posts=entry.posts %} -
- -

{{ proj.name }}

-
- - {% if entry.featured and proj.slug != "hmn" %} - {% with post=entry.featured.0 has_read=entry.featured.1 %} - {% if post.category.kind == 5 and post.parent == None %} - {% include "thread_list_entry.html" with thread=post.thread %} - {% else %} - {% include "blog_index_thread_list_entry.html" with align_top=True %} - {% endif %} - {% endwith %} - {% endif %} - - {% for post, has_read in posts %} - {% if forloop.counter0 < max_posts %} - {% include "thread_list_entry.html" with thread=post.thread %} - {% endif %} - {% endfor %} - {% with more=posts|length|add:-5|clamp_lower:0 %} - {% if more > 0 %} - - {% endif %} - {% endwith %} -
- {% endwith %} - {% endfor %} -
-
- {% endfor %} -
-{% endspaceless %} -{% endblock %} -*/}} - {{ define "landing_page_featured_post" }} {{/* Call this template with a LandingPageFeaturedPost. */}}
diff --git a/src/templates/src/user_profile.html b/src/templates/src/user_profile.html index 9040a20..f37dfa8 100644 --- a/src/templates/src/user_profile.html +++ b/src/templates/src/user_profile.html @@ -25,12 +25,6 @@ {{ if gt .NumBlogs 0 }}
{{ end }} - {{ if gt .NumWiki 0 }} -
- {{ end }} - {{ if gt .NumLibrary 0 }} -
- {{ end }} {{ if gt .NumSnippets 0 }}
{{ end }} @@ -61,7 +55,7 @@
Posts
-
{{ add .NumForums .NumBlogs .NumWiki .NumLibrary }}
+
{{ add .NumForums .NumBlogs }}
{{ if .ProfileUser.Email }} diff --git a/src/templates/templates.go b/src/templates/templates.go index 645dc0b..294c40e 100644 --- a/src/templates/templates.go +++ b/src/templates/templates.go @@ -218,11 +218,7 @@ var HMNTemplateFuncs = template.FuncMap{ if item.Type == TimelineTypeForumThread || item.Type == TimelineTypeForumReply || item.Type == TimelineTypeBlogPost || - item.Type == TimelineTypeBlogComment || - item.Type == TimelineTypeWikiCreate || - item.Type == TimelineTypeWikiEdit || - item.Type == TimelineTypeWikiTalk || - item.Type == TimelineTypeLibraryComment { + item.Type == TimelineTypeBlogComment { return true } diff --git a/src/templates/types.go b/src/templates/types.go index 3296ae1..fee5266 100644 --- a/src/templates/types.go +++ b/src/templates/types.go @@ -39,7 +39,6 @@ type Header struct { ProjectIndexUrl string BlogUrl string ForumsUrl string - WikiUrl string LibraryUrl string ManifestoUrl string EpisodeGuideUrl string @@ -109,7 +108,6 @@ type Project struct { HasBlog bool HasForum bool - HasWiki bool HasLibrary bool UUID string @@ -202,10 +200,6 @@ const ( PostTypeBlogComment PostTypeForumThread PostTypeForumReply - PostTypeWikiCreate - PostTypeWikiTalk - PostTypeWikiEdit - PostTypeLibraryComment ) // Data from post_list_item.html @@ -253,12 +247,6 @@ const ( TimelineTypeBlogPost TimelineTypeBlogComment - TimelineTypeWikiCreate - TimelineTypeWikiEdit - TimelineTypeWikiTalk - - TimelineTypeLibraryComment - TimelineTypeSnippetImage TimelineTypeSnippetVideo TimelineTypeSnippetAudio diff --git a/src/website/category_helper.go b/src/website/category_helper.go deleted file mode 100644 index 429dba2..0000000 --- a/src/website/category_helper.go +++ /dev/null @@ -1,14 +0,0 @@ -package website - -import ( - "git.handmade.network/hmn/hmn/src/models" -) - -var CategoryKindDisplayNames = map[models.CategoryKind]string{ - models.CatKindBlog: "Blog", - models.CatKindForum: "Forums", - models.CatKindStatic: "Static Page", - models.CatKindAnnotation: "Episode Guide", - models.CatKindWiki: "Wiki", - models.CatKindLibraryResource: "Library", -} diff --git a/src/website/feed.go b/src/website/feed.go index 3f570db..b27e38a 100644 --- a/src/website/feed.go +++ b/src/website/feed.go @@ -37,11 +37,11 @@ func Feed(c *RequestContext) ResponseData { FROM handmade_post AS post WHERE - post.category_kind = ANY ($1) + post.thread_type = ANY ($1) AND deleted = FALSE AND post.thread_id IS NOT NULL `, - []models.CategoryKind{models.CatKindForum, models.CatKindBlog, models.CatKindWiki, models.CatKindLibraryResource}, + []models.ThreadType{models.ThreadTypeForumPost, models.ThreadTypeProjectArticle}, ) c.Perf.EndBlock() if err != nil { @@ -80,9 +80,9 @@ func Feed(c *RequestContext) ResponseData { currentUserId = &c.CurrentUser.ID } - c.Perf.StartBlock("SQL", "Fetch category tree") - categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn) - lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree) + c.Perf.StartBlock("SQL", "Fetch subforum tree") + subforumTree := models.GetFullSubforumTree(c.Context(), c.Conn) + lineageBuilder := models.MakeSubforumLineageBuilder(subforumTree) c.Perf.EndBlock() posts, err := fetchAllPosts(c, lineageBuilder, currentUserId, howManyPostsToSkip, postsPerPage) @@ -98,7 +98,7 @@ func Feed(c *RequestContext) ResponseData { BaseData: baseData, AtomFeedUrl: hmnurl.BuildAtomFeed(), - MarkAllReadUrl: hmnurl.BuildForumCategoryMarkRead(0), + MarkAllReadUrl: hmnurl.BuildForumMarkRead(0), Posts: posts, Pagination: pagination, }, c.Perf) @@ -158,9 +158,9 @@ func AtomFeed(c *RequestContext) ResponseData { feedData.AtomFeedUrl = hmnurl.BuildAtomFeed() feedData.FeedUrl = hmnurl.BuildFeed() - c.Perf.StartBlock("SQL", "Fetch category tree") - categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn) - lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree) + c.Perf.StartBlock("SQL", "Fetch subforum tree") + subforumTree := models.GetFullSubforumTree(c.Context(), c.Conn) + lineageBuilder := models.MakeSubforumLineageBuilder(subforumTree) c.Perf.EndBlock() posts, err := fetchAllPosts(c, lineageBuilder, nil, 0, itemsPerFeed) @@ -303,18 +303,16 @@ func AtomFeed(c *RequestContext) ResponseData { return res } -func fetchAllPosts(c *RequestContext, lineageBuilder *models.CategoryLineageBuilder, currentUserID *int, offset int, limit int) ([]templates.PostListItem, error) { +func fetchAllPosts(c *RequestContext, lineageBuilder *models.SubforumLineageBuilder, currentUserID *int, offset int, limit int) ([]templates.PostListItem, error) { c.Perf.StartBlock("SQL", "Fetch posts") type feedPostQuery struct { - Post models.Post `db:"post"` - PostVersion models.PostVersion `db:"version"` - Thread models.Thread `db:"thread"` - Cat models.Category `db:"cat"` - Proj models.Project `db:"proj"` - LibraryResource *models.LibraryResource `db:"lib_resource"` - User models.User `db:"auth_user"` - ThreadLastReadTime *time.Time `db:"tlri.lastread"` - CatLastReadTime *time.Time `db:"clri.lastread"` + Post models.Post `db:"post"` + PostVersion models.PostVersion `db:"version"` + Thread models.Thread `db:"thread"` + Proj models.Project `db:"proj"` + User models.User `db:"auth_user"` + ThreadLastReadTime *time.Time `db:"tlri.lastread"` + SubforumLastReadTime *time.Time `db:"slri.lastread"` } posts, err := db.Query(c.Context(), c.Conn, feedPostQuery{}, ` @@ -323,27 +321,25 @@ func fetchAllPosts(c *RequestContext, lineageBuilder *models.CategoryLineageBuil handmade_post AS post JOIN handmade_postversion AS version ON version.id = post.current_id JOIN handmade_thread AS thread ON thread.id = post.thread_id - JOIN handmade_category AS cat ON cat.id = post.category_id JOIN handmade_project AS proj ON proj.id = post.project_id LEFT JOIN handmade_threadlastreadinfo AS tlri ON ( tlri.thread_id = post.thread_id AND tlri.user_id = $1 ) - LEFT JOIN handmade_categorylastreadinfo AS clri ON ( - clri.category_id = post.category_id - AND clri.user_id = $1 + LEFT JOIN handmade_subforumlastreadinfo AS slri ON ( + slri.subforum_id = thread.subforum_id + AND slri.user_id = $1 ) LEFT JOIN auth_user ON post.author_id = auth_user.id - LEFT JOIN handmade_libraryresource as lib_resource ON lib_resource.category_id = post.category_id WHERE - post.category_kind = ANY ($2) + thread.type = ANY ($2) AND post.deleted = FALSE AND post.thread_id IS NOT NULL ORDER BY postdate DESC LIMIT $3 OFFSET $4 `, currentUserID, - []models.CategoryKind{models.CatKindForum, models.CatKindBlog, models.CatKindWiki, models.CatKindLibraryResource}, + []models.ThreadType{models.ThreadTypeForumPost, models.ThreadTypeProjectArticle}, limit, offset, ) @@ -360,7 +356,7 @@ func fetchAllPosts(c *RequestContext, lineageBuilder *models.CategoryLineageBuil hasRead := false if postResult.ThreadLastReadTime != nil && postResult.ThreadLastReadTime.After(postResult.Post.PostDate) { hasRead = true - } else if postResult.CatLastReadTime != nil && postResult.CatLastReadTime.After(postResult.Post.PostDate) { + } else if postResult.SubforumLastReadTime != nil && postResult.SubforumLastReadTime.After(postResult.Post.PostDate) { hasRead = true } @@ -370,7 +366,6 @@ func fetchAllPosts(c *RequestContext, lineageBuilder *models.CategoryLineageBuil &postResult.Thread, &postResult.Post, &postResult.User, - postResult.LibraryResource, !hasRead, true, c.Theme, diff --git a/src/website/forums.go b/src/website/forums.go index dad4951..46156da 100644 --- a/src/website/forums.go +++ b/src/website/forums.go @@ -21,17 +21,17 @@ import ( "github.com/jackc/pgx/v4" ) -type forumCategoryData struct { +type forumData struct { templates.BaseData - NewThreadUrl string - MarkReadUrl string - Threads []templates.ThreadListItem - Pagination templates.Pagination - Subcategories []forumSubcategoryData + NewThreadUrl string + MarkReadUrl string + Threads []templates.ThreadListItem + Pagination templates.Pagination + Subforums []forumSubforumData } -type forumSubcategoryData struct { +type forumSubforumData struct { Name string Url string Threads []templates.ThreadListItem @@ -50,7 +50,7 @@ type editorData struct { PostReplyingTo *templates.Post } -func ForumCategory(c *RequestContext) ResponseData { +func Forum(c *RequestContext) ResponseData { const threadsPerPage = 25 cd, ok := getCommonForumData(c) @@ -58,7 +58,7 @@ func ForumCategory(c *RequestContext) ResponseData { return FourOhFour(c) } - currentSubforumSlugs := cd.LineageBuilder.GetSubforumLineageSlugs(cd.CatID) + currentSubforumSlugs := cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID) c.Perf.StartBlock("SQL", "Fetch count of page threads") numThreads, err := db.QueryInt(c.Context(), c.Conn, @@ -66,10 +66,10 @@ func ForumCategory(c *RequestContext) ResponseData { SELECT COUNT(*) FROM handmade_thread AS thread WHERE - thread.category_id = $1 + thread.subforum_id = $1 AND NOT thread.deleted `, - cd.CatID, + cd.SubforumID, ) if err != nil { panic(oops.New(err, "failed to get count of threads")) @@ -84,11 +84,11 @@ func ForumCategory(c *RequestContext) ResponseData { if pageParsed, err := strconv.Atoi(pageString); err == nil { page = pageParsed } else { - return c.Redirect(hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs, 1), http.StatusSeeOther) + return c.Redirect(hmnurl.BuildForum(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) + return c.Redirect(hmnurl.BuildForum(c.CurrentProject.Slug, currentSubforumSlugs, utils.IntClamp(1, page, numPages)), http.StatusSeeOther) } howManyThreadsToSkip := (page - 1) * threadsPerPage @@ -106,7 +106,7 @@ func ForumCategory(c *RequestContext) ResponseData { FirstUser *models.User `db:"firstuser"` LastUser *models.User `db:"lastuser"` ThreadLastReadTime *time.Time `db:"tlri.lastread"` - CatLastReadTime *time.Time `db:"clri.lastread"` + ForumLastReadTime *time.Time `db:"slri.lastread"` } itMainThreads, err := db.Query(c.Context(), c.Conn, threadQueryResult{}, ` @@ -121,17 +121,17 @@ func ForumCategory(c *RequestContext) ResponseData { 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 + LEFT JOIN handmade_subforumlastreadinfo AS slri ON ( + slri.subforum_id = $1 + AND slri.user_id = $2 ) WHERE - thread.category_id = $1 + thread.subforum_id = $1 AND NOT thread.deleted ORDER BY lastpost.postdate DESC LIMIT $3 OFFSET $4 `, - cd.CatID, + cd.SubforumID, currentUserId, threadsPerPage, howManyThreadsToSkip, @@ -146,13 +146,13 @@ func ForumCategory(c *RequestContext) ResponseData { 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) { + } else if row.ForumLastReadTime != nil && row.ForumLastReadTime.After(row.LastPost.PostDate) { hasRead = true } return templates.ThreadListItem{ Title: row.Thread.Title, - Url: hmnurl.BuildForumThread(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(row.Thread.CategoryID), row.Thread.ID, row.Thread.Title, 1), + Url: hmnurl.BuildForumThread(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(*row.Thread.SubforumID), 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), @@ -169,32 +169,32 @@ func ForumCategory(c *RequestContext) ResponseData { } // --------------------- - // Subcategory things + // Subforum things // --------------------- - var subcats []forumSubcategoryData + var subforums []forumSubforumData if page == 1 { - subcatNodes := cd.CategoryTree[cd.CatID].Children + subforumNodes := cd.SubforumTree[cd.SubforumID].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. + for _, sfNode := range subforumNodes { + c.Perf.StartBlock("SQL", "Fetch count of subforum threads") + // TODO(asaf): [PERF] [MINOR] Consider replacing querying count per subforum with a single query for all subforums with GROUP BY. numThreads, err := db.QueryInt(c.Context(), c.Conn, ` SELECT COUNT(*) FROM handmade_thread AS thread WHERE - thread.category_id = $1 + thread.subforum_id = $1 AND NOT thread.deleted `, - catNode.ID, + sfNode.ID, ) if err != nil { panic(oops.New(err, "failed to get count of threads")) } c.Perf.EndBlock() - c.Perf.StartBlock("SQL", "Fetch subcategory threads") + c.Perf.StartBlock("SQL", "Fetch subforum threads") // TODO(asaf): [PERF] [MINOR] Consider batching these. itThreads, err := db.Query(c.Context(), c.Conn, threadQueryResult{}, ` @@ -209,17 +209,17 @@ func ForumCategory(c *RequestContext) ResponseData { 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 + LEFT JOIN handmade_subforumlastreadinfo AS slri ON ( + slri.subforum_id = $1 + AND slri.user_id = $2 ) WHERE - thread.category_id = $1 + thread.subforum_id = $1 AND NOT thread.deleted ORDER BY lastpost.postdate DESC LIMIT 3 `, - catNode.ID, + sfNode.ID, currentUserId, ) if err != nil { @@ -234,9 +234,9 @@ func ForumCategory(c *RequestContext) ResponseData { threads = append(threads, makeThreadListItem(threadRow)) } - subcats = append(subcats, forumSubcategoryData{ - Name: *catNode.Name, - Url: hmnurl.BuildForumCategory(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(catNode.ID), 1), + subforums = append(subforums, forumSubforumData{ + Name: sfNode.Name, + Url: hmnurl.BuildForum(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(sfNode.ID), 1), Threads: threads, TotalThreads: numThreads, }) @@ -249,53 +249,53 @@ func ForumCategory(c *RequestContext) ResponseData { baseData := getBaseData(c) baseData.Title = c.CurrentProject.Name + " Forums" - baseData.Breadcrumbs = []templates.Breadcrumb{ // TODO(ben): This is wrong; it needs to account for subcategories. + baseData.Breadcrumbs = []templates.Breadcrumb{ // TODO(ben): This is wrong; it needs to account for subforums. { Name: c.CurrentProject.Name, Url: hmnurl.BuildProjectHomepage(c.CurrentProject.Slug), }, { Name: "Forums", - Url: hmnurl.BuildForumCategory(c.CurrentProject.Slug, nil, 1), + Url: hmnurl.BuildForum(c.CurrentProject.Slug, nil, 1), Current: true, }, } - currentSubforums := cd.LineageBuilder.GetSubforumLineage(cd.CatID) + currentSubforums := cd.LineageBuilder.GetSubforumLineage(cd.SubforumID) 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), + Name: subforum.Name, + Url: hmnurl.BuildForum(c.CurrentProject.Slug, currentSubforumSlugs[0:i+1], 1), }) } var res ResponseData - res.MustWriteTemplate("forum_category.html", forumCategoryData{ + res.MustWriteTemplate("forum.html", forumData{ BaseData: baseData, NewThreadUrl: hmnurl.BuildForumNewThread(c.CurrentProject.Slug, currentSubforumSlugs, false), - MarkReadUrl: hmnurl.BuildForumCategoryMarkRead(cd.CatID), + MarkReadUrl: hmnurl.BuildForumMarkRead(cd.SubforumID), 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)), + FirstUrl: hmnurl.BuildForum(c.CurrentProject.Slug, currentSubforumSlugs, 1), + LastUrl: hmnurl.BuildForum(c.CurrentProject.Slug, currentSubforumSlugs, numPages), + NextUrl: hmnurl.BuildForum(c.CurrentProject.Slug, currentSubforumSlugs, utils.IntClamp(1, page+1, numPages)), + PreviousUrl: hmnurl.BuildForum(c.CurrentProject.Slug, currentSubforumSlugs, utils.IntClamp(1, page-1, numPages)), }, - Subcategories: subcats, + Subforums: subforums, }, c.Perf) return res } -func ForumCategoryMarkRead(c *RequestContext) ResponseData { - c.Perf.StartBlock("SQL", "Fetch category tree") - categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn) - lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree) +func ForumMarkRead(c *RequestContext) ResponseData { + c.Perf.StartBlock("SQL", "Fetch subforum tree") + subforumTree := models.GetFullSubforumTree(c.Context(), c.Conn) + lineageBuilder := models.MakeSubforumLineageBuilder(subforumTree) c.Perf.EndBlock() - catId, err := strconv.Atoi(c.PathParams["catid"]) + sfId, err := strconv.Atoi(c.PathParams["sfid"]) if err != nil { return FourOhFour(c) } @@ -307,28 +307,28 @@ func ForumCategoryMarkRead(c *RequestContext) ResponseData { defer tx.Rollback(c.Context()) // TODO(ben): Rework this logic when we rework blogs, threads, etc. - catIds := []int{catId} - if catId == 0 { + sfIds := []int{sfId} + if sfId == 0 { // Select all categories - type catIdResult struct { - CatID int `db:"id"` + type sfIdResult struct { + SubforumID int `db:"id"` } - cats, err := db.Query(c.Context(), tx, catIdResult{}, + cats, err := db.Query(c.Context(), tx, sfIdResult{}, ` SELECT $columns - FROM handmade_category + FROM handmade_subforum WHERE kind = ANY ($1) `, - []models.CategoryKind{models.CatKindBlog, models.CatKindForum, models.CatKindWiki, models.CatKindLibraryResource}, + []models.ThreadType{models.ThreadTypeProjectArticle, models.ThreadTypeForumPost}, ) if err != nil { return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch category IDs for CLRI")) } catIdResults := cats.ToSlice() - catIds = make([]int, len(catIdResults)) + sfIds = make([]int, len(catIdResults)) for i, res := range catIdResults { - catIds[i] = res.(*catIdResult).CatID + sfIds[i] = res.(*sfIdResult).SubforumID } } @@ -342,7 +342,7 @@ func ForumCategoryMarkRead(c *RequestContext) ResponseData { ON CONFLICT (category_id, user_id) DO UPDATE SET lastread = EXCLUDED.lastread `, - catIds, + sfIds, c.CurrentUser.ID, time.Now(), ) @@ -364,7 +364,7 @@ func ForumCategoryMarkRead(c *RequestContext) ResponseData { category_id = ANY ($1) ) `, - catIds, + sfIds, c.CurrentUser.ID, ) c.Perf.EndBlock() @@ -378,10 +378,10 @@ func ForumCategoryMarkRead(c *RequestContext) ResponseData { } var redirUrl string - if catId == 0 { + if sfId == 0 { redirUrl = hmnurl.BuildFeed() } else { - redirUrl = hmnurl.BuildForumCategory(c.CurrentProject.Slug, lineageBuilder.GetSubforumLineageSlugs(catId), 1) + redirUrl = hmnurl.BuildForum(c.CurrentProject.Slug, lineageBuilder.GetSubforumLineageSlugs(sfId), 1) } return c.Redirect(redirUrl, http.StatusSeeOther) } @@ -392,7 +392,7 @@ type forumThreadData struct { Thread templates.Thread Posts []templates.Post - CategoryUrl string + SubforumUrl string ReplyUrl string Pagination templates.Pagination } @@ -405,7 +405,7 @@ func ForumThread(c *RequestContext) ResponseData { return FourOhFour(c) } - currentSubforumSlugs := cd.LineageBuilder.GetSubforumLineageSlugs(cd.CatID) + currentSubforumSlugs := cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID) thread := cd.FetchThread(c.Context(), c.Conn) @@ -519,7 +519,7 @@ func ForumThread(c *RequestContext) ResponseData { BaseData: baseData, Thread: templates.ThreadToTemplate(thread), Posts: posts, - CategoryUrl: hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs, 1), + SubforumUrl: hmnurl.BuildForum(c.CurrentProject.Slug, currentSubforumSlugs, 1), ReplyUrl: hmnurl.BuildForumPostReply(c.CurrentProject.Slug, currentSubforumSlugs, thread.ID, *thread.FirstID), Pagination: pagination, }, c.Perf) @@ -542,12 +542,10 @@ func ForumPostRedirect(c *RequestContext) ResponseData { FROM handmade_post AS post WHERE - post.category_id = $1 - AND post.thread_id = $2 + post.thread_id = $1 AND NOT post.deleted ORDER BY postdate `, - cd.CatID, cd.ThreadID, ) if err != nil { @@ -588,7 +586,7 @@ func ForumPostRedirect(c *RequestContext) ResponseData { return c.Redirect(hmnurl.BuildForumThreadWithPostHash( c.CurrentProject.Slug, - cd.LineageBuilder.GetSubforumLineageSlugs(cd.CatID), + cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID), cd.ThreadID, threadTitle, page, @@ -610,7 +608,7 @@ func ForumNewThread(c *RequestContext) ResponseData { var res ResponseData res.MustWriteTemplate("editor.html", editorData{ BaseData: baseData, - SubmitUrl: hmnurl.BuildForumNewThread(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.CatID), true), + SubmitUrl: hmnurl.BuildForumNewThread(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID), true), SubmitLabel: "Post New Thread", }, c.Perf) return res @@ -643,13 +641,15 @@ func ForumNewThreadSubmit(c *RequestContext) ResponseData { var threadId int err = tx.QueryRow(c.Context(), ` - INSERT INTO handmade_thread (title, sticky, category_id, first_id, last_id) - VALUES ($1, $2, $3, $4, $5) + INSERT INTO handmade_thread (title, sticky, type, project_id, subforum_id, first_id, last_id) + VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id `, title, sticky, - cd.CatID, + models.ThreadTypeForumPost, + c.CurrentProject.ID, + cd.SubforumID, -1, -1, ).Scan(&threadId) @@ -657,7 +657,7 @@ func ForumNewThreadSubmit(c *RequestContext) ResponseData { panic(oops.New(err, "failed to create thread")) } - postId, _ := createNewForumPostAndVersion(c.Context(), tx, cd.CatID, threadId, c.CurrentUser.ID, c.CurrentProject.ID, unparsed, c.Req.Host, nil) + postId, _ := createNewForumPostAndVersion(c.Context(), tx, threadId, c.CurrentUser.ID, c.CurrentProject.ID, unparsed, c.Req.Host, nil) // Update thread with post id _, err = tx.Exec(c.Context(), @@ -680,7 +680,7 @@ func ForumNewThreadSubmit(c *RequestContext) ResponseData { return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to create new forum thread")) } - newThreadUrl := hmnurl.BuildForumThread(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.CatID), threadId, title, 1) + newThreadUrl := hmnurl.BuildForumThread(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID), threadId, title, 1) return c.Redirect(newThreadUrl, http.StatusSeeOther) } @@ -693,7 +693,7 @@ func ForumPostReply(c *RequestContext) ResponseData { postData := cd.FetchPostAndStuff(c.Context(), c.Conn) baseData := getBaseData(c) - baseData.Title = fmt.Sprintf("Replying to \"%s\" | %s", postData.Thread.Title, *cd.CategoryTree[cd.CatID].Name) + baseData.Title = fmt.Sprintf("Replying to \"%s\" | %s", postData.Thread.Title, cd.SubforumTree[cd.SubforumID].Name) baseData.MathjaxEnabled = true // TODO(ben): Set breadcrumbs @@ -703,7 +703,7 @@ func ForumPostReply(c *RequestContext) ResponseData { var res ResponseData res.MustWriteTemplate("editor.html", editorData{ BaseData: baseData, - SubmitUrl: hmnurl.BuildForumPostReply(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.CatID), cd.ThreadID, cd.PostID), + SubmitUrl: hmnurl.BuildForumPostReply(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID), cd.ThreadID, cd.PostID), SubmitLabel: "Submit Reply", Title: "Replying to post", @@ -729,14 +729,14 @@ func ForumPostReplySubmit(c *RequestContext) ResponseData { unparsed := c.Req.Form.Get("body") - newPostId, _ := createNewForumPostAndVersion(c.Context(), tx, cd.CatID, cd.ThreadID, c.CurrentUser.ID, c.CurrentProject.ID, unparsed, c.Req.Host, &cd.PostID) + newPostId, _ := createNewForumPostAndVersion(c.Context(), tx, cd.ThreadID, c.CurrentUser.ID, c.CurrentProject.ID, unparsed, c.Req.Host, &cd.PostID) err = tx.Commit(c.Context()) if err != nil { return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to reply to forum post")) } - newPostUrl := hmnurl.BuildForumPost(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.CatID), cd.ThreadID, newPostId) + newPostUrl := hmnurl.BuildForumPost(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID), cd.ThreadID, newPostId) return c.Redirect(newPostUrl, http.StatusSeeOther) } @@ -753,7 +753,7 @@ func ForumPostEdit(c *RequestContext) ResponseData { postData := cd.FetchPostAndStuff(c.Context(), c.Conn) baseData := getBaseData(c) - baseData.Title = fmt.Sprintf("Editing \"%s\" | %s", postData.Thread.Title, *cd.CategoryTree[cd.CatID].Name) + baseData.Title = fmt.Sprintf("Editing \"%s\" | %s", postData.Thread.Title, cd.SubforumTree[cd.SubforumID].Name) baseData.MathjaxEnabled = true // TODO(ben): Set breadcrumbs @@ -763,7 +763,7 @@ func ForumPostEdit(c *RequestContext) ResponseData { var res ResponseData res.MustWriteTemplate("editor.html", editorData{ BaseData: baseData, - SubmitUrl: hmnurl.BuildForumPostEdit(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.CatID), cd.ThreadID, cd.PostID), + SubmitUrl: hmnurl.BuildForumPostEdit(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID), cd.ThreadID, cd.PostID), Title: postData.Thread.Title, SubmitLabel: "Submit Edited Post", @@ -801,7 +801,7 @@ func ForumPostEditSubmit(c *RequestContext) ResponseData { return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to edit forum post")) } - postUrl := hmnurl.BuildForumPost(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.CatID), cd.ThreadID, cd.PostID) + postUrl := hmnurl.BuildForumPost(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID), cd.ThreadID, cd.PostID) return c.Redirect(postUrl, http.StatusSeeOther) } @@ -818,7 +818,7 @@ func ForumPostDelete(c *RequestContext) ResponseData { postData := cd.FetchPostAndStuff(c.Context(), c.Conn) baseData := getBaseData(c) - baseData.Title = fmt.Sprintf("Deleting post in \"%s\" | %s", postData.Thread.Title, *cd.CategoryTree[cd.CatID].Name) + baseData.Title = fmt.Sprintf("Deleting post in \"%s\" | %s", postData.Thread.Title, cd.SubforumTree[cd.SubforumID].Name) baseData.MathjaxEnabled = true // TODO(ben): Set breadcrumbs @@ -834,7 +834,7 @@ func ForumPostDelete(c *RequestContext) ResponseData { var res ResponseData res.MustWriteTemplate("forum_post_delete.html", forumPostDeleteData{ BaseData: baseData, - SubmitUrl: hmnurl.BuildForumPostDelete(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.CatID), cd.ThreadID, cd.PostID), + SubmitUrl: hmnurl.BuildForumPostDelete(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID), cd.ThreadID, cd.PostID), Post: templatePost, }, c.Perf) return res @@ -895,7 +895,7 @@ func ForumPostDeleteSubmit(c *RequestContext) ResponseData { return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete thread and posts when deleting the first post")) } - forumUrl := hmnurl.BuildForumCategory(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.CatID), 1) + forumUrl := hmnurl.BuildForum(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID), 1) return c.Redirect(forumUrl, http.StatusSeeOther) } @@ -926,24 +926,23 @@ func ForumPostDeleteSubmit(c *RequestContext) ResponseData { return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete post")) } - threadUrl := hmnurl.BuildForumThread(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.CatID), cd.ThreadID, "", 1) // TODO: Go to the last page of the thread? Or the post before the post we just deleted? + threadUrl := hmnurl.BuildForumThread(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID), cd.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) } -func createNewForumPostAndVersion(ctx context.Context, tx pgx.Tx, catId, threadId, userId, projectId int, unparsedContent string, ipString string, replyId *int) (postId, versionId int) { +func createNewForumPostAndVersion(ctx context.Context, tx pgx.Tx, threadId, userId, projectId int, unparsedContent string, ipString string, replyId *int) (postId, versionId int) { // Create post err := tx.QueryRow(ctx, ` - 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) + INSERT INTO handmade_post (postdate, thread_id, thread_type, current_id, author_id, project_id, reply_id, preview) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id `, time.Now(), - catId, threadId, + models.ThreadTypeForumPost, -1, userId, - models.CatKindForum, projectId, replyId, "", // empty preview, will be updated later @@ -1064,16 +1063,16 @@ func fixThreadPostIds(ctx context.Context, tx pgx.Tx, threadId int) error { type commonForumData struct { c *RequestContext - CatID int - ThreadID int - PostID int + SubforumID int + ThreadID int + PostID int - CategoryTree models.CategoryTree - LineageBuilder *models.CategoryLineageBuilder + SubforumTree models.SubforumTree + LineageBuilder *models.SubforumLineageBuilder } /* -Gets data that is used on basically every forums-related route. Parses path params for category, +Gets data that is used on basically every forums-related route. Parses path params for subforum, thread, and post ids and validates that all those resources do in fact exist. Returns false if any data is invalid and you should return a 404. @@ -1082,25 +1081,25 @@ func getCommonForumData(c *RequestContext) (commonForumData, bool) { c.Perf.StartBlock("FORUMS", "Fetch common forum data") defer c.Perf.EndBlock() - c.Perf.StartBlock("SQL", "Fetch category tree") - categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn) - lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree) + c.Perf.StartBlock("SQL", "Fetch subforum tree") + subforumTree := models.GetFullSubforumTree(c.Context(), c.Conn) + lineageBuilder := models.MakeSubforumLineageBuilder(subforumTree) c.Perf.EndBlock() res := commonForumData{ c: c, - CategoryTree: categoryTree, + SubforumTree: subforumTree, LineageBuilder: lineageBuilder, } - if cats, hasCats := c.PathParams["cats"]; hasCats { - catId, valid := validateSubforums(lineageBuilder, c.CurrentProject, cats) + if subforums, hasSubforums := c.PathParams["subforums"]; hasSubforums { + sfId, valid := validateSubforums(lineageBuilder, c.CurrentProject, subforums) if !valid { return commonForumData{}, false } - res.CatID = catId + res.SubforumID = sfId - // No need to validate cat here; it's handled by validateSubforums. + // No need to validate that subforum exists here; it's handled by validateSubforums. } if threadIdStr, hasThreadId := c.PathParams["threadid"]; hasThreadId { @@ -1117,10 +1116,10 @@ func getCommonForumData(c *RequestContext) (commonForumData, bool) { FROM handmade_thread WHERE id = $1 - AND category_id = $2 + AND subforum_id = $2 `, res.ThreadID, - res.CatID, + res.SubforumID, ) c.Perf.EndBlock() if err != nil { @@ -1146,11 +1145,9 @@ func getCommonForumData(c *RequestContext) (commonForumData, bool) { WHERE id = $1 AND thread_id = $2 - AND category_id = $3 `, res.PostID, res.ThreadID, - res.CatID, ) c.Perf.EndBlock() if err != nil { @@ -1180,14 +1177,14 @@ func (cd *commonForumData) FetchThread(ctx context.Context, connOrTx db.ConnOrTx SELECT $columns FROM handmade_thread AS thread - JOIN handmade_category AS cat ON cat.id = thread.category_id + JOIN handmade_subforum AS sf ON sf.id = thread.subforum_id WHERE thread.id = $1 AND NOT thread.deleted - AND cat.id = $2 + AND sf.id = $2 `, cd.ThreadID, - cd.CatID, // NOTE(asaf): This verifies that the requested thread is under the requested subforum. + cd.SubforumID, // NOTE(asaf): This verifies that the requested thread is under the requested subforum. ) cd.c.Perf.EndBlock() if err != nil { @@ -1225,12 +1222,10 @@ func (cd *commonForumData) FetchPostAndStuff(ctx context.Context, connOrTx db.Co 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 + post.thread_id = $1 + AND post.id = $2 AND NOT post.deleted `, - cd.CatID, cd.ThreadID, cd.PostID, ) @@ -1274,27 +1269,27 @@ func (cd *commonForumData) UserCanEditPost(ctx context.Context, connOrTx db.Conn return result.AuthorID != nil && *result.AuthorID == user.ID } -func validateSubforums(lineageBuilder *models.CategoryLineageBuilder, project *models.Project, catPath string) (int, bool) { +func validateSubforums(lineageBuilder *models.SubforumLineageBuilder, project *models.Project, sfPath string) (int, bool) { if project.ForumID == nil { return -1, false } - subforumCatId := *project.ForumID - if len(catPath) == 0 { - return subforumCatId, true + subforumId := *project.ForumID + if len(sfPath) == 0 { + return subforumId, true } - catPath = strings.ToLower(catPath) + sfPath = strings.ToLower(sfPath) valid := false - catSlugs := strings.Split(catPath, "/") - lastSlug := catSlugs[len(catSlugs)-1] + sfSlugs := strings.Split(sfPath, "/") + lastSlug := sfSlugs[len(sfSlugs)-1] if len(lastSlug) > 0 { - lastSlugCatId := lineageBuilder.FindIdBySlug(project.ID, lastSlug) - if lastSlugCatId != -1 { - subforumSlugs := lineageBuilder.GetSubforumLineageSlugs(lastSlugCatId) + lastSlugSfId := lineageBuilder.FindIdBySlug(project.ID, lastSlug) + if lastSlugSfId != -1 { + subforumSlugs := lineageBuilder.GetSubforumLineageSlugs(lastSlugSfId) allMatch := true for i, subforum := range subforumSlugs { - if subforum != catSlugs[i] { + if subforum != sfSlugs[i] { allMatch = false break } @@ -1302,8 +1297,8 @@ func validateSubforums(lineageBuilder *models.CategoryLineageBuilder, project *m valid = allMatch } if valid { - subforumCatId = lastSlugCatId + subforumId = lastSlugSfId } } - return subforumCatId, valid + return subforumId, valid } diff --git a/src/website/landing.go b/src/website/landing.go index 81a066f..88e38b3 100644 --- a/src/website/landing.go +++ b/src/website/landing.go @@ -74,9 +74,9 @@ func Index(c *RequestContext) ResponseData { allProjects := iterProjects.ToSlice() c.Perf.EndBlock() - c.Perf.StartBlock("SQL", "Fetch category tree") - categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn) - lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree) + c.Perf.StartBlock("SQL", "Fetch subforum tree") + subforumTree := models.GetFullSubforumTree(c.Context(), c.Conn) + lineageBuilder := models.MakeSubforumLineageBuilder(subforumTree) c.Perf.EndBlock() var currentUserId *int @@ -90,12 +90,11 @@ func Index(c *RequestContext) ResponseData { c.Perf.StartBlock("SQL", fmt.Sprintf("Fetch posts for %s", proj.Name)) type projectPostQuery struct { - Post models.Post `db:"post"` - Thread models.Thread `db:"thread"` - User models.User `db:"auth_user"` - LibraryResource *models.LibraryResource `db:"lib_resource"` - ThreadLastReadTime *time.Time `db:"tlri.lastread"` - CatLastReadTime *time.Time `db:"clri.lastread"` + Post models.Post `db:"post"` + Thread models.Thread `db:"thread"` + User models.User `db:"auth_user"` + ThreadLastReadTime *time.Time `db:"tlri.lastread"` + SubforumLastReadTime *time.Time `db:"slri.lastread"` } projectPostIter, err := db.Query(c.Context(), c.Conn, projectPostQuery{}, ` @@ -107,22 +106,21 @@ func Index(c *RequestContext) ResponseData { tlri.thread_id = post.thread_id AND tlri.user_id = $1 ) - LEFT JOIN handmade_categorylastreadinfo AS clri ON ( - clri.category_id = post.category_id - AND clri.user_id = $1 + LEFT JOIN handmade_subforumlastreadinfo AS slri ON ( + slri.subforum_id = thread.subforum_id + AND slri.user_id = $1 ) LEFT JOIN auth_user ON post.author_id = auth_user.id - LEFT JOIN handmade_libraryresource as lib_resource ON lib_resource.category_id = post.category_id WHERE post.project_id = $2 - AND post.category_kind IN ($3, $4, $5, $6) + AND post.thread_type = ANY ($3) AND post.deleted = FALSE ORDER BY postdate DESC - LIMIT $7 + LIMIT $4 `, currentUserId, proj.ID, - models.CatKindBlog, models.CatKindForum, models.CatKindWiki, models.CatKindLibraryResource, + []models.ThreadType{models.ThreadTypeProjectArticle, models.ThreadTypeForumPost}, maxPosts, ) c.Perf.EndBlock() @@ -134,7 +132,7 @@ func Index(c *RequestContext) ResponseData { forumsUrl := "" if proj.ForumID != nil { - forumsUrl = hmnurl.BuildForumCategory(proj.Slug, lineageBuilder.GetSubforumLineageSlugs(*proj.ForumID), 1) + forumsUrl = hmnurl.BuildForum(proj.Slug, lineageBuilder.GetSubforumLineageSlugs(*proj.ForumID), 1) } else { c.Logger.Error().Int("ProjectID", proj.ID).Str("ProjectName", proj.Name).Msg("Project fetched by landing page but it doesn't have forums") } @@ -150,12 +148,12 @@ func Index(c *RequestContext) ResponseData { hasRead := false if projectPost.ThreadLastReadTime != nil && projectPost.ThreadLastReadTime.After(projectPost.Post.PostDate) { hasRead = true - } else if projectPost.CatLastReadTime != nil && projectPost.CatLastReadTime.After(projectPost.Post.PostDate) { + } else if projectPost.SubforumLastReadTime != nil && projectPost.SubforumLastReadTime.After(projectPost.Post.PostDate) { hasRead = true } featurable := (!proj.IsHMN() && - projectPost.Post.CategoryKind == models.CatKindBlog && + projectPost.Post.ThreadType == models.ThreadTypeProjectArticle && projectPost.Post.ParentID == nil && landingPageProject.FeaturedPost == nil) @@ -197,7 +195,6 @@ func Index(c *RequestContext) ResponseData { &projectPost.Thread, &projectPost.Post, &projectPost.User, - projectPost.LibraryResource, !hasRead, false, c.Theme, @@ -263,14 +260,14 @@ func Index(c *RequestContext) ResponseData { JOIN handmade_postversion AS ver ON post.current_id = ver.id WHERE post.project_id = $1 - AND post.category_kind = $2 + AND thread.type = $2 AND post.id = thread.first_id AND NOT thread.deleted ORDER BY post.postdate DESC LIMIT 1 `, models.HMNProjectID, - models.CatKindBlog, + models.ThreadTypeProjectArticle, ) if err != nil { return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch news post")) @@ -294,6 +291,7 @@ func Index(c *RequestContext) ResponseData { LEFT JOIN handmade_asset AS asset ON asset.id = snippet.asset_id LEFT JOIN handmade_discordmessage AS discord_message ON discord_message.id = snippet.discord_message_id ORDER BY snippet.when DESC + LIMIT 20 `, ) if err != nil { diff --git a/src/website/post_helper.go b/src/website/post_helper.go index 55bdbf8..93afa22 100644 --- a/src/website/post_helper.go +++ b/src/website/post_helper.go @@ -7,92 +7,74 @@ import ( ) // NOTE(asaf): Please don't use this if you already know the kind of the post beforehand. Just call the appropriate build function. -// You may pass 0 for `libraryResourceId` if the post is not a library resource post. -func UrlForGenericPost(post *models.Post, subforums []string, threadTitle string, libraryResourceId int, projectSlug string) string { - switch post.CategoryKind { - case models.CatKindBlog: +func UrlForGenericPost(thread *models.Thread, post *models.Post, lineageBuilder *models.SubforumLineageBuilder, projectSlug string) string { + switch post.ThreadType { + case models.ThreadTypeProjectArticle: return hmnurl.BuildBlogPost(projectSlug, post.ThreadID, post.ID) - case models.CatKindForum: - return hmnurl.BuildForumPost(projectSlug, subforums, post.ThreadID, post.ID) - case models.CatKindWiki: - if post.ParentID == nil { - // NOTE(asaf): First post on a wiki "thread" is the wiki article itself - return hmnurl.BuildWikiArticle(projectSlug, post.ThreadID, threadTitle) - } else { - // NOTE(asaf): Subsequent posts on a wiki "thread" are wiki talk posts - return hmnurl.BuildWikiTalkPost(projectSlug, post.ThreadID, post.ID) - } - case models.CatKindLibraryResource: - return hmnurl.BuildLibraryPost(projectSlug, libraryResourceId, post.ThreadID, post.ID) + case models.ThreadTypeForumPost: + return hmnurl.BuildForumPost(projectSlug, lineageBuilder.GetSubforumLineageSlugs(*thread.SubforumID), post.ThreadID, post.ID) } return hmnurl.BuildProjectHomepage(projectSlug) } -var PostTypeMap = map[models.CategoryKind][]templates.PostType{ - models.CatKindBlog: []templates.PostType{templates.PostTypeBlogPost, templates.PostTypeBlogComment}, - models.CatKindForum: []templates.PostType{templates.PostTypeForumThread, templates.PostTypeForumReply}, - models.CatKindWiki: []templates.PostType{templates.PostTypeWikiCreate, templates.PostTypeWikiTalk}, - models.CatKindLibraryResource: []templates.PostType{templates.PostTypeLibraryComment, templates.PostTypeLibraryComment}, +var PostTypeMap = map[models.ThreadType][]templates.PostType{ + models.ThreadTypeProjectArticle: {templates.PostTypeBlogPost, templates.PostTypeBlogComment}, + models.ThreadTypeForumPost: {templates.PostTypeForumThread, templates.PostTypeForumReply}, } var PostTypePrefix = map[templates.PostType]string{ - templates.PostTypeBlogPost: "New blog post", - templates.PostTypeBlogComment: "Blog comment", - templates.PostTypeForumThread: "New forum thread", - templates.PostTypeForumReply: "Forum reply", - templates.PostTypeWikiCreate: "New wiki page", - templates.PostTypeWikiTalk: "Wiki comment", - templates.PostTypeWikiEdit: "Wiki edit", - templates.PostTypeLibraryComment: "Library comment", + templates.PostTypeBlogPost: "New blog post", + templates.PostTypeBlogComment: "Blog comment", + templates.PostTypeForumThread: "New forum thread", + templates.PostTypeForumReply: "Forum reply", } -func PostBreadcrumbs(lineageBuilder *models.CategoryLineageBuilder, project *models.Project, post *models.Post, libraryResource *models.LibraryResource) []templates.Breadcrumb { +func PostBreadcrumbs(lineageBuilder *models.SubforumLineageBuilder, project *models.Project, thread *models.Thread) []templates.Breadcrumb { var result []templates.Breadcrumb result = append(result, templates.Breadcrumb{ Name: project.Name, Url: hmnurl.BuildProjectHomepage(project.Slug), }) result = append(result, templates.Breadcrumb{ - Name: CategoryKindDisplayNames[post.CategoryKind], - Url: BuildProjectMainCategoryUrl(project.Slug, post.CategoryKind), + Name: ThreadTypeDisplayNames[thread.Type], + Url: BuildProjectRootResourceUrl(project.Slug, thread.Type), }) - switch post.CategoryKind { - case models.CatKindForum: - subforums := lineageBuilder.GetSubforumLineage(post.CategoryID) - slugs := lineageBuilder.GetSubforumLineageSlugs(post.CategoryID) + switch thread.Type { + case models.ThreadTypeForumPost: + subforums := lineageBuilder.GetSubforumLineage(*thread.SubforumID) + slugs := lineageBuilder.GetSubforumLineageSlugs(*thread.SubforumID) for i, subforum := range subforums { result = append(result, templates.Breadcrumb{ - Name: *subforum.Name, // NOTE(asaf): All subforum categories must have names. - Url: hmnurl.BuildForumCategory(project.Slug, slugs[0:i+1], 1), + Name: subforum.Name, + Url: hmnurl.BuildForum(project.Slug, slugs[0:i+1], 1), }) } - case models.CatKindLibraryResource: - result = append(result, templates.Breadcrumb{ - Name: libraryResource.Name, - Url: hmnurl.BuildLibraryResource(project.Slug, libraryResource.ID), - }) } return result } -// NOTE(asaf): THIS DOESN'T HANDLE WIKI EDIT ITEMS. Wiki edits are PostTextVersions, not Posts. -func MakePostListItem(lineageBuilder *models.CategoryLineageBuilder, project *models.Project, thread *models.Thread, post *models.Post, user *models.User, libraryResource *models.LibraryResource, unread bool, includeBreadcrumbs bool, currentTheme string) templates.PostListItem { +func MakePostListItem( + lineageBuilder *models.SubforumLineageBuilder, + project *models.Project, + thread *models.Thread, + post *models.Post, + user *models.User, + unread bool, + includeBreadcrumbs bool, + currentTheme string, +) templates.PostListItem { var result templates.PostListItem result.Title = thread.Title result.User = templates.UserToTemplate(user, currentTheme) result.Date = post.PostDate result.Unread = unread - libraryResourceId := 0 - if libraryResource != nil { - libraryResourceId = libraryResource.ID - } - result.Url = UrlForGenericPost(post, lineageBuilder.GetSubforumLineageSlugs(post.CategoryID), thread.Title, libraryResourceId, project.Slug) + result.Url = UrlForGenericPost(thread, post, lineageBuilder, project.Slug) result.Preview = post.Preview postType := templates.PostTypeUnknown - postTypeOptions, found := PostTypeMap[post.CategoryKind] + postTypeOptions, found := PostTypeMap[post.ThreadType] if found { var hasParent int if post.ParentID != nil { @@ -104,7 +86,7 @@ func MakePostListItem(lineageBuilder *models.CategoryLineageBuilder, project *mo result.PostTypePrefix = PostTypePrefix[result.PostType] if includeBreadcrumbs { - result.Breadcrumbs = PostBreadcrumbs(lineageBuilder, project, post, libraryResource) + result.Breadcrumbs = PostBreadcrumbs(lineageBuilder, project, thread) } return result diff --git a/src/website/projects.go b/src/website/projects.go index ff45721..2d6efab 100644 --- a/src/website/projects.go +++ b/src/website/projects.go @@ -339,9 +339,9 @@ func ProjectHomepage(c *RequestContext) ResponseData { } c.Perf.EndBlock() - c.Perf.StartBlock("SQL", "Fetch category tree") - categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn) - lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree) + c.Perf.StartBlock("SQL", "Fetch subforum tree") + subforumTree := models.GetFullSubforumTree(c.Context(), c.Conn) + lineageBuilder := models.MakeSubforumLineageBuilder(subforumTree) c.Perf.EndBlock() c.Perf.StartBlock("SQL", "Fetching project timeline") @@ -431,7 +431,6 @@ func ProjectHomepage(c *RequestContext) ResponseData { &post.(*postQuery).Post, &post.(*postQuery).Thread, project, - nil, &post.(*postQuery).Author, c.Theme, )) diff --git a/src/website/routes.go b/src/website/routes.go index 4841c11..3780478 100644 --- a/src/website/routes.go +++ b/src/website/routes.go @@ -156,8 +156,8 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt mainRoutes.GET(hmnurl.RegexForumNewThread, authMiddleware(ForumNewThread)) mainRoutes.POST(hmnurl.RegexForumNewThreadSubmit, authMiddleware(csrfMiddleware(ForumNewThreadSubmit))) mainRoutes.GET(hmnurl.RegexForumThread, ForumThread) - mainRoutes.GET(hmnurl.RegexForumCategory, ForumCategory) - mainRoutes.POST(hmnurl.RegexForumCategoryMarkRead, authMiddleware(csrfMiddleware(ForumCategoryMarkRead))) + mainRoutes.GET(hmnurl.RegexForum, Forum) + mainRoutes.POST(hmnurl.RegexForumMarkRead, authMiddleware(csrfMiddleware(ForumMarkRead))) mainRoutes.GET(hmnurl.RegexForumPost, ForumPostRedirect) mainRoutes.GET(hmnurl.RegexForumPostReply, authMiddleware(ForumPostReply)) mainRoutes.POST(hmnurl.RegexForumPostReply, authMiddleware(csrfMiddleware(ForumPostReplySubmit))) @@ -216,8 +216,7 @@ func getBaseData(c *RequestContext) templates.BaseData { ProjectHomepageUrl: hmnurl.BuildProjectHomepage(c.CurrentProject.Slug), ProjectIndexUrl: hmnurl.BuildProjectIndex(1), BlogUrl: hmnurl.BuildBlog(c.CurrentProject.Slug, 1), - ForumsUrl: hmnurl.BuildForumCategory(c.CurrentProject.Slug, nil, 1), - WikiUrl: hmnurl.BuildWiki(c.CurrentProject.Slug), + ForumsUrl: hmnurl.BuildForum(c.CurrentProject.Slug, nil, 1), LibraryUrl: hmnurl.BuildLibrary(c.CurrentProject.Slug), ManifestoUrl: hmnurl.BuildManifesto(), EpisodeGuideUrl: hmnurl.BuildHomepage(), // TODO(asaf) @@ -231,7 +230,7 @@ func getBaseData(c *RequestContext) templates.BaseData { CodeOfConductUrl: hmnurl.BuildCodeOfConduct(), CommunicationGuidelinesUrl: hmnurl.BuildCommunicationGuidelines(), ProjectIndexUrl: hmnurl.BuildProjectIndex(1), - ForumsUrl: hmnurl.BuildForumCategory(models.HMNProjectSlug, nil, 1), + ForumsUrl: hmnurl.BuildForum(models.HMNProjectSlug, nil, 1), ContactUrl: hmnurl.BuildContactPage(), SitemapUrl: hmnurl.BuildSiteMap(), }, diff --git a/src/website/subforum_helper.go b/src/website/subforum_helper.go new file mode 100644 index 0000000..1e8c13c --- /dev/null +++ b/src/website/subforum_helper.go @@ -0,0 +1,10 @@ +package website + +import ( + "git.handmade.network/hmn/hmn/src/models" +) + +var ThreadTypeDisplayNames = map[models.ThreadType]string{ + models.ThreadTypeProjectArticle: "Blog", + models.ThreadTypeForumPost: "Forums", +} diff --git a/src/website/timeline_helper.go b/src/website/timeline_helper.go index f9367a5..6d6eab6 100644 --- a/src/website/timeline_helper.go +++ b/src/website/timeline_helper.go @@ -10,12 +10,10 @@ import ( "git.handmade.network/hmn/hmn/src/templates" ) -var TimelineTypeMap = map[models.CategoryKind][]templates.TimelineType{ - // { No parent , Has parent } - models.CatKindBlog: {templates.TimelineTypeBlogPost, templates.TimelineTypeBlogComment}, - models.CatKindForum: {templates.TimelineTypeForumThread, templates.TimelineTypeForumReply}, - models.CatKindWiki: {templates.TimelineTypeWikiCreate, templates.TimelineTypeWikiTalk}, - models.CatKindLibraryResource: {templates.TimelineTypeLibraryComment, templates.TimelineTypeLibraryComment}, +var TimelineTypeMap = map[models.ThreadType][]templates.TimelineType{ + // { No parent , Has parent } + models.ThreadTypeProjectArticle: {templates.TimelineTypeBlogPost, templates.TimelineTypeBlogComment}, + models.ThreadTypeForumPost: {templates.TimelineTypeForumThread, templates.TimelineTypeForumReply}, } var TimelineItemClassMap = map[templates.TimelineType]string{ @@ -27,12 +25,6 @@ var TimelineItemClassMap = map[templates.TimelineType]string{ templates.TimelineTypeBlogPost: "blogs", templates.TimelineTypeBlogComment: "blogs", - templates.TimelineTypeWikiCreate: "wiki", - templates.TimelineTypeWikiEdit: "wiki", - templates.TimelineTypeWikiTalk: "wiki", - - templates.TimelineTypeLibraryComment: "library", - templates.TimelineTypeSnippetImage: "snippets", templates.TimelineTypeSnippetVideo: "snippets", templates.TimelineTypeSnippetAudio: "snippets", @@ -48,21 +40,15 @@ var TimelineTypeTitleMap = map[templates.TimelineType]string{ templates.TimelineTypeBlogPost: "New blog post", templates.TimelineTypeBlogComment: "Blog comment", - templates.TimelineTypeWikiCreate: "New wiki article", - templates.TimelineTypeWikiEdit: "Wiki edit", - templates.TimelineTypeWikiTalk: "Wiki talk", - - templates.TimelineTypeLibraryComment: "Library comment", - templates.TimelineTypeSnippetImage: "Snippet", templates.TimelineTypeSnippetVideo: "Snippet", templates.TimelineTypeSnippetAudio: "Snippet", templates.TimelineTypeSnippetYoutube: "Snippet", } -func PostToTimelineItem(lineageBuilder *models.CategoryLineageBuilder, post *models.Post, thread *models.Thread, project *models.Project, libraryResource *models.LibraryResource, owner *models.User, currentTheme string) templates.TimelineItem { +func PostToTimelineItem(lineageBuilder *models.SubforumLineageBuilder, post *models.Post, thread *models.Thread, project *models.Project, owner *models.User, currentTheme string) templates.TimelineItem { itemType := templates.TimelineTypeUnknown - typeByCatKind, found := TimelineTypeMap[post.CategoryKind] + typeByCatKind, found := TimelineTypeMap[post.ThreadType] if found { hasParent := 0 if post.ParentID != nil { @@ -71,17 +57,12 @@ func PostToTimelineItem(lineageBuilder *models.CategoryLineageBuilder, post *mod itemType = typeByCatKind[hasParent] } - libraryResourceId := 0 - if libraryResource != nil { - libraryResourceId = libraryResource.ID - } - return templates.TimelineItem{ Type: itemType, TypeTitle: TimelineTypeTitleMap[itemType], Class: TimelineItemClassMap[itemType], Date: post.PostDate, - Url: UrlForGenericPost(post, lineageBuilder.GetSubforumLineageSlugs(post.CategoryID), thread.Title, libraryResourceId, project.Slug), + Url: UrlForGenericPost(thread, post, lineageBuilder, project.Slug), OwnerAvatarUrl: templates.UserAvatarUrl(owner, currentTheme), OwnerName: templates.UserDisplayName(owner), @@ -89,25 +70,7 @@ func PostToTimelineItem(lineageBuilder *models.CategoryLineageBuilder, post *mod Description: "", // NOTE(asaf): No description for posts Title: thread.Title, - Breadcrumbs: PostBreadcrumbs(lineageBuilder, project, post, libraryResource), - } -} - -func PostVersionToWikiTimelineItem(lineageBuilder *models.CategoryLineageBuilder, version *models.PostVersion, post *models.Post, thread *models.Thread, project *models.Project, owner *models.User, currentTheme string) templates.TimelineItem { - return templates.TimelineItem{ - Type: templates.TimelineTypeWikiEdit, - TypeTitle: TimelineTypeTitleMap[templates.TimelineTypeWikiEdit], - Class: TimelineItemClassMap[templates.TimelineTypeWikiEdit], - Date: version.Date, - Url: hmnurl.BuildWikiArticle(project.Slug, thread.ID, thread.Title), - - OwnerAvatarUrl: templates.UserAvatarUrl(owner, currentTheme), - OwnerName: templates.UserDisplayName(owner), - OwnerUrl: hmnurl.BuildUserProfile(owner.Username), - Description: "", // NOTE(asaf): No description for posts - - Title: thread.Title, - Breadcrumbs: PostBreadcrumbs(lineageBuilder, project, post, nil), + Breadcrumbs: PostBreadcrumbs(lineageBuilder, project, thread), } } diff --git a/src/website/urls.go b/src/website/urls.go index b402a35..f7955f6 100644 --- a/src/website/urls.go +++ b/src/website/urls.go @@ -5,16 +5,12 @@ import ( "git.handmade.network/hmn/hmn/src/models" ) -func BuildProjectMainCategoryUrl(projectSlug string, kind models.CategoryKind) string { +func BuildProjectRootResourceUrl(projectSlug string, kind models.ThreadType) string { switch kind { - case models.CatKindBlog: + case models.ThreadTypeProjectArticle: return hmnurl.BuildBlog(projectSlug, 1) - case models.CatKindForum: - return hmnurl.BuildForumCategory(projectSlug, nil, 1) - case models.CatKindWiki: - return hmnurl.BuildWiki(projectSlug) - case models.CatKindLibraryResource: - return hmnurl.BuildLibrary(projectSlug) + case models.ThreadTypeForumPost: + return hmnurl.BuildForum(projectSlug, nil, 1) } return hmnurl.BuildProjectHomepage(projectSlug) } diff --git a/src/website/user.go b/src/website/user.go index cf5da30..603cc49 100644 --- a/src/website/user.go +++ b/src/website/user.go @@ -20,8 +20,6 @@ type UserProfileTemplateData struct { TimelineItems []templates.TimelineItem NumForums int NumBlogs int - NumWiki int - NumLibrary int NumSnippets int } @@ -115,10 +113,9 @@ func UserProfile(c *RequestContext) ResponseData { c.Perf.EndBlock() type postQuery struct { - Post models.Post `db:"post"` - Thread models.Thread `db:"thread"` - LibraryResource *models.LibraryResource `db:"lib_resource"` - Project models.Project `db:"project"` + Post models.Post `db:"post"` + Thread models.Thread `db:"thread"` + Project models.Project `db:"project"` } c.Perf.StartBlock("SQL", "Fetch posts") postQueryResult, err := db.Query(c.Context(), c.Conn, postQuery{}, @@ -128,7 +125,6 @@ func UserProfile(c *RequestContext) ResponseData { handmade_post AS post INNER JOIN handmade_thread AS thread ON thread.id = post.thread_id INNER JOIN handmade_project AS project ON project.id = post.project_id - LEFT JOIN handmade_libraryresource AS lib_resource ON lib_resource.category_id = post.category_id WHERE post.author_id = $1 AND project.lifecycle = ANY ($2) @@ -142,37 +138,6 @@ func UserProfile(c *RequestContext) ResponseData { postQuerySlice := postQueryResult.ToSlice() c.Perf.EndBlock() - type wikiEditQuery struct { - PostVersion models.PostVersion `db:"version"` - Post models.Post `db:"post"` - Thread models.Thread `db:"thread"` - Project models.Project `db:"project"` - } - c.Perf.StartBlock("SQL", "Fetch wiki edits") - wikiEditQueryResult, err := db.Query(c.Context(), c.Conn, wikiEditQuery{}, - ` - SELECT $columns - FROM - handmade_postversion AS version - INNER JOIN handmade_post AS post ON post.id = version.post_id - INNER JOIN handmade_thread AS thread on thread.id = post.thread_id - INNER JOIN handmade_project AS project ON project.id = post.project_id - WHERE - version.editor_id = $1 - AND post.parent_id IS NULL - AND post.category_kind = $2 - AND project.lifecycle = ANY ($3) - `, - profileUser.ID, - models.CatKindWiki, - models.VisibleProjectLifecycles, - ) - if err != nil { - return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch wiki edits for user: %s", username)) - } - wikiEditQuerySlice := wikiEditQueryResult.ToSlice() - c.Perf.EndBlock() - type snippetQuery struct { Snippet models.Snippet `db:"snippet"` Asset *models.Asset `db:"asset"` @@ -197,17 +162,15 @@ func UserProfile(c *RequestContext) ResponseData { snippetQuerySlice := snippetQueryResult.ToSlice() c.Perf.EndBlock() - c.Perf.StartBlock("SQL", "Fetch category tree") - categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn) - lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree) + c.Perf.StartBlock("SQL", "Fetch subforum tree") + subforumTree := models.GetFullSubforumTree(c.Context(), c.Conn) + lineageBuilder := models.MakeSubforumLineageBuilder(subforumTree) c.Perf.EndBlock() c.Perf.StartBlock("PROFILE", "Construct timeline items") - timelineItems := make([]templates.TimelineItem, 0, len(postQuerySlice)+len(wikiEditQuerySlice)+len(snippetQuerySlice)) + timelineItems := make([]templates.TimelineItem, 0, len(postQuerySlice)+len(snippetQuerySlice)) numForums := 0 numBlogs := 0 - numWiki := len(wikiEditQuerySlice) - numLibrary := 0 numSnippets := len(snippetQuerySlice) for _, postRow := range postQuerySlice { @@ -217,7 +180,6 @@ func UserProfile(c *RequestContext) ResponseData { &postData.Post, &postData.Thread, &postData.Project, - postData.LibraryResource, profileUser, c.Theme, ) @@ -226,10 +188,6 @@ func UserProfile(c *RequestContext) ResponseData { numForums += 1 case templates.TimelineTypeBlogPost, templates.TimelineTypeBlogComment: numBlogs += 1 - case templates.TimelineTypeWikiCreate, templates.TimelineTypeWikiTalk: - numWiki += 1 - case templates.TimelineTypeLibraryComment: - numLibrary += 1 } if timelineItem.Type != templates.TimelineTypeUnknown { timelineItems = append(timelineItems, timelineItem) @@ -238,20 +196,6 @@ func UserProfile(c *RequestContext) ResponseData { } } - for _, wikiEditRow := range wikiEditQuerySlice { - wikiEditData := wikiEditRow.(*wikiEditQuery) - timelineItem := PostVersionToWikiTimelineItem( - lineageBuilder, - &wikiEditData.PostVersion, - &wikiEditData.Post, - &wikiEditData.Thread, - &wikiEditData.Project, - profileUser, - c.Theme, - ) - timelineItems = append(timelineItems, timelineItem) - } - for _, snippetRow := range snippetQuerySlice { snippetData := snippetRow.(*snippetQuery) timelineItem := SnippetToTimelineItem( @@ -282,8 +226,6 @@ func UserProfile(c *RequestContext) ResponseData { TimelineItems: timelineItems, NumForums: numForums, NumBlogs: numBlogs, - NumWiki: numWiki, - NumLibrary: numLibrary, NumSnippets: numSnippets, }, c.Perf) return res