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.
This commit is contained in:
parent
15ff1de6fc
commit
8ecb4a7173
|
@ -4,14 +4,10 @@ const TimelineTypes = {
|
||||||
FORUM_REPLY: 2,
|
FORUM_REPLY: 2,
|
||||||
BLOG_POST: 3,
|
BLOG_POST: 3,
|
||||||
BLOG_COMMENT: 4,
|
BLOG_COMMENT: 4,
|
||||||
WIKI_CREATE: 5,
|
SNIPPET_IMAGE: 5,
|
||||||
WIKI_EDIT: 6,
|
SNIPPET_VIDEO: 6,
|
||||||
WIKI_TALK: 7,
|
SNIPPET_AUDIO: 7,
|
||||||
LIBRARY_COMMENT: 8,
|
SNIPPET_YOUTUBE: 8
|
||||||
SNIPPET_IMAGE: 9,
|
|
||||||
SNIPPET_VIDEO: 10,
|
|
||||||
SNIPPET_AUDIO: 11,
|
|
||||||
SNIPPET_YOUTUBE: 12
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const showcaseItemTemplate = makeTemplateCloner("showcase_item");
|
const showcaseItemTemplate = makeTemplateCloner("showcase_item");
|
||||||
|
|
|
@ -8634,11 +8634,6 @@ input[type=submit] {
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
max-width: 80em; }
|
max-width: 80em; }
|
||||||
|
|
||||||
.wiki .post {
|
|
||||||
padding: 0;
|
|
||||||
margin: auto;
|
|
||||||
max-width: 70em; }
|
|
||||||
|
|
||||||
.post .contents h1, .post .contents h2 {
|
.post .contents h1, .post .contents h2 {
|
||||||
margin: 20px 0px; }
|
margin: 20px 0px; }
|
||||||
|
|
||||||
|
@ -8750,56 +8745,11 @@ input[type=submit] {
|
||||||
.blog .post-list .post:nth-child(even) {
|
.blog .post-list .post:nth-child(even) {
|
||||||
background-color: transparent; }
|
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 {
|
.featured-post .meta .avatar-icon {
|
||||||
left: -60px;
|
left: -60px;
|
||||||
bottom: -5px; }
|
bottom: -5px; }
|
||||||
|
|
||||||
.blog .body blockquote,
|
.blog .body blockquote {
|
||||||
.wiki .body blockquote {
|
|
||||||
padding-top: 1px;
|
padding-top: 1px;
|
||||||
padding-bottom: 1px; }
|
padding-bottom: 1px; }
|
||||||
|
|
||||||
|
@ -9446,9 +9396,6 @@ span.icon-rss::before {
|
||||||
.timeline.no-blogs .blogs {
|
.timeline.no-blogs .blogs {
|
||||||
display: none; }
|
display: none; }
|
||||||
|
|
||||||
.timeline.no-wiki .wiki {
|
|
||||||
display: none; }
|
|
||||||
|
|
||||||
.timeline.no-library .library {
|
.timeline.no-library .library {
|
||||||
display: none; }
|
display: none; }
|
||||||
|
|
||||||
|
|
|
@ -300,8 +300,6 @@ will throw an error.
|
||||||
--irc-users-popout-background: #181818;
|
--irc-users-popout-background: #181818;
|
||||||
--irc-users-popout-border-color-left: #444;
|
--irc-users-popout-border-color-left: #444;
|
||||||
--irc-users-popout-border-color-right: #333;
|
--irc-users-popout-border-color-right: #333;
|
||||||
--wiki-border-color: #444;
|
|
||||||
--wiki-toc-number-color: #bbb;
|
|
||||||
--code-line-number-color: #444;
|
--code-line-number-color: #444;
|
||||||
--library-star-btn-background: #252525;
|
--library-star-btn-background: #252525;
|
||||||
--library-star-btn-border-color: #bbb;
|
--library-star-btn-border-color: #bbb;
|
||||||
|
|
|
@ -318,8 +318,6 @@ will throw an error.
|
||||||
--irc-users-popout-background: #fff;
|
--irc-users-popout-background: #fff;
|
||||||
--irc-users-popout-border-color-left: #bbb;
|
--irc-users-popout-border-color-left: #bbb;
|
||||||
--irc-users-popout-border-color-right: #ccc;
|
--irc-users-popout-border-color-right: #ccc;
|
||||||
--wiki-border-color: #aaa;
|
|
||||||
--wiki-toc-number-color: #333;
|
|
||||||
--code-line-number-color: #777;
|
--code-line-number-color: #777;
|
||||||
--library-star-btn-background: #fff;
|
--library-star-btn-background: #fff;
|
||||||
--library-star-btn-border-color: #999;
|
--library-star-btn-border-color: #999;
|
||||||
|
|
|
@ -22,7 +22,7 @@ import (
|
||||||
Values of these kinds are ok to query even if they are not directly understood by pgtype.
|
Values of these kinds are ok to query even if they are not directly understood by pgtype.
|
||||||
This is common for custom types like:
|
This is common for custom types like:
|
||||||
|
|
||||||
type CategoryKind int
|
type ThreadType int
|
||||||
*/
|
*/
|
||||||
var queryableKinds = []reflect.Kind{
|
var queryableKinds = []reflect.Kind{
|
||||||
reflect.Int,
|
reflect.Int,
|
||||||
|
|
|
@ -145,22 +145,22 @@ func TestPodcastRSS(t *testing.T) {
|
||||||
AssertRegexMatch(t, BuildPodcastRSS(""), RegexPodcastRSS, nil)
|
AssertRegexMatch(t, BuildPodcastRSS(""), RegexPodcastRSS, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestForumCategory(t *testing.T) {
|
func TestForum(t *testing.T) {
|
||||||
AssertRegexMatch(t, BuildForumCategory("", nil, 1), RegexForumCategory, nil)
|
AssertRegexMatch(t, BuildForum("", nil, 1), RegexForum, nil)
|
||||||
AssertRegexMatch(t, BuildForumCategory("", []string{"wip"}, 2), RegexForumCategory, map[string]string{"cats": "wip", "page": "2"})
|
AssertRegexMatch(t, BuildForum("", []string{"wip"}, 2), RegexForum, map[string]string{"subforums": "wip", "page": "2"})
|
||||||
AssertRegexMatch(t, BuildForumCategory("", []string{"sub", "wip"}, 2), RegexForumCategory, map[string]string{"cats": "sub/wip", "page": "2"})
|
AssertRegexMatch(t, BuildForum("", []string{"sub", "wip"}, 2), RegexForum, map[string]string{"subforums": "sub/wip", "page": "2"})
|
||||||
AssertSubdomain(t, BuildForumCategory("hmn", nil, 1), "")
|
AssertSubdomain(t, BuildForum("hmn", nil, 1), "")
|
||||||
AssertSubdomain(t, BuildForumCategory("", nil, 1), "")
|
AssertSubdomain(t, BuildForum("", nil, 1), "")
|
||||||
AssertSubdomain(t, BuildForumCategory("hero", nil, 1), "hero")
|
AssertSubdomain(t, BuildForum("hero", nil, 1), "hero")
|
||||||
assert.Panics(t, func() { BuildForumCategory("", nil, 0) })
|
assert.Panics(t, func() { BuildForum("", nil, 0) })
|
||||||
assert.Panics(t, func() { BuildForumCategory("", []string{"", "wip"}, 1) })
|
assert.Panics(t, func() { BuildForum("", []string{"", "wip"}, 1) })
|
||||||
assert.Panics(t, func() { BuildForumCategory("", []string{" ", "wip"}, 1) })
|
assert.Panics(t, func() { BuildForum("", []string{" ", "wip"}, 1) })
|
||||||
assert.Panics(t, func() { BuildForumCategory("", []string{"wip/jobs"}, 1) })
|
assert.Panics(t, func() { BuildForum("", []string{"wip/jobs"}, 1) })
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestForumNewThread(t *testing.T) {
|
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"}, false), RegexForumNewThread, map[string]string{"subforums": "sub/wip"})
|
||||||
AssertRegexMatch(t, BuildForumNewThread("", []string{"sub", "wip"}, true), RegexForumNewThreadSubmit, map[string]string{"cats": "sub/wip"})
|
AssertRegexMatch(t, BuildForumNewThread("", []string{"sub", "wip"}, true), RegexForumNewThreadSubmit, map[string]string{"subforums": "sub/wip"})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestForumThread(t *testing.T) {
|
func TestForumThread(t *testing.T) {
|
||||||
|
@ -197,12 +197,6 @@ func TestForumPostReply(t *testing.T) {
|
||||||
AssertSubdomain(t, BuildForumPostReply("hero", nil, 1, 2), "hero")
|
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) {
|
func TestBlog(t *testing.T) {
|
||||||
AssertRegexMatch(t, BuildBlog("", 1), RegexBlog, nil)
|
AssertRegexMatch(t, BuildBlog("", 1), RegexBlog, nil)
|
||||||
AssertRegexMatch(t, BuildBlog("", 2), RegexBlog, map[string]string{"page": "2"})
|
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")
|
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) {
|
func TestLibrary(t *testing.T) {
|
||||||
AssertRegexMatch(t, BuildLibrary(""), RegexLibrary, nil)
|
AssertRegexMatch(t, BuildLibrary(""), RegexLibrary, nil)
|
||||||
AssertSubdomain(t, BuildLibrary("hero"), "hero")
|
AssertSubdomain(t, BuildLibrary("hero"), "hero")
|
||||||
|
@ -344,38 +262,6 @@ func TestLibraryResource(t *testing.T) {
|
||||||
AssertSubdomain(t, BuildLibraryResource("hero", 1), "hero")
|
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) {
|
func TestProjectCSS(t *testing.T) {
|
||||||
AssertRegexMatch(t, BuildProjectCSS("000000"), RegexProjectCSS, nil)
|
AssertRegexMatch(t, BuildProjectCSS("000000"), RegexProjectCSS, nil)
|
||||||
}
|
}
|
||||||
|
@ -395,8 +281,8 @@ func TestPublic(t *testing.T) {
|
||||||
AssertRegexMatch(t, BuildUserFile("mylogo.png"), RegexPublic, nil)
|
AssertRegexMatch(t, BuildUserFile("mylogo.png"), RegexPublic, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestForumCategoryMarkRead(t *testing.T) {
|
func TestForumMarkRead(t *testing.T) {
|
||||||
AssertRegexMatch(t, BuildForumCategoryMarkRead(5), RegexForumCategoryMarkRead, map[string]string{"catid": "5"})
|
AssertRegexMatch(t, BuildForumMarkRead(5), RegexForumMarkRead, map[string]string{"sfid": "5"})
|
||||||
}
|
}
|
||||||
|
|
||||||
func AssertSubdomain(t *testing.T, fullUrl string, expectedSubdomain string) {
|
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)
|
match := regex.FindStringSubmatch(requestPath)
|
||||||
assert.Nilf(t, match, "Url matched regex: [%s] vs [%s]", requestPath, regex.String())
|
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")
|
||||||
|
}
|
||||||
|
|
|
@ -305,17 +305,17 @@ func BuildPodcastEpisodeFile(projectSlug string, filename string) string {
|
||||||
* Forums
|
* Forums
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// TODO(asaf): This also matches urls generated by BuildForumThread (/t/ is identified as a cat, and the threadid as a page)
|
// 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 Category in the router, but should we enforce it here?
|
// This shouldn't be a problem since we will match Thread before Subforum in the router, but should we enforce it here?
|
||||||
var RegexForumCategory = regexp.MustCompile(`^/forums(/(?P<cats>[^\d/]+(/[^\d]+)*))?(/(?P<page>\d+))?$`)
|
var RegexForum = regexp.MustCompile(`^/forums(/(?P<subforums>[^\d/]+(/[^\d]+)*))?(/(?P<page>\d+))?$`)
|
||||||
|
|
||||||
func BuildForumCategory(projectSlug string, subforums []string, page int) string {
|
func BuildForum(projectSlug string, subforums []string, page int) string {
|
||||||
defer CatchPanic()
|
defer CatchPanic()
|
||||||
if page < 1 {
|
if page < 1 {
|
||||||
panic(oops.New(nil, "Invalid forum thread page (%d), must be >= 1", page))
|
panic(oops.New(nil, "Invalid forum thread page (%d), must be >= 1", page))
|
||||||
}
|
}
|
||||||
|
|
||||||
builder := buildForumCategoryPath(subforums)
|
builder := buildSubforumPath(subforums)
|
||||||
|
|
||||||
if page > 1 {
|
if page > 1 {
|
||||||
builder.WriteRune('/')
|
builder.WriteRune('/')
|
||||||
|
@ -325,12 +325,12 @@ func BuildForumCategory(projectSlug string, subforums []string, page int) string
|
||||||
return ProjectUrl(builder.String(), nil, projectSlug)
|
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||||
}
|
}
|
||||||
|
|
||||||
var RegexForumNewThread = regexp.MustCompile(`^/forums(/(?P<cats>[^\d/]+(/[^\d]+)*))?/t/new$`)
|
var RegexForumNewThread = regexp.MustCompile(`^/forums(/(?P<subforums>[^\d/]+(/[^\d]+)*))?/t/new$`)
|
||||||
var RegexForumNewThreadSubmit = regexp.MustCompile(`^/forums(/(?P<cats>[^\d/]+(/[^\d]+)*))?/t/new/submit$`)
|
var RegexForumNewThreadSubmit = regexp.MustCompile(`^/forums(/(?P<subforums>[^\d/]+(/[^\d]+)*))?/t/new/submit$`)
|
||||||
|
|
||||||
func BuildForumNewThread(projectSlug string, subforums []string, submit bool) string {
|
func BuildForumNewThread(projectSlug string, subforums []string, submit bool) string {
|
||||||
defer CatchPanic()
|
defer CatchPanic()
|
||||||
builder := buildForumCategoryPath(subforums)
|
builder := buildSubforumPath(subforums)
|
||||||
builder.WriteString("/t/new")
|
builder.WriteString("/t/new")
|
||||||
if submit {
|
if submit {
|
||||||
builder.WriteString("/submit")
|
builder.WriteString("/submit")
|
||||||
|
@ -339,7 +339,7 @@ func BuildForumNewThread(projectSlug string, subforums []string, submit bool) st
|
||||||
return ProjectUrl(builder.String(), nil, projectSlug)
|
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||||
}
|
}
|
||||||
|
|
||||||
var RegexForumThread = regexp.MustCompile(`^/forums(/(?P<cats>[^\d/]+(/[^\d]+)*))?/t/(?P<threadid>\d+)(-([^/]+))?(/(?P<page>\d+))?$`)
|
var RegexForumThread = regexp.MustCompile(`^/forums(/(?P<subforums>[^\d/]+(/[^\d]+)*))?/t/(?P<threadid>\d+)(-([^/]+))?(/(?P<page>\d+))?$`)
|
||||||
|
|
||||||
func BuildForumThread(projectSlug string, subforums []string, threadId int, title string, page int) string {
|
func BuildForumThread(projectSlug string, subforums []string, threadId int, title string, page int) string {
|
||||||
defer CatchPanic()
|
defer CatchPanic()
|
||||||
|
@ -355,7 +355,7 @@ func BuildForumThreadWithPostHash(projectSlug string, subforums []string, thread
|
||||||
return ProjectUrlWithFragment(builder.String(), nil, projectSlug, strconv.Itoa(postId))
|
return ProjectUrlWithFragment(builder.String(), nil, projectSlug, strconv.Itoa(postId))
|
||||||
}
|
}
|
||||||
|
|
||||||
var RegexForumPost = regexp.MustCompile(`^/forums(/(?P<cats>[^\d/]+(/[^\d]+)*))?/t/(?P<threadid>\d+)/p/(?P<postid>\d+)$`)
|
var RegexForumPost = regexp.MustCompile(`^/forums(/(?P<subforums>[^\d/]+(/[^\d]+)*))?/t/(?P<threadid>\d+)/p/(?P<postid>\d+)$`)
|
||||||
|
|
||||||
func BuildForumPost(projectSlug string, subforums []string, threadId int, postId int) string {
|
func BuildForumPost(projectSlug string, subforums []string, threadId int, postId int) string {
|
||||||
defer CatchPanic()
|
defer CatchPanic()
|
||||||
|
@ -364,7 +364,7 @@ func BuildForumPost(projectSlug string, subforums []string, threadId int, postId
|
||||||
return ProjectUrl(builder.String(), nil, projectSlug)
|
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||||
}
|
}
|
||||||
|
|
||||||
var RegexForumPostDelete = regexp.MustCompile(`^/forums(/(?P<cats>[^\d/]+(/[^\d]+)*))?/t/(?P<threadid>\d+)/p/(?P<postid>\d+)/delete$`)
|
var RegexForumPostDelete = regexp.MustCompile(`^/forums(/(?P<subforums>[^\d/]+(/[^\d]+)*))?/t/(?P<threadid>\d+)/p/(?P<postid>\d+)/delete$`)
|
||||||
|
|
||||||
func BuildForumPostDelete(projectSlug string, subforums []string, threadId int, postId int) string {
|
func BuildForumPostDelete(projectSlug string, subforums []string, threadId int, postId int) string {
|
||||||
defer CatchPanic()
|
defer CatchPanic()
|
||||||
|
@ -373,7 +373,7 @@ func BuildForumPostDelete(projectSlug string, subforums []string, threadId int,
|
||||||
return ProjectUrl(builder.String(), nil, projectSlug)
|
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||||
}
|
}
|
||||||
|
|
||||||
var RegexForumPostEdit = regexp.MustCompile(`^/forums(/(?P<cats>[^\d/]+(/[^\d]+)*))?/t/(?P<threadid>\d+)/p/(?P<postid>\d+)/edit$`)
|
var RegexForumPostEdit = regexp.MustCompile(`^/forums(/(?P<subforums>[^\d/]+(/[^\d]+)*))?/t/(?P<threadid>\d+)/p/(?P<postid>\d+)/edit$`)
|
||||||
|
|
||||||
func BuildForumPostEdit(projectSlug string, subforums []string, threadId int, postId int) string {
|
func BuildForumPostEdit(projectSlug string, subforums []string, threadId int, postId int) string {
|
||||||
defer CatchPanic()
|
defer CatchPanic()
|
||||||
|
@ -382,7 +382,7 @@ func BuildForumPostEdit(projectSlug string, subforums []string, threadId int, po
|
||||||
return ProjectUrl(builder.String(), nil, projectSlug)
|
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||||
}
|
}
|
||||||
|
|
||||||
var RegexForumPostReply = regexp.MustCompile(`^/forums(/(?P<cats>[^\d/]+(/[^\d]+)*))?/t/(?P<threadid>\d+)/p/(?P<postid>\d+)/reply$`)
|
var RegexForumPostReply = regexp.MustCompile(`^/forums(/(?P<subforums>[^\d/]+(/[^\d]+)*))?/t/(?P<threadid>\d+)/p/(?P<postid>\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?
|
// 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 {
|
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)
|
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<articleid>\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<articleid>\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<articleid>\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<articleid>\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<articleid>\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<articleid>\d+)(-([^/])+)?/(?P<revisionid>\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<articleid>\d+)(-([^/])+)?/diff/(?P<revisionidold>\d+)/(?P<revisionidnew>\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<articleid>\d+)/talk/(?P<postid>\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<articleid>\d+)/talk/(?P<postid>\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<articleid>\d+)/talk/(?P<postid>\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<articleid>\d+)/talk/(?P<postid>\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<articleid>\d+)/talk/(?P<postid>\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
|
* Library
|
||||||
*/
|
*/
|
||||||
|
@ -671,74 +514,6 @@ func BuildLibraryResource(projectSlug string, resourceId int) string {
|
||||||
return ProjectUrl(builder.String(), nil, projectSlug)
|
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||||
}
|
}
|
||||||
|
|
||||||
var RegexLibraryDiscussion = regexp.MustCompile(`^/library/resource/(?P<resourceid>\d+)/d/(?P<threadid>\d+)(/(?P<page>\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<resourceid>\d+)/d/(?P<threadid>\d+)/p/(?P<postid>\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<resourceid>\d+)/d/(?P<threadid>\d+)/p/(?P<postid>\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<resourceid>\d+)/d/(?P<threadid>\d+)/p/(?P<postid>\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<resourceid>\d+)/d/(?P<threadid>\d+)/p/(?P<postid>\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<resourceid>\d+)/d/(?P<threadid>\d+)/p/(?P<postid>\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
|
* Assets
|
||||||
*/
|
*/
|
||||||
|
@ -803,18 +578,18 @@ func BuildUserFile(filepath string) string {
|
||||||
* Other
|
* Other
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var RegexForumCategoryMarkRead = regexp.MustCompile(`^/markread/(?P<catid>\d+)$`)
|
var RegexForumMarkRead = regexp.MustCompile(`^/markread/(?P<sfid>\d+)$`)
|
||||||
|
|
||||||
// NOTE(asaf): categoryId == 0 means ALL CATEGORIES
|
// NOTE(asaf): subforumId == 0 means ALL SUBFORUMS
|
||||||
func BuildForumCategoryMarkRead(categoryId int) string {
|
func BuildForumMarkRead(subforumId int) string {
|
||||||
defer CatchPanic()
|
defer CatchPanic()
|
||||||
if categoryId < 0 {
|
if subforumId < 0 {
|
||||||
panic(oops.New(nil, "Invalid category ID (%d), must be >= 0", categoryId))
|
panic(oops.New(nil, "Invalid subforum ID (%d), must be >= 0", subforumId))
|
||||||
}
|
}
|
||||||
|
|
||||||
var builder strings.Builder
|
var builder strings.Builder
|
||||||
builder.WriteString("/markread/")
|
builder.WriteString("/markread/")
|
||||||
builder.WriteString(strconv.Itoa(categoryId))
|
builder.WriteString(strconv.Itoa(subforumId))
|
||||||
|
|
||||||
return Url(builder.String(), nil)
|
return Url(builder.String(), nil)
|
||||||
}
|
}
|
||||||
|
@ -825,7 +600,7 @@ var RegexCatchAll = regexp.MustCompile("")
|
||||||
* Helper functions
|
* Helper functions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
func buildForumCategoryPath(subforums []string) *strings.Builder {
|
func buildSubforumPath(subforums []string) *strings.Builder {
|
||||||
for _, subforum := range subforums {
|
for _, subforum := range subforums {
|
||||||
if strings.Contains(subforum, "/") {
|
if strings.Contains(subforum, "/") {
|
||||||
panic(oops.New(nil, "Tried building forum url with / in subforum name"))
|
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))
|
panic(oops.New(nil, "Invalid forum thread ID (%d), must be >= 1", threadId))
|
||||||
}
|
}
|
||||||
|
|
||||||
builder := buildForumCategoryPath(subforums)
|
builder := buildSubforumPath(subforums)
|
||||||
|
|
||||||
builder.WriteString("/t/")
|
builder.WriteString("/t/")
|
||||||
builder.WriteString(strconv.Itoa(threadId))
|
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))
|
panic(oops.New(nil, "Invalid forum post ID (%d), must be >= 1", postId))
|
||||||
}
|
}
|
||||||
|
|
||||||
builder := buildForumCategoryPath(subforums)
|
builder := buildSubforumPath(subforums)
|
||||||
|
|
||||||
builder.WriteString("/t/")
|
builder.WriteString("/t/")
|
||||||
builder.WriteString(strconv.Itoa(threadId))
|
builder.WriteString(strconv.Itoa(threadId))
|
||||||
|
@ -934,34 +709,6 @@ func buildBlogPostPath(threadId int, postId int) *strings.Builder {
|
||||||
return &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 {
|
func buildLibraryResourcePath(resourceId int) *strings.Builder {
|
||||||
if resourceId < 1 {
|
if resourceId < 1 {
|
||||||
panic(oops.New(nil, "Invalid library resource ID (%d), must be >= 1", resourceId))
|
panic(oops.New(nil, "Invalid library resource ID (%d), must be >= 1", resourceId))
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.handmade.network/hmn/hmn/src/migration/types"
|
"git.handmade.network/hmn/hmn/src/migration/types"
|
||||||
"git.handmade.network/hmn/hmn/src/oops"
|
|
||||||
"github.com/jackc/pgx/v4"
|
"github.com/jackc/pgx/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
|
@ -1,9 +1,26 @@
|
||||||
package migrations
|
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)
|
var All map[types.MigrationVersion]types.Migration = make(map[types.MigrationVersion]types.Migration)
|
||||||
|
|
||||||
func registerMigration(m types.Migration) {
|
func registerMigration(m types.Migration) {
|
||||||
All[m.Version()] = m
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -3,8 +3,7 @@ package models
|
||||||
type LibraryResource struct {
|
type LibraryResource struct {
|
||||||
ID int `db:"id"`
|
ID int `db:"id"`
|
||||||
|
|
||||||
CategoryID int `db:"category_id"`
|
ProjectID *int `db:"project_id"`
|
||||||
ProjectID *int `db:"project_id"`
|
|
||||||
|
|
||||||
Name string `db:"name"`
|
Name string `db:"name"`
|
||||||
Description string `db:"description"`
|
Description string `db:"description"`
|
||||||
|
|
|
@ -9,14 +9,13 @@ type Post struct {
|
||||||
ID int `db:"id"`
|
ID int `db:"id"`
|
||||||
|
|
||||||
// TODO: Document each of these
|
// TODO: Document each of these
|
||||||
AuthorID *int `db:"author_id"`
|
AuthorID *int `db:"author_id"`
|
||||||
CategoryID int `db:"category_id"`
|
ParentID *int `db:"parent_id"`
|
||||||
ParentID *int `db:"parent_id"`
|
ThreadID int `db:"thread_id"`
|
||||||
ThreadID int `db:"thread_id"`
|
CurrentID int `db:"current_id"`
|
||||||
CurrentID int `db:"current_id"`
|
ProjectID int `db:"project_id"`
|
||||||
ProjectID int `db:"project_id"`
|
|
||||||
|
|
||||||
CategoryKind CategoryKind `db:"category_kind"`
|
ThreadType ThreadType `db:"thread_type"`
|
||||||
|
|
||||||
PostDate time.Time `db:"postdate"`
|
PostDate time.Time `db:"postdate"`
|
||||||
Deleted bool `db:"deleted"`
|
Deleted bool `db:"deleted"`
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -1,9 +1,24 @@
|
||||||
package models
|
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 {
|
type Thread struct {
|
||||||
ID int `db:"id"`
|
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"`
|
Title string `db:"title"`
|
||||||
Sticky bool `db:"sticky"`
|
Sticky bool `db:"sticky"`
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
// +build js
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
|
@ -157,13 +157,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.post {
|
.post {
|
||||||
.wiki &,
|
|
||||||
{
|
|
||||||
padding: 0;
|
|
||||||
margin: auto;
|
|
||||||
max-width: 70em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contents {
|
.contents {
|
||||||
h1, h2 {
|
h1, h2 {
|
||||||
margin: 20px 0px;
|
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 {
|
.featured-post .meta .avatar-icon {
|
||||||
left:-60px;
|
left:-60px;
|
||||||
bottom:-5px;
|
bottom:-5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blog .body blockquote,
|
.blog .body blockquote {
|
||||||
.wiki .body blockquote {
|
|
||||||
padding-top:1px;
|
padding-top:1px;
|
||||||
padding-bottom:1px;
|
padding-bottom:1px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,6 @@
|
||||||
&.no-blogs .blogs {
|
&.no-blogs .blogs {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
&.no-wiki .wiki {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
&.no-library .library {
|
&.no-library .library {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,9 +112,6 @@ $vars: (
|
||||||
irc-users-popout-border-color-left: #444,
|
irc-users-popout-border-color-left: #444,
|
||||||
irc-users-popout-border-color-right: #333,
|
irc-users-popout-border-color-right: #333,
|
||||||
|
|
||||||
wiki-border-color: #444,
|
|
||||||
wiki-toc-number-color: #bbb,
|
|
||||||
|
|
||||||
code-line-number-color: #444,
|
code-line-number-color: #444,
|
||||||
|
|
||||||
library-star-btn-background: #252525,
|
library-star-btn-background: #252525,
|
||||||
|
|
|
@ -112,9 +112,6 @@ $vars: (
|
||||||
irc-users-popout-border-color-left: #bbb,
|
irc-users-popout-border-color-left: #bbb,
|
||||||
irc-users-popout-border-color-right: #ccc,
|
irc-users-popout-border-color-right: #ccc,
|
||||||
|
|
||||||
wiki-border-color: #aaa,
|
|
||||||
wiki-toc-number-color: #333,
|
|
||||||
|
|
||||||
code-line-number-color: #777,
|
code-line-number-color: #777,
|
||||||
|
|
||||||
library-star-btn-background: #fff,
|
library-star-btn-background: #fff,
|
||||||
|
|
|
@ -99,7 +99,6 @@ func ProjectToTemplate(p *models.Project, theme string) Project {
|
||||||
|
|
||||||
HasBlog: true, // TODO: Check flag sets or whatever
|
HasBlog: true, // TODO: Check flag sets or whatever
|
||||||
HasForum: true,
|
HasForum: true,
|
||||||
HasWiki: true,
|
|
||||||
HasLibrary: true,
|
HasLibrary: true,
|
||||||
|
|
||||||
DateApproved: p.DateApproved,
|
DateApproved: p.DateApproved,
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<div class="content-block">
|
<div class="content-block">
|
||||||
{{ range .Subcategories }}
|
{{ range .Subforums }}
|
||||||
<div class="pv3">
|
<div class="pv3">
|
||||||
<h2 class="ma0 ph3 pb2">
|
<h2 class="ma0 ph3 pb2">
|
||||||
<a href="{{ .Url }}">
|
<a href="{{ .Url }}">
|
||||||
|
@ -21,18 +21,18 @@
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<div class="optionbar">
|
<div class="optionbar">
|
||||||
{{ template "forum_category_options" . }}
|
{{ template "subforum_options" . }}
|
||||||
</div>
|
</div>
|
||||||
{{ range .Threads }}
|
{{ range .Threads }}
|
||||||
{{ template "thread_list_item.html" . }}
|
{{ template "thread_list_item.html" . }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<div class="optionbar bottom">
|
<div class="optionbar bottom">
|
||||||
{{ template "forum_category_options" . }}
|
{{ template "subforum_options" . }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{ define "forum_category_options" }}
|
{{ define "subforum_options" }}
|
||||||
<div class="options">
|
<div class="options">
|
||||||
{{ if .User }}
|
{{ if .User }}
|
||||||
<a class="button new-thread" href="{{ .NewThreadUrl }}"><span class="big pr1">+</span> New Thread</a>
|
<a class="button new-thread" href="{{ .NewThreadUrl }}"><span class="big pr1">+</span> New Thread</a>
|
|
@ -4,7 +4,7 @@
|
||||||
<div class="content-block">
|
<div class="content-block">
|
||||||
<div class="optionbar">
|
<div class="optionbar">
|
||||||
<div class="options">
|
<div class="options">
|
||||||
<a class="button" href="{{ .CategoryUrl }}">← Back to index</a>
|
<a class="button" href="{{ .SubforumUrl }}">← Back to index</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="options">
|
<div class="options">
|
||||||
{{ template "pagination.html" .Pagination }}
|
{{ template "pagination.html" .Pagination }}
|
||||||
|
@ -118,7 +118,7 @@
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<div class="optionbar bottom">
|
<div class="optionbar bottom">
|
||||||
<div class="options order-1">
|
<div class="options order-1">
|
||||||
<a class="button" href="{{ .CategoryUrl }}">← Back to index</a>
|
<a class="button" href="{{ .SubforumUrl }}">← Back to index</a>
|
||||||
{{ if .Thread.Locked }}
|
{{ if .Thread.Locked }}
|
||||||
<span>Thread is locked.</span>
|
<span>Thread is locked.</span>
|
||||||
{{ else if .User }}
|
{{ else if .User }}
|
||||||
|
|
|
@ -51,9 +51,6 @@
|
||||||
{{ if .Project.HasForum }}
|
{{ if .Project.HasForum }}
|
||||||
<a href="{{ .Header.ForumsUrl }}" class="forums">Forums</a>
|
<a href="{{ .Header.ForumsUrl }}" class="forums">Forums</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ if .Project.HasWiki }}
|
|
||||||
<a href="{{ .Header.WikiUrl }}" class="wiki">Wiki</a>
|
|
||||||
{{ end }}
|
|
||||||
{{ if .Project.HasLibrary }}
|
{{ if .Project.HasLibrary }}
|
||||||
<a href="{{ .Header.LibraryUrl }}" class="library">Library</a>
|
<a href="{{ .Header.LibraryUrl }}" class="library">Library</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -172,166 +172,6 @@
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ 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" %}
|
|
||||||
<div class="content-block pb3">
|
|
||||||
<div class="tc tl-l w-100 pb2">
|
|
||||||
<h2 class="di-l mr2-l">Community Showcase</h2>
|
|
||||||
<ul class="list dib-l">
|
|
||||||
<li class="dib-ns ma0 ph2">
|
|
||||||
<a href="{{ .ShowcaseUrl }}">View all</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="showcase relative overflow-hidden">
|
|
||||||
<div id="showcase-items" class="flex relative pl3 pl0-ns"></div>
|
|
||||||
<div class="arrow-container left">
|
|
||||||
<a href="javascript:void(0)" class="arrow svgicon svgicon-nofix" onclick="scrollShowcase('left')">{% svg 'chevron-left' %}</a>
|
|
||||||
</div>
|
|
||||||
<div class="arrow-container right">
|
|
||||||
<a href="javascript:void(0)" class="arrow svgicon svgicon-nofix" onclick="scrollShowcase('right')">{% svg 'chevron-right' %}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="c--dimmer i pv2 ph3 ph0-ns">
|
|
||||||
This is a selection of recent work done by community members. Want to participate? <a href="{{ .DiscordUrl }}" target="_blank">Join us on Discord.</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
const timelineData = JSON.parse("{{ showcase_timeline_json|escapejs }}");
|
|
||||||
|
|
||||||
const showcaseEl = document.querySelector('#showcase-items');
|
|
||||||
for (const item of timelineData.items) {
|
|
||||||
const [itemEl, addThumbnail] = makeShowcaseItem(item);
|
|
||||||
addThumbnail();
|
|
||||||
itemEl.container.classList.add('mr3');
|
|
||||||
showcaseEl.appendChild(itemEl.root);
|
|
||||||
}
|
|
||||||
|
|
||||||
function rem2px(rem) {
|
|
||||||
return rem * parseFloat(getComputedStyle(document.documentElement).fontSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
function scrollShowcase(direction = null) {
|
|
||||||
const ITEM_WIDTH = showcaseEl.querySelector('.showcase-item').getBoundingClientRect().width;
|
|
||||||
const ITEM_SPACING = rem2px(1);
|
|
||||||
|
|
||||||
const showcaseWidth = showcaseEl.getBoundingClientRect().width;
|
|
||||||
const numVisible = showcaseWidth / (ITEM_WIDTH + ITEM_SPACING);
|
|
||||||
const scrollMagnitude = Math.floor(numVisible) - 1;
|
|
||||||
const scrollDirection = (direction === 'right' ? 1 : (direction === 'left' ? -1 : 0));
|
|
||||||
const scrollAmount = scrollMagnitude * scrollDirection;
|
|
||||||
|
|
||||||
const minIndex = 0;
|
|
||||||
const maxIndex = timelineData.items.length - Math.floor(numVisible);
|
|
||||||
|
|
||||||
const currentScrollIndex = parseInt(showcaseEl.getAttribute('data-scroll-index'), 10) || 0;
|
|
||||||
const newScrollIndex = Math.max(minIndex, Math.min(maxIndex, currentScrollIndex + scrollAmount));
|
|
||||||
|
|
||||||
showcaseEl.style.transform = `translateX(${-newScrollIndex * (ITEM_WIDTH + ITEM_SPACING)}px)`;
|
|
||||||
showcaseEl.setAttribute('data-scroll-index', newScrollIndex);
|
|
||||||
|
|
||||||
const leftArrowEl = document.querySelector('.arrow-container.left');
|
|
||||||
const rightArrowEl = document.querySelector('.arrow-container.right');
|
|
||||||
|
|
||||||
leftArrowEl.classList.toggle('hide', newScrollIndex === minIndex);
|
|
||||||
rightArrowEl.classList.toggle('hide', newScrollIndex === maxIndex);
|
|
||||||
}
|
|
||||||
scrollShowcase(); // force a scroll as an easy way to initialize styles
|
|
||||||
|
|
||||||
window.addEventListener('resize', () => scrollShowcase());
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="content-block">
|
|
||||||
<div class="optionbar pb2">
|
|
||||||
<div class="tc tl-l w-100">
|
|
||||||
<h2 class="di-l mr2-l">Around the Network</h2>
|
|
||||||
<ul class="list dib-l">
|
|
||||||
<li class="dib-ns ma0 ph2">
|
|
||||||
<a href="{% url 'feed' %}">View all posts on HMN</a>
|
|
||||||
</li>
|
|
||||||
<li class="dib-ns ma0 ph2">
|
|
||||||
<a href="{% url 'podcast' %}">Podcast</a>
|
|
||||||
</li>
|
|
||||||
<!-- <li class="dib-ns ma0 ph2">
|
|
||||||
<a href="{% url 'streams' %}">See who's live</a>
|
|
||||||
</li> -->
|
|
||||||
<li class="dib-ns ma0 ph2">
|
|
||||||
<a href="/blogs/p/1138-%5Btutorial%5D_handmade_network_irc" target="_blank">Chat in IRC</a>
|
|
||||||
</li>
|
|
||||||
<li class="dib-ns ma0 ph2">
|
|
||||||
<a href="https://discord.gg/hxWxDee" target="_blank">Chat on Discord</a>
|
|
||||||
</li>
|
|
||||||
<li class="dib-ns ma0 ph2">
|
|
||||||
<a href="https://handmadedev.show/" target="_blank">See the Show</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% spaceless %}
|
|
||||||
<div class="content-block news cf">
|
|
||||||
{% for col in recent_post_columns %}
|
|
||||||
<div class="fl w-100 w-50-l">
|
|
||||||
<div class="mw7 mw-none-l center-layout">
|
|
||||||
{% if forloop.counter == 1 %}
|
|
||||||
<div class="pt3">
|
|
||||||
{% include "blog_index_thread_list_entry.html" with post=featured_post align_top=True %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% for entry in col %}
|
|
||||||
{% with proj=entry.project posts=entry.posts %}
|
|
||||||
<div class="pt3" id="p{{proj.slug}}">
|
|
||||||
<a
|
|
||||||
href="{% url 'cover_page' subdomain=proj.slug %}"
|
|
||||||
{% if user|get_theme == 'dark' %}
|
|
||||||
style="color:#{% rgb_accent proj.color_1 0.55 %};"
|
|
||||||
{% else %}
|
|
||||||
style="color:#{% rgb_accent proj.color_1 0.25 %};"
|
|
||||||
{% endif %}
|
|
||||||
>
|
|
||||||
<h2 class="ph3">{{ proj.name }}</h2>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{% 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 %}
|
|
||||||
<div class="ph3 thread unread more">
|
|
||||||
<a class="title"
|
|
||||||
href="{% url 'project_forum' subdomain=proj.slug %}"
|
|
||||||
>{{ more }} more recently →</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endwith %}
|
|
||||||
</div>
|
|
||||||
{% endwith %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endspaceless %}
|
|
||||||
{% endblock %}
|
|
||||||
*/}}
|
|
||||||
|
|
||||||
{{ define "landing_page_featured_post" }}
|
{{ define "landing_page_featured_post" }}
|
||||||
{{/* Call this template with a LandingPageFeaturedPost. */}}
|
{{/* Call this template with a LandingPageFeaturedPost. */}}
|
||||||
<div class="flex items-start ph3 pv2 {{ if .Unread }}unread{{ else }}read{{ end }}">
|
<div class="flex items-start ph3 pv2 {{ if .Unread }}unread{{ else }}read{{ end }}">
|
||||||
|
|
|
@ -25,12 +25,6 @@
|
||||||
{{ if gt .NumBlogs 0 }}
|
{{ if gt .NumBlogs 0 }}
|
||||||
<div class="dib filter blogs mr2"><input data-type="blogs" class="v-mid mr1" type="checkbox" id="timeline-checkbox-blogs" checked /><label class="v-mid" for="timeline-checkbox-blogs">Blogs (<span class="count">{{ .NumBlogs }}</span>)</label></div>
|
<div class="dib filter blogs mr2"><input data-type="blogs" class="v-mid mr1" type="checkbox" id="timeline-checkbox-blogs" checked /><label class="v-mid" for="timeline-checkbox-blogs">Blogs (<span class="count">{{ .NumBlogs }}</span>)</label></div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ if gt .NumWiki 0 }}
|
|
||||||
<div class="dib filter wiki mr2"><input data-type="wiki" class="v-mid mr1" type="checkbox" id="timeline-checkbox-wiki" checked /><label class="v-mid" for="timeline-checkbox-wiki">Wiki (<span class="count">{{ .NumWiki }}</span>)</label></div>
|
|
||||||
{{ end }}
|
|
||||||
{{ if gt .NumLibrary 0 }}
|
|
||||||
<div class="dib filter library mr2"><input data-type="library" class="v-mid mr1" type="checkbox" id="timeline-checkbox-library" checked /><label class="v-mid" for="timeline-checkbox-library">Library (<span class="count">{{ .NumLibrary }}</span>)</label></div>
|
|
||||||
{{ end }}
|
|
||||||
{{ if gt .NumSnippets 0 }}
|
{{ if gt .NumSnippets 0 }}
|
||||||
<div class="dib filter snippets mr2"><input data-type="snippets" class="v-mid mr1" type="checkbox" id="timeline-checkbox-snippets" checked /><label class="v-mid" for="timeline-checkbox-snippets">Snippets (<span class="count">{{ .NumSnippets }}</span>)</label></div>
|
<div class="dib filter snippets mr2"><input data-type="snippets" class="v-mid mr1" type="checkbox" id="timeline-checkbox-snippets" checked /><label class="v-mid" for="timeline-checkbox-snippets">Snippets (<span class="count">{{ .NumSnippets }}</span>)</label></div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
@ -61,7 +55,7 @@
|
||||||
|
|
||||||
<div class="pair flex flex-wrap">
|
<div class="pair flex flex-wrap">
|
||||||
<div class="key flex-auto mr1">Posts</div>
|
<div class="key flex-auto mr1">Posts</div>
|
||||||
<div class="value">{{ add .NumForums .NumBlogs .NumWiki .NumLibrary }}</div>
|
<div class="value">{{ add .NumForums .NumBlogs }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ if .ProfileUser.Email }}
|
{{ if .ProfileUser.Email }}
|
||||||
|
|
|
@ -218,11 +218,7 @@ var HMNTemplateFuncs = template.FuncMap{
|
||||||
if item.Type == TimelineTypeForumThread ||
|
if item.Type == TimelineTypeForumThread ||
|
||||||
item.Type == TimelineTypeForumReply ||
|
item.Type == TimelineTypeForumReply ||
|
||||||
item.Type == TimelineTypeBlogPost ||
|
item.Type == TimelineTypeBlogPost ||
|
||||||
item.Type == TimelineTypeBlogComment ||
|
item.Type == TimelineTypeBlogComment {
|
||||||
item.Type == TimelineTypeWikiCreate ||
|
|
||||||
item.Type == TimelineTypeWikiEdit ||
|
|
||||||
item.Type == TimelineTypeWikiTalk ||
|
|
||||||
item.Type == TimelineTypeLibraryComment {
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,6 @@ type Header struct {
|
||||||
ProjectIndexUrl string
|
ProjectIndexUrl string
|
||||||
BlogUrl string
|
BlogUrl string
|
||||||
ForumsUrl string
|
ForumsUrl string
|
||||||
WikiUrl string
|
|
||||||
LibraryUrl string
|
LibraryUrl string
|
||||||
ManifestoUrl string
|
ManifestoUrl string
|
||||||
EpisodeGuideUrl string
|
EpisodeGuideUrl string
|
||||||
|
@ -109,7 +108,6 @@ type Project struct {
|
||||||
|
|
||||||
HasBlog bool
|
HasBlog bool
|
||||||
HasForum bool
|
HasForum bool
|
||||||
HasWiki bool
|
|
||||||
HasLibrary bool
|
HasLibrary bool
|
||||||
|
|
||||||
UUID string
|
UUID string
|
||||||
|
@ -202,10 +200,6 @@ const (
|
||||||
PostTypeBlogComment
|
PostTypeBlogComment
|
||||||
PostTypeForumThread
|
PostTypeForumThread
|
||||||
PostTypeForumReply
|
PostTypeForumReply
|
||||||
PostTypeWikiCreate
|
|
||||||
PostTypeWikiTalk
|
|
||||||
PostTypeWikiEdit
|
|
||||||
PostTypeLibraryComment
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Data from post_list_item.html
|
// Data from post_list_item.html
|
||||||
|
@ -253,12 +247,6 @@ const (
|
||||||
TimelineTypeBlogPost
|
TimelineTypeBlogPost
|
||||||
TimelineTypeBlogComment
|
TimelineTypeBlogComment
|
||||||
|
|
||||||
TimelineTypeWikiCreate
|
|
||||||
TimelineTypeWikiEdit
|
|
||||||
TimelineTypeWikiTalk
|
|
||||||
|
|
||||||
TimelineTypeLibraryComment
|
|
||||||
|
|
||||||
TimelineTypeSnippetImage
|
TimelineTypeSnippetImage
|
||||||
TimelineTypeSnippetVideo
|
TimelineTypeSnippetVideo
|
||||||
TimelineTypeSnippetAudio
|
TimelineTypeSnippetAudio
|
||||||
|
|
|
@ -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",
|
|
||||||
}
|
|
|
@ -37,11 +37,11 @@ func Feed(c *RequestContext) ResponseData {
|
||||||
FROM
|
FROM
|
||||||
handmade_post AS post
|
handmade_post AS post
|
||||||
WHERE
|
WHERE
|
||||||
post.category_kind = ANY ($1)
|
post.thread_type = ANY ($1)
|
||||||
AND deleted = FALSE
|
AND deleted = FALSE
|
||||||
AND post.thread_id IS NOT NULL
|
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()
|
c.Perf.EndBlock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -80,9 +80,9 @@ func Feed(c *RequestContext) ResponseData {
|
||||||
currentUserId = &c.CurrentUser.ID
|
currentUserId = &c.CurrentUser.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Perf.StartBlock("SQL", "Fetch category tree")
|
c.Perf.StartBlock("SQL", "Fetch subforum tree")
|
||||||
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
|
subforumTree := models.GetFullSubforumTree(c.Context(), c.Conn)
|
||||||
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
lineageBuilder := models.MakeSubforumLineageBuilder(subforumTree)
|
||||||
c.Perf.EndBlock()
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
posts, err := fetchAllPosts(c, lineageBuilder, currentUserId, howManyPostsToSkip, postsPerPage)
|
posts, err := fetchAllPosts(c, lineageBuilder, currentUserId, howManyPostsToSkip, postsPerPage)
|
||||||
|
@ -98,7 +98,7 @@ func Feed(c *RequestContext) ResponseData {
|
||||||
BaseData: baseData,
|
BaseData: baseData,
|
||||||
|
|
||||||
AtomFeedUrl: hmnurl.BuildAtomFeed(),
|
AtomFeedUrl: hmnurl.BuildAtomFeed(),
|
||||||
MarkAllReadUrl: hmnurl.BuildForumCategoryMarkRead(0),
|
MarkAllReadUrl: hmnurl.BuildForumMarkRead(0),
|
||||||
Posts: posts,
|
Posts: posts,
|
||||||
Pagination: pagination,
|
Pagination: pagination,
|
||||||
}, c.Perf)
|
}, c.Perf)
|
||||||
|
@ -158,9 +158,9 @@ func AtomFeed(c *RequestContext) ResponseData {
|
||||||
feedData.AtomFeedUrl = hmnurl.BuildAtomFeed()
|
feedData.AtomFeedUrl = hmnurl.BuildAtomFeed()
|
||||||
feedData.FeedUrl = hmnurl.BuildFeed()
|
feedData.FeedUrl = hmnurl.BuildFeed()
|
||||||
|
|
||||||
c.Perf.StartBlock("SQL", "Fetch category tree")
|
c.Perf.StartBlock("SQL", "Fetch subforum tree")
|
||||||
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
|
subforumTree := models.GetFullSubforumTree(c.Context(), c.Conn)
|
||||||
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
lineageBuilder := models.MakeSubforumLineageBuilder(subforumTree)
|
||||||
c.Perf.EndBlock()
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
posts, err := fetchAllPosts(c, lineageBuilder, nil, 0, itemsPerFeed)
|
posts, err := fetchAllPosts(c, lineageBuilder, nil, 0, itemsPerFeed)
|
||||||
|
@ -303,18 +303,16 @@ func AtomFeed(c *RequestContext) ResponseData {
|
||||||
return res
|
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")
|
c.Perf.StartBlock("SQL", "Fetch posts")
|
||||||
type feedPostQuery struct {
|
type feedPostQuery struct {
|
||||||
Post models.Post `db:"post"`
|
Post models.Post `db:"post"`
|
||||||
PostVersion models.PostVersion `db:"version"`
|
PostVersion models.PostVersion `db:"version"`
|
||||||
Thread models.Thread `db:"thread"`
|
Thread models.Thread `db:"thread"`
|
||||||
Cat models.Category `db:"cat"`
|
Proj models.Project `db:"proj"`
|
||||||
Proj models.Project `db:"proj"`
|
User models.User `db:"auth_user"`
|
||||||
LibraryResource *models.LibraryResource `db:"lib_resource"`
|
ThreadLastReadTime *time.Time `db:"tlri.lastread"`
|
||||||
User models.User `db:"auth_user"`
|
SubforumLastReadTime *time.Time `db:"slri.lastread"`
|
||||||
ThreadLastReadTime *time.Time `db:"tlri.lastread"`
|
|
||||||
CatLastReadTime *time.Time `db:"clri.lastread"`
|
|
||||||
}
|
}
|
||||||
posts, err := db.Query(c.Context(), c.Conn, feedPostQuery{},
|
posts, err := db.Query(c.Context(), c.Conn, feedPostQuery{},
|
||||||
`
|
`
|
||||||
|
@ -323,27 +321,25 @@ func fetchAllPosts(c *RequestContext, lineageBuilder *models.CategoryLineageBuil
|
||||||
handmade_post AS post
|
handmade_post AS post
|
||||||
JOIN handmade_postversion AS version ON version.id = post.current_id
|
JOIN handmade_postversion AS version ON version.id = post.current_id
|
||||||
JOIN handmade_thread AS thread ON thread.id = post.thread_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
|
JOIN handmade_project AS proj ON proj.id = post.project_id
|
||||||
LEFT JOIN handmade_threadlastreadinfo AS tlri ON (
|
LEFT JOIN handmade_threadlastreadinfo AS tlri ON (
|
||||||
tlri.thread_id = post.thread_id
|
tlri.thread_id = post.thread_id
|
||||||
AND tlri.user_id = $1
|
AND tlri.user_id = $1
|
||||||
)
|
)
|
||||||
LEFT JOIN handmade_categorylastreadinfo AS clri ON (
|
LEFT JOIN handmade_subforumlastreadinfo AS slri ON (
|
||||||
clri.category_id = post.category_id
|
slri.subforum_id = thread.subforum_id
|
||||||
AND clri.user_id = $1
|
AND slri.user_id = $1
|
||||||
)
|
)
|
||||||
LEFT JOIN auth_user ON post.author_id = auth_user.id
|
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
|
WHERE
|
||||||
post.category_kind = ANY ($2)
|
thread.type = ANY ($2)
|
||||||
AND post.deleted = FALSE
|
AND post.deleted = FALSE
|
||||||
AND post.thread_id IS NOT NULL
|
AND post.thread_id IS NOT NULL
|
||||||
ORDER BY postdate DESC
|
ORDER BY postdate DESC
|
||||||
LIMIT $3 OFFSET $4
|
LIMIT $3 OFFSET $4
|
||||||
`,
|
`,
|
||||||
currentUserID,
|
currentUserID,
|
||||||
[]models.CategoryKind{models.CatKindForum, models.CatKindBlog, models.CatKindWiki, models.CatKindLibraryResource},
|
[]models.ThreadType{models.ThreadTypeForumPost, models.ThreadTypeProjectArticle},
|
||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
)
|
)
|
||||||
|
@ -360,7 +356,7 @@ func fetchAllPosts(c *RequestContext, lineageBuilder *models.CategoryLineageBuil
|
||||||
hasRead := false
|
hasRead := false
|
||||||
if postResult.ThreadLastReadTime != nil && postResult.ThreadLastReadTime.After(postResult.Post.PostDate) {
|
if postResult.ThreadLastReadTime != nil && postResult.ThreadLastReadTime.After(postResult.Post.PostDate) {
|
||||||
hasRead = true
|
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
|
hasRead = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -370,7 +366,6 @@ func fetchAllPosts(c *RequestContext, lineageBuilder *models.CategoryLineageBuil
|
||||||
&postResult.Thread,
|
&postResult.Thread,
|
||||||
&postResult.Post,
|
&postResult.Post,
|
||||||
&postResult.User,
|
&postResult.User,
|
||||||
postResult.LibraryResource,
|
|
||||||
!hasRead,
|
!hasRead,
|
||||||
true,
|
true,
|
||||||
c.Theme,
|
c.Theme,
|
||||||
|
|
|
@ -21,17 +21,17 @@ import (
|
||||||
"github.com/jackc/pgx/v4"
|
"github.com/jackc/pgx/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
type forumCategoryData struct {
|
type forumData struct {
|
||||||
templates.BaseData
|
templates.BaseData
|
||||||
|
|
||||||
NewThreadUrl string
|
NewThreadUrl string
|
||||||
MarkReadUrl string
|
MarkReadUrl string
|
||||||
Threads []templates.ThreadListItem
|
Threads []templates.ThreadListItem
|
||||||
Pagination templates.Pagination
|
Pagination templates.Pagination
|
||||||
Subcategories []forumSubcategoryData
|
Subforums []forumSubforumData
|
||||||
}
|
}
|
||||||
|
|
||||||
type forumSubcategoryData struct {
|
type forumSubforumData struct {
|
||||||
Name string
|
Name string
|
||||||
Url string
|
Url string
|
||||||
Threads []templates.ThreadListItem
|
Threads []templates.ThreadListItem
|
||||||
|
@ -50,7 +50,7 @@ type editorData struct {
|
||||||
PostReplyingTo *templates.Post
|
PostReplyingTo *templates.Post
|
||||||
}
|
}
|
||||||
|
|
||||||
func ForumCategory(c *RequestContext) ResponseData {
|
func Forum(c *RequestContext) ResponseData {
|
||||||
const threadsPerPage = 25
|
const threadsPerPage = 25
|
||||||
|
|
||||||
cd, ok := getCommonForumData(c)
|
cd, ok := getCommonForumData(c)
|
||||||
|
@ -58,7 +58,7 @@ func ForumCategory(c *RequestContext) ResponseData {
|
||||||
return FourOhFour(c)
|
return FourOhFour(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
currentSubforumSlugs := cd.LineageBuilder.GetSubforumLineageSlugs(cd.CatID)
|
currentSubforumSlugs := cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID)
|
||||||
|
|
||||||
c.Perf.StartBlock("SQL", "Fetch count of page threads")
|
c.Perf.StartBlock("SQL", "Fetch count of page threads")
|
||||||
numThreads, err := db.QueryInt(c.Context(), c.Conn,
|
numThreads, err := db.QueryInt(c.Context(), c.Conn,
|
||||||
|
@ -66,10 +66,10 @@ func ForumCategory(c *RequestContext) ResponseData {
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM handmade_thread AS thread
|
FROM handmade_thread AS thread
|
||||||
WHERE
|
WHERE
|
||||||
thread.category_id = $1
|
thread.subforum_id = $1
|
||||||
AND NOT thread.deleted
|
AND NOT thread.deleted
|
||||||
`,
|
`,
|
||||||
cd.CatID,
|
cd.SubforumID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(oops.New(err, "failed to get count of threads"))
|
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 {
|
if pageParsed, err := strconv.Atoi(pageString); err == nil {
|
||||||
page = pageParsed
|
page = pageParsed
|
||||||
} else {
|
} 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 {
|
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
|
howManyThreadsToSkip := (page - 1) * threadsPerPage
|
||||||
|
@ -106,7 +106,7 @@ func ForumCategory(c *RequestContext) ResponseData {
|
||||||
FirstUser *models.User `db:"firstuser"`
|
FirstUser *models.User `db:"firstuser"`
|
||||||
LastUser *models.User `db:"lastuser"`
|
LastUser *models.User `db:"lastuser"`
|
||||||
ThreadLastReadTime *time.Time `db:"tlri.lastread"`
|
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{},
|
itMainThreads, err := db.Query(c.Context(), c.Conn, threadQueryResult{},
|
||||||
`
|
`
|
||||||
|
@ -121,17 +121,17 @@ func ForumCategory(c *RequestContext) ResponseData {
|
||||||
tlri.thread_id = thread.id
|
tlri.thread_id = thread.id
|
||||||
AND tlri.user_id = $2
|
AND tlri.user_id = $2
|
||||||
)
|
)
|
||||||
LEFT JOIN handmade_categorylastreadinfo AS clri ON (
|
LEFT JOIN handmade_subforumlastreadinfo AS slri ON (
|
||||||
clri.category_id = $1
|
slri.subforum_id = $1
|
||||||
AND clri.user_id = $2
|
AND slri.user_id = $2
|
||||||
)
|
)
|
||||||
WHERE
|
WHERE
|
||||||
thread.category_id = $1
|
thread.subforum_id = $1
|
||||||
AND NOT thread.deleted
|
AND NOT thread.deleted
|
||||||
ORDER BY lastpost.postdate DESC
|
ORDER BY lastpost.postdate DESC
|
||||||
LIMIT $3 OFFSET $4
|
LIMIT $3 OFFSET $4
|
||||||
`,
|
`,
|
||||||
cd.CatID,
|
cd.SubforumID,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
threadsPerPage,
|
threadsPerPage,
|
||||||
howManyThreadsToSkip,
|
howManyThreadsToSkip,
|
||||||
|
@ -146,13 +146,13 @@ func ForumCategory(c *RequestContext) ResponseData {
|
||||||
hasRead := false
|
hasRead := false
|
||||||
if row.ThreadLastReadTime != nil && row.ThreadLastReadTime.After(row.LastPost.PostDate) {
|
if row.ThreadLastReadTime != nil && row.ThreadLastReadTime.After(row.LastPost.PostDate) {
|
||||||
hasRead = true
|
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
|
hasRead = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return templates.ThreadListItem{
|
return templates.ThreadListItem{
|
||||||
Title: row.Thread.Title,
|
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),
|
FirstUser: templates.UserToTemplate(row.FirstUser, c.Theme),
|
||||||
FirstDate: row.FirstPost.PostDate,
|
FirstDate: row.FirstPost.PostDate,
|
||||||
LastUser: templates.UserToTemplate(row.LastUser, c.Theme),
|
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 {
|
if page == 1 {
|
||||||
subcatNodes := cd.CategoryTree[cd.CatID].Children
|
subforumNodes := cd.SubforumTree[cd.SubforumID].Children
|
||||||
|
|
||||||
for _, catNode := range subcatNodes {
|
for _, sfNode := range subforumNodes {
|
||||||
c.Perf.StartBlock("SQL", "Fetch count of subcategory threads")
|
c.Perf.StartBlock("SQL", "Fetch count of subforum threads")
|
||||||
// TODO(asaf): [PERF] [MINOR] Consider replacing querying count per subcat with a single query for all cats with GROUP BY.
|
// 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,
|
numThreads, err := db.QueryInt(c.Context(), c.Conn,
|
||||||
`
|
`
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM handmade_thread AS thread
|
FROM handmade_thread AS thread
|
||||||
WHERE
|
WHERE
|
||||||
thread.category_id = $1
|
thread.subforum_id = $1
|
||||||
AND NOT thread.deleted
|
AND NOT thread.deleted
|
||||||
`,
|
`,
|
||||||
catNode.ID,
|
sfNode.ID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(oops.New(err, "failed to get count of threads"))
|
panic(oops.New(err, "failed to get count of threads"))
|
||||||
}
|
}
|
||||||
c.Perf.EndBlock()
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
c.Perf.StartBlock("SQL", "Fetch subcategory threads")
|
c.Perf.StartBlock("SQL", "Fetch subforum threads")
|
||||||
// TODO(asaf): [PERF] [MINOR] Consider batching these.
|
// TODO(asaf): [PERF] [MINOR] Consider batching these.
|
||||||
itThreads, err := db.Query(c.Context(), c.Conn, threadQueryResult{},
|
itThreads, err := db.Query(c.Context(), c.Conn, threadQueryResult{},
|
||||||
`
|
`
|
||||||
|
@ -209,17 +209,17 @@ func ForumCategory(c *RequestContext) ResponseData {
|
||||||
tlri.thread_id = thread.id
|
tlri.thread_id = thread.id
|
||||||
AND tlri.user_id = $2
|
AND tlri.user_id = $2
|
||||||
)
|
)
|
||||||
LEFT JOIN handmade_categorylastreadinfo AS clri ON (
|
LEFT JOIN handmade_subforumlastreadinfo AS slri ON (
|
||||||
clri.category_id = $1
|
slri.subforum_id = $1
|
||||||
AND clri.user_id = $2
|
AND slri.user_id = $2
|
||||||
)
|
)
|
||||||
WHERE
|
WHERE
|
||||||
thread.category_id = $1
|
thread.subforum_id = $1
|
||||||
AND NOT thread.deleted
|
AND NOT thread.deleted
|
||||||
ORDER BY lastpost.postdate DESC
|
ORDER BY lastpost.postdate DESC
|
||||||
LIMIT 3
|
LIMIT 3
|
||||||
`,
|
`,
|
||||||
catNode.ID,
|
sfNode.ID,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -234,9 +234,9 @@ func ForumCategory(c *RequestContext) ResponseData {
|
||||||
threads = append(threads, makeThreadListItem(threadRow))
|
threads = append(threads, makeThreadListItem(threadRow))
|
||||||
}
|
}
|
||||||
|
|
||||||
subcats = append(subcats, forumSubcategoryData{
|
subforums = append(subforums, forumSubforumData{
|
||||||
Name: *catNode.Name,
|
Name: sfNode.Name,
|
||||||
Url: hmnurl.BuildForumCategory(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(catNode.ID), 1),
|
Url: hmnurl.BuildForum(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(sfNode.ID), 1),
|
||||||
Threads: threads,
|
Threads: threads,
|
||||||
TotalThreads: numThreads,
|
TotalThreads: numThreads,
|
||||||
})
|
})
|
||||||
|
@ -249,53 +249,53 @@ func ForumCategory(c *RequestContext) ResponseData {
|
||||||
|
|
||||||
baseData := getBaseData(c)
|
baseData := getBaseData(c)
|
||||||
baseData.Title = c.CurrentProject.Name + " Forums"
|
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,
|
Name: c.CurrentProject.Name,
|
||||||
Url: hmnurl.BuildProjectHomepage(c.CurrentProject.Slug),
|
Url: hmnurl.BuildProjectHomepage(c.CurrentProject.Slug),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Forums",
|
Name: "Forums",
|
||||||
Url: hmnurl.BuildForumCategory(c.CurrentProject.Slug, nil, 1),
|
Url: hmnurl.BuildForum(c.CurrentProject.Slug, nil, 1),
|
||||||
Current: true,
|
Current: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
currentSubforums := cd.LineageBuilder.GetSubforumLineage(cd.CatID)
|
currentSubforums := cd.LineageBuilder.GetSubforumLineage(cd.SubforumID)
|
||||||
for i, subforum := range currentSubforums {
|
for i, subforum := range currentSubforums {
|
||||||
baseData.Breadcrumbs = append(baseData.Breadcrumbs, templates.Breadcrumb{
|
baseData.Breadcrumbs = append(baseData.Breadcrumbs, templates.Breadcrumb{
|
||||||
Name: *subforum.Name, // NOTE(asaf): All subforum categories must have names.
|
Name: subforum.Name,
|
||||||
Url: hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs[0:i+1], 1),
|
Url: hmnurl.BuildForum(c.CurrentProject.Slug, currentSubforumSlugs[0:i+1], 1),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var res ResponseData
|
var res ResponseData
|
||||||
res.MustWriteTemplate("forum_category.html", forumCategoryData{
|
res.MustWriteTemplate("forum.html", forumData{
|
||||||
BaseData: baseData,
|
BaseData: baseData,
|
||||||
NewThreadUrl: hmnurl.BuildForumNewThread(c.CurrentProject.Slug, currentSubforumSlugs, false),
|
NewThreadUrl: hmnurl.BuildForumNewThread(c.CurrentProject.Slug, currentSubforumSlugs, false),
|
||||||
MarkReadUrl: hmnurl.BuildForumCategoryMarkRead(cd.CatID),
|
MarkReadUrl: hmnurl.BuildForumMarkRead(cd.SubforumID),
|
||||||
Threads: threads,
|
Threads: threads,
|
||||||
Pagination: templates.Pagination{
|
Pagination: templates.Pagination{
|
||||||
Current: page,
|
Current: page,
|
||||||
Total: numPages,
|
Total: numPages,
|
||||||
|
|
||||||
FirstUrl: hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs, 1),
|
FirstUrl: hmnurl.BuildForum(c.CurrentProject.Slug, currentSubforumSlugs, 1),
|
||||||
LastUrl: hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs, numPages),
|
LastUrl: hmnurl.BuildForum(c.CurrentProject.Slug, currentSubforumSlugs, numPages),
|
||||||
NextUrl: hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs, utils.IntClamp(1, page+1, numPages)),
|
NextUrl: hmnurl.BuildForum(c.CurrentProject.Slug, currentSubforumSlugs, utils.IntClamp(1, page+1, numPages)),
|
||||||
PreviousUrl: hmnurl.BuildForumCategory(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)
|
}, c.Perf)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
func ForumCategoryMarkRead(c *RequestContext) ResponseData {
|
func ForumMarkRead(c *RequestContext) ResponseData {
|
||||||
c.Perf.StartBlock("SQL", "Fetch category tree")
|
c.Perf.StartBlock("SQL", "Fetch subforum tree")
|
||||||
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
|
subforumTree := models.GetFullSubforumTree(c.Context(), c.Conn)
|
||||||
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
lineageBuilder := models.MakeSubforumLineageBuilder(subforumTree)
|
||||||
c.Perf.EndBlock()
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
catId, err := strconv.Atoi(c.PathParams["catid"])
|
sfId, err := strconv.Atoi(c.PathParams["sfid"])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return FourOhFour(c)
|
return FourOhFour(c)
|
||||||
}
|
}
|
||||||
|
@ -307,28 +307,28 @@ func ForumCategoryMarkRead(c *RequestContext) ResponseData {
|
||||||
defer tx.Rollback(c.Context())
|
defer tx.Rollback(c.Context())
|
||||||
|
|
||||||
// TODO(ben): Rework this logic when we rework blogs, threads, etc.
|
// TODO(ben): Rework this logic when we rework blogs, threads, etc.
|
||||||
catIds := []int{catId}
|
sfIds := []int{sfId}
|
||||||
if catId == 0 {
|
if sfId == 0 {
|
||||||
// Select all categories
|
// Select all categories
|
||||||
type catIdResult struct {
|
type sfIdResult struct {
|
||||||
CatID int `db:"id"`
|
SubforumID int `db:"id"`
|
||||||
}
|
}
|
||||||
cats, err := db.Query(c.Context(), tx, catIdResult{},
|
cats, err := db.Query(c.Context(), tx, sfIdResult{},
|
||||||
`
|
`
|
||||||
SELECT $columns
|
SELECT $columns
|
||||||
FROM handmade_category
|
FROM handmade_subforum
|
||||||
WHERE kind = ANY ($1)
|
WHERE kind = ANY ($1)
|
||||||
`,
|
`,
|
||||||
[]models.CategoryKind{models.CatKindBlog, models.CatKindForum, models.CatKindWiki, models.CatKindLibraryResource},
|
[]models.ThreadType{models.ThreadTypeProjectArticle, models.ThreadTypeForumPost},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch category IDs for CLRI"))
|
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch category IDs for CLRI"))
|
||||||
}
|
}
|
||||||
|
|
||||||
catIdResults := cats.ToSlice()
|
catIdResults := cats.ToSlice()
|
||||||
catIds = make([]int, len(catIdResults))
|
sfIds = make([]int, len(catIdResults))
|
||||||
for i, res := range 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
|
ON CONFLICT (category_id, user_id) DO UPDATE
|
||||||
SET lastread = EXCLUDED.lastread
|
SET lastread = EXCLUDED.lastread
|
||||||
`,
|
`,
|
||||||
catIds,
|
sfIds,
|
||||||
c.CurrentUser.ID,
|
c.CurrentUser.ID,
|
||||||
time.Now(),
|
time.Now(),
|
||||||
)
|
)
|
||||||
|
@ -364,7 +364,7 @@ func ForumCategoryMarkRead(c *RequestContext) ResponseData {
|
||||||
category_id = ANY ($1)
|
category_id = ANY ($1)
|
||||||
)
|
)
|
||||||
`,
|
`,
|
||||||
catIds,
|
sfIds,
|
||||||
c.CurrentUser.ID,
|
c.CurrentUser.ID,
|
||||||
)
|
)
|
||||||
c.Perf.EndBlock()
|
c.Perf.EndBlock()
|
||||||
|
@ -378,10 +378,10 @@ func ForumCategoryMarkRead(c *RequestContext) ResponseData {
|
||||||
}
|
}
|
||||||
|
|
||||||
var redirUrl string
|
var redirUrl string
|
||||||
if catId == 0 {
|
if sfId == 0 {
|
||||||
redirUrl = hmnurl.BuildFeed()
|
redirUrl = hmnurl.BuildFeed()
|
||||||
} else {
|
} 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)
|
return c.Redirect(redirUrl, http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
@ -392,7 +392,7 @@ type forumThreadData struct {
|
||||||
Thread templates.Thread
|
Thread templates.Thread
|
||||||
Posts []templates.Post
|
Posts []templates.Post
|
||||||
|
|
||||||
CategoryUrl string
|
SubforumUrl string
|
||||||
ReplyUrl string
|
ReplyUrl string
|
||||||
Pagination templates.Pagination
|
Pagination templates.Pagination
|
||||||
}
|
}
|
||||||
|
@ -405,7 +405,7 @@ func ForumThread(c *RequestContext) ResponseData {
|
||||||
return FourOhFour(c)
|
return FourOhFour(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
currentSubforumSlugs := cd.LineageBuilder.GetSubforumLineageSlugs(cd.CatID)
|
currentSubforumSlugs := cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID)
|
||||||
|
|
||||||
thread := cd.FetchThread(c.Context(), c.Conn)
|
thread := cd.FetchThread(c.Context(), c.Conn)
|
||||||
|
|
||||||
|
@ -519,7 +519,7 @@ func ForumThread(c *RequestContext) ResponseData {
|
||||||
BaseData: baseData,
|
BaseData: baseData,
|
||||||
Thread: templates.ThreadToTemplate(thread),
|
Thread: templates.ThreadToTemplate(thread),
|
||||||
Posts: posts,
|
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),
|
ReplyUrl: hmnurl.BuildForumPostReply(c.CurrentProject.Slug, currentSubforumSlugs, thread.ID, *thread.FirstID),
|
||||||
Pagination: pagination,
|
Pagination: pagination,
|
||||||
}, c.Perf)
|
}, c.Perf)
|
||||||
|
@ -542,12 +542,10 @@ func ForumPostRedirect(c *RequestContext) ResponseData {
|
||||||
FROM
|
FROM
|
||||||
handmade_post AS post
|
handmade_post AS post
|
||||||
WHERE
|
WHERE
|
||||||
post.category_id = $1
|
post.thread_id = $1
|
||||||
AND post.thread_id = $2
|
|
||||||
AND NOT post.deleted
|
AND NOT post.deleted
|
||||||
ORDER BY postdate
|
ORDER BY postdate
|
||||||
`,
|
`,
|
||||||
cd.CatID,
|
|
||||||
cd.ThreadID,
|
cd.ThreadID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -588,7 +586,7 @@ func ForumPostRedirect(c *RequestContext) ResponseData {
|
||||||
|
|
||||||
return c.Redirect(hmnurl.BuildForumThreadWithPostHash(
|
return c.Redirect(hmnurl.BuildForumThreadWithPostHash(
|
||||||
c.CurrentProject.Slug,
|
c.CurrentProject.Slug,
|
||||||
cd.LineageBuilder.GetSubforumLineageSlugs(cd.CatID),
|
cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID),
|
||||||
cd.ThreadID,
|
cd.ThreadID,
|
||||||
threadTitle,
|
threadTitle,
|
||||||
page,
|
page,
|
||||||
|
@ -610,7 +608,7 @@ func ForumNewThread(c *RequestContext) ResponseData {
|
||||||
var res ResponseData
|
var res ResponseData
|
||||||
res.MustWriteTemplate("editor.html", editorData{
|
res.MustWriteTemplate("editor.html", editorData{
|
||||||
BaseData: baseData,
|
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",
|
SubmitLabel: "Post New Thread",
|
||||||
}, c.Perf)
|
}, c.Perf)
|
||||||
return res
|
return res
|
||||||
|
@ -643,13 +641,15 @@ func ForumNewThreadSubmit(c *RequestContext) ResponseData {
|
||||||
var threadId int
|
var threadId int
|
||||||
err = tx.QueryRow(c.Context(),
|
err = tx.QueryRow(c.Context(),
|
||||||
`
|
`
|
||||||
INSERT INTO handmade_thread (title, sticky, category_id, first_id, last_id)
|
INSERT INTO handmade_thread (title, sticky, type, project_id, subforum_id, first_id, last_id)
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`,
|
`,
|
||||||
title,
|
title,
|
||||||
sticky,
|
sticky,
|
||||||
cd.CatID,
|
models.ThreadTypeForumPost,
|
||||||
|
c.CurrentProject.ID,
|
||||||
|
cd.SubforumID,
|
||||||
-1,
|
-1,
|
||||||
-1,
|
-1,
|
||||||
).Scan(&threadId)
|
).Scan(&threadId)
|
||||||
|
@ -657,7 +657,7 @@ func ForumNewThreadSubmit(c *RequestContext) ResponseData {
|
||||||
panic(oops.New(err, "failed to create thread"))
|
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
|
// Update thread with post id
|
||||||
_, err = tx.Exec(c.Context(),
|
_, 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"))
|
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)
|
return c.Redirect(newThreadUrl, http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -693,7 +693,7 @@ func ForumPostReply(c *RequestContext) ResponseData {
|
||||||
postData := cd.FetchPostAndStuff(c.Context(), c.Conn)
|
postData := cd.FetchPostAndStuff(c.Context(), c.Conn)
|
||||||
|
|
||||||
baseData := getBaseData(c)
|
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
|
baseData.MathjaxEnabled = true
|
||||||
// TODO(ben): Set breadcrumbs
|
// TODO(ben): Set breadcrumbs
|
||||||
|
|
||||||
|
@ -703,7 +703,7 @@ func ForumPostReply(c *RequestContext) ResponseData {
|
||||||
var res ResponseData
|
var res ResponseData
|
||||||
res.MustWriteTemplate("editor.html", editorData{
|
res.MustWriteTemplate("editor.html", editorData{
|
||||||
BaseData: baseData,
|
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",
|
SubmitLabel: "Submit Reply",
|
||||||
|
|
||||||
Title: "Replying to post",
|
Title: "Replying to post",
|
||||||
|
@ -729,14 +729,14 @@ func ForumPostReplySubmit(c *RequestContext) ResponseData {
|
||||||
|
|
||||||
unparsed := c.Req.Form.Get("body")
|
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())
|
err = tx.Commit(c.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to reply to forum post"))
|
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)
|
return c.Redirect(newPostUrl, http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -753,7 +753,7 @@ func ForumPostEdit(c *RequestContext) ResponseData {
|
||||||
postData := cd.FetchPostAndStuff(c.Context(), c.Conn)
|
postData := cd.FetchPostAndStuff(c.Context(), c.Conn)
|
||||||
|
|
||||||
baseData := getBaseData(c)
|
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
|
baseData.MathjaxEnabled = true
|
||||||
// TODO(ben): Set breadcrumbs
|
// TODO(ben): Set breadcrumbs
|
||||||
|
|
||||||
|
@ -763,7 +763,7 @@ func ForumPostEdit(c *RequestContext) ResponseData {
|
||||||
var res ResponseData
|
var res ResponseData
|
||||||
res.MustWriteTemplate("editor.html", editorData{
|
res.MustWriteTemplate("editor.html", editorData{
|
||||||
BaseData: baseData,
|
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,
|
Title: postData.Thread.Title,
|
||||||
SubmitLabel: "Submit Edited Post",
|
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"))
|
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)
|
return c.Redirect(postUrl, http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -818,7 +818,7 @@ func ForumPostDelete(c *RequestContext) ResponseData {
|
||||||
postData := cd.FetchPostAndStuff(c.Context(), c.Conn)
|
postData := cd.FetchPostAndStuff(c.Context(), c.Conn)
|
||||||
|
|
||||||
baseData := getBaseData(c)
|
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
|
baseData.MathjaxEnabled = true
|
||||||
// TODO(ben): Set breadcrumbs
|
// TODO(ben): Set breadcrumbs
|
||||||
|
|
||||||
|
@ -834,7 +834,7 @@ func ForumPostDelete(c *RequestContext) ResponseData {
|
||||||
var res ResponseData
|
var res ResponseData
|
||||||
res.MustWriteTemplate("forum_post_delete.html", forumPostDeleteData{
|
res.MustWriteTemplate("forum_post_delete.html", forumPostDeleteData{
|
||||||
BaseData: baseData,
|
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,
|
Post: templatePost,
|
||||||
}, c.Perf)
|
}, c.Perf)
|
||||||
return res
|
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"))
|
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)
|
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"))
|
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)
|
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
|
// Create post
|
||||||
err := tx.QueryRow(ctx,
|
err := tx.QueryRow(ctx,
|
||||||
`
|
`
|
||||||
INSERT INTO handmade_post (postdate, category_id, thread_id, current_id, author_id, category_kind, project_id, reply_id, preview)
|
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, $9)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`,
|
`,
|
||||||
time.Now(),
|
time.Now(),
|
||||||
catId,
|
|
||||||
threadId,
|
threadId,
|
||||||
|
models.ThreadTypeForumPost,
|
||||||
-1,
|
-1,
|
||||||
userId,
|
userId,
|
||||||
models.CatKindForum,
|
|
||||||
projectId,
|
projectId,
|
||||||
replyId,
|
replyId,
|
||||||
"", // empty preview, will be updated later
|
"", // empty preview, will be updated later
|
||||||
|
@ -1064,16 +1063,16 @@ func fixThreadPostIds(ctx context.Context, tx pgx.Tx, threadId int) error {
|
||||||
type commonForumData struct {
|
type commonForumData struct {
|
||||||
c *RequestContext
|
c *RequestContext
|
||||||
|
|
||||||
CatID int
|
SubforumID int
|
||||||
ThreadID int
|
ThreadID int
|
||||||
PostID int
|
PostID int
|
||||||
|
|
||||||
CategoryTree models.CategoryTree
|
SubforumTree models.SubforumTree
|
||||||
LineageBuilder *models.CategoryLineageBuilder
|
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.
|
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.
|
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")
|
c.Perf.StartBlock("FORUMS", "Fetch common forum data")
|
||||||
defer c.Perf.EndBlock()
|
defer c.Perf.EndBlock()
|
||||||
|
|
||||||
c.Perf.StartBlock("SQL", "Fetch category tree")
|
c.Perf.StartBlock("SQL", "Fetch subforum tree")
|
||||||
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
|
subforumTree := models.GetFullSubforumTree(c.Context(), c.Conn)
|
||||||
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
lineageBuilder := models.MakeSubforumLineageBuilder(subforumTree)
|
||||||
c.Perf.EndBlock()
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
res := commonForumData{
|
res := commonForumData{
|
||||||
c: c,
|
c: c,
|
||||||
CategoryTree: categoryTree,
|
SubforumTree: subforumTree,
|
||||||
LineageBuilder: lineageBuilder,
|
LineageBuilder: lineageBuilder,
|
||||||
}
|
}
|
||||||
|
|
||||||
if cats, hasCats := c.PathParams["cats"]; hasCats {
|
if subforums, hasSubforums := c.PathParams["subforums"]; hasSubforums {
|
||||||
catId, valid := validateSubforums(lineageBuilder, c.CurrentProject, cats)
|
sfId, valid := validateSubforums(lineageBuilder, c.CurrentProject, subforums)
|
||||||
if !valid {
|
if !valid {
|
||||||
return commonForumData{}, false
|
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 {
|
if threadIdStr, hasThreadId := c.PathParams["threadid"]; hasThreadId {
|
||||||
|
@ -1117,10 +1116,10 @@ func getCommonForumData(c *RequestContext) (commonForumData, bool) {
|
||||||
FROM handmade_thread
|
FROM handmade_thread
|
||||||
WHERE
|
WHERE
|
||||||
id = $1
|
id = $1
|
||||||
AND category_id = $2
|
AND subforum_id = $2
|
||||||
`,
|
`,
|
||||||
res.ThreadID,
|
res.ThreadID,
|
||||||
res.CatID,
|
res.SubforumID,
|
||||||
)
|
)
|
||||||
c.Perf.EndBlock()
|
c.Perf.EndBlock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1146,11 +1145,9 @@ func getCommonForumData(c *RequestContext) (commonForumData, bool) {
|
||||||
WHERE
|
WHERE
|
||||||
id = $1
|
id = $1
|
||||||
AND thread_id = $2
|
AND thread_id = $2
|
||||||
AND category_id = $3
|
|
||||||
`,
|
`,
|
||||||
res.PostID,
|
res.PostID,
|
||||||
res.ThreadID,
|
res.ThreadID,
|
||||||
res.CatID,
|
|
||||||
)
|
)
|
||||||
c.Perf.EndBlock()
|
c.Perf.EndBlock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1180,14 +1177,14 @@ func (cd *commonForumData) FetchThread(ctx context.Context, connOrTx db.ConnOrTx
|
||||||
SELECT $columns
|
SELECT $columns
|
||||||
FROM
|
FROM
|
||||||
handmade_thread AS thread
|
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
|
WHERE
|
||||||
thread.id = $1
|
thread.id = $1
|
||||||
AND NOT thread.deleted
|
AND NOT thread.deleted
|
||||||
AND cat.id = $2
|
AND sf.id = $2
|
||||||
`,
|
`,
|
||||||
cd.ThreadID,
|
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()
|
cd.c.Perf.EndBlock()
|
||||||
if err != nil {
|
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 author ON post.author_id = author.id
|
||||||
LEFT JOIN auth_user AS editor ON ver.editor_id = editor.id
|
LEFT JOIN auth_user AS editor ON ver.editor_id = editor.id
|
||||||
WHERE
|
WHERE
|
||||||
post.category_id = $1
|
post.thread_id = $1
|
||||||
AND post.thread_id = $2
|
AND post.id = $2
|
||||||
AND post.id = $3
|
|
||||||
AND NOT post.deleted
|
AND NOT post.deleted
|
||||||
`,
|
`,
|
||||||
cd.CatID,
|
|
||||||
cd.ThreadID,
|
cd.ThreadID,
|
||||||
cd.PostID,
|
cd.PostID,
|
||||||
)
|
)
|
||||||
|
@ -1274,27 +1269,27 @@ func (cd *commonForumData) UserCanEditPost(ctx context.Context, connOrTx db.Conn
|
||||||
return result.AuthorID != nil && *result.AuthorID == user.ID
|
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 {
|
if project.ForumID == nil {
|
||||||
return -1, false
|
return -1, false
|
||||||
}
|
}
|
||||||
|
|
||||||
subforumCatId := *project.ForumID
|
subforumId := *project.ForumID
|
||||||
if len(catPath) == 0 {
|
if len(sfPath) == 0 {
|
||||||
return subforumCatId, true
|
return subforumId, true
|
||||||
}
|
}
|
||||||
|
|
||||||
catPath = strings.ToLower(catPath)
|
sfPath = strings.ToLower(sfPath)
|
||||||
valid := false
|
valid := false
|
||||||
catSlugs := strings.Split(catPath, "/")
|
sfSlugs := strings.Split(sfPath, "/")
|
||||||
lastSlug := catSlugs[len(catSlugs)-1]
|
lastSlug := sfSlugs[len(sfSlugs)-1]
|
||||||
if len(lastSlug) > 0 {
|
if len(lastSlug) > 0 {
|
||||||
lastSlugCatId := lineageBuilder.FindIdBySlug(project.ID, lastSlug)
|
lastSlugSfId := lineageBuilder.FindIdBySlug(project.ID, lastSlug)
|
||||||
if lastSlugCatId != -1 {
|
if lastSlugSfId != -1 {
|
||||||
subforumSlugs := lineageBuilder.GetSubforumLineageSlugs(lastSlugCatId)
|
subforumSlugs := lineageBuilder.GetSubforumLineageSlugs(lastSlugSfId)
|
||||||
allMatch := true
|
allMatch := true
|
||||||
for i, subforum := range subforumSlugs {
|
for i, subforum := range subforumSlugs {
|
||||||
if subforum != catSlugs[i] {
|
if subforum != sfSlugs[i] {
|
||||||
allMatch = false
|
allMatch = false
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -1302,8 +1297,8 @@ func validateSubforums(lineageBuilder *models.CategoryLineageBuilder, project *m
|
||||||
valid = allMatch
|
valid = allMatch
|
||||||
}
|
}
|
||||||
if valid {
|
if valid {
|
||||||
subforumCatId = lastSlugCatId
|
subforumId = lastSlugSfId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return subforumCatId, valid
|
return subforumId, valid
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,9 +74,9 @@ func Index(c *RequestContext) ResponseData {
|
||||||
allProjects := iterProjects.ToSlice()
|
allProjects := iterProjects.ToSlice()
|
||||||
c.Perf.EndBlock()
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
c.Perf.StartBlock("SQL", "Fetch category tree")
|
c.Perf.StartBlock("SQL", "Fetch subforum tree")
|
||||||
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
|
subforumTree := models.GetFullSubforumTree(c.Context(), c.Conn)
|
||||||
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
lineageBuilder := models.MakeSubforumLineageBuilder(subforumTree)
|
||||||
c.Perf.EndBlock()
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
var currentUserId *int
|
var currentUserId *int
|
||||||
|
@ -90,12 +90,11 @@ func Index(c *RequestContext) ResponseData {
|
||||||
|
|
||||||
c.Perf.StartBlock("SQL", fmt.Sprintf("Fetch posts for %s", proj.Name))
|
c.Perf.StartBlock("SQL", fmt.Sprintf("Fetch posts for %s", proj.Name))
|
||||||
type projectPostQuery struct {
|
type projectPostQuery struct {
|
||||||
Post models.Post `db:"post"`
|
Post models.Post `db:"post"`
|
||||||
Thread models.Thread `db:"thread"`
|
Thread models.Thread `db:"thread"`
|
||||||
User models.User `db:"auth_user"`
|
User models.User `db:"auth_user"`
|
||||||
LibraryResource *models.LibraryResource `db:"lib_resource"`
|
ThreadLastReadTime *time.Time `db:"tlri.lastread"`
|
||||||
ThreadLastReadTime *time.Time `db:"tlri.lastread"`
|
SubforumLastReadTime *time.Time `db:"slri.lastread"`
|
||||||
CatLastReadTime *time.Time `db:"clri.lastread"`
|
|
||||||
}
|
}
|
||||||
projectPostIter, err := db.Query(c.Context(), c.Conn, projectPostQuery{},
|
projectPostIter, err := db.Query(c.Context(), c.Conn, projectPostQuery{},
|
||||||
`
|
`
|
||||||
|
@ -107,22 +106,21 @@ func Index(c *RequestContext) ResponseData {
|
||||||
tlri.thread_id = post.thread_id
|
tlri.thread_id = post.thread_id
|
||||||
AND tlri.user_id = $1
|
AND tlri.user_id = $1
|
||||||
)
|
)
|
||||||
LEFT JOIN handmade_categorylastreadinfo AS clri ON (
|
LEFT JOIN handmade_subforumlastreadinfo AS slri ON (
|
||||||
clri.category_id = post.category_id
|
slri.subforum_id = thread.subforum_id
|
||||||
AND clri.user_id = $1
|
AND slri.user_id = $1
|
||||||
)
|
)
|
||||||
LEFT JOIN auth_user ON post.author_id = auth_user.id
|
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
|
WHERE
|
||||||
post.project_id = $2
|
post.project_id = $2
|
||||||
AND post.category_kind IN ($3, $4, $5, $6)
|
AND post.thread_type = ANY ($3)
|
||||||
AND post.deleted = FALSE
|
AND post.deleted = FALSE
|
||||||
ORDER BY postdate DESC
|
ORDER BY postdate DESC
|
||||||
LIMIT $7
|
LIMIT $4
|
||||||
`,
|
`,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
proj.ID,
|
proj.ID,
|
||||||
models.CatKindBlog, models.CatKindForum, models.CatKindWiki, models.CatKindLibraryResource,
|
[]models.ThreadType{models.ThreadTypeProjectArticle, models.ThreadTypeForumPost},
|
||||||
maxPosts,
|
maxPosts,
|
||||||
)
|
)
|
||||||
c.Perf.EndBlock()
|
c.Perf.EndBlock()
|
||||||
|
@ -134,7 +132,7 @@ func Index(c *RequestContext) ResponseData {
|
||||||
|
|
||||||
forumsUrl := ""
|
forumsUrl := ""
|
||||||
if proj.ForumID != nil {
|
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 {
|
} else {
|
||||||
c.Logger.Error().Int("ProjectID", proj.ID).Str("ProjectName", proj.Name).Msg("Project fetched by landing page but it doesn't have forums")
|
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
|
hasRead := false
|
||||||
if projectPost.ThreadLastReadTime != nil && projectPost.ThreadLastReadTime.After(projectPost.Post.PostDate) {
|
if projectPost.ThreadLastReadTime != nil && projectPost.ThreadLastReadTime.After(projectPost.Post.PostDate) {
|
||||||
hasRead = true
|
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
|
hasRead = true
|
||||||
}
|
}
|
||||||
|
|
||||||
featurable := (!proj.IsHMN() &&
|
featurable := (!proj.IsHMN() &&
|
||||||
projectPost.Post.CategoryKind == models.CatKindBlog &&
|
projectPost.Post.ThreadType == models.ThreadTypeProjectArticle &&
|
||||||
projectPost.Post.ParentID == nil &&
|
projectPost.Post.ParentID == nil &&
|
||||||
landingPageProject.FeaturedPost == nil)
|
landingPageProject.FeaturedPost == nil)
|
||||||
|
|
||||||
|
@ -197,7 +195,6 @@ func Index(c *RequestContext) ResponseData {
|
||||||
&projectPost.Thread,
|
&projectPost.Thread,
|
||||||
&projectPost.Post,
|
&projectPost.Post,
|
||||||
&projectPost.User,
|
&projectPost.User,
|
||||||
projectPost.LibraryResource,
|
|
||||||
!hasRead,
|
!hasRead,
|
||||||
false,
|
false,
|
||||||
c.Theme,
|
c.Theme,
|
||||||
|
@ -263,14 +260,14 @@ func Index(c *RequestContext) ResponseData {
|
||||||
JOIN handmade_postversion AS ver ON post.current_id = ver.id
|
JOIN handmade_postversion AS ver ON post.current_id = ver.id
|
||||||
WHERE
|
WHERE
|
||||||
post.project_id = $1
|
post.project_id = $1
|
||||||
AND post.category_kind = $2
|
AND thread.type = $2
|
||||||
AND post.id = thread.first_id
|
AND post.id = thread.first_id
|
||||||
AND NOT thread.deleted
|
AND NOT thread.deleted
|
||||||
ORDER BY post.postdate DESC
|
ORDER BY post.postdate DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`,
|
`,
|
||||||
models.HMNProjectID,
|
models.HMNProjectID,
|
||||||
models.CatKindBlog,
|
models.ThreadTypeProjectArticle,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch news post"))
|
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_asset AS asset ON asset.id = snippet.asset_id
|
||||||
LEFT JOIN handmade_discordmessage AS discord_message ON discord_message.id = snippet.discord_message_id
|
LEFT JOIN handmade_discordmessage AS discord_message ON discord_message.id = snippet.discord_message_id
|
||||||
ORDER BY snippet.when DESC
|
ORDER BY snippet.when DESC
|
||||||
|
LIMIT 20
|
||||||
`,
|
`,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -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.
|
// 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(thread *models.Thread, post *models.Post, lineageBuilder *models.SubforumLineageBuilder, projectSlug string) string {
|
||||||
func UrlForGenericPost(post *models.Post, subforums []string, threadTitle string, libraryResourceId int, projectSlug string) string {
|
switch post.ThreadType {
|
||||||
switch post.CategoryKind {
|
case models.ThreadTypeProjectArticle:
|
||||||
case models.CatKindBlog:
|
|
||||||
return hmnurl.BuildBlogPost(projectSlug, post.ThreadID, post.ID)
|
return hmnurl.BuildBlogPost(projectSlug, post.ThreadID, post.ID)
|
||||||
case models.CatKindForum:
|
case models.ThreadTypeForumPost:
|
||||||
return hmnurl.BuildForumPost(projectSlug, subforums, post.ThreadID, post.ID)
|
return hmnurl.BuildForumPost(projectSlug, lineageBuilder.GetSubforumLineageSlugs(*thread.SubforumID), 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return hmnurl.BuildProjectHomepage(projectSlug)
|
return hmnurl.BuildProjectHomepage(projectSlug)
|
||||||
}
|
}
|
||||||
|
|
||||||
var PostTypeMap = map[models.CategoryKind][]templates.PostType{
|
var PostTypeMap = map[models.ThreadType][]templates.PostType{
|
||||||
models.CatKindBlog: []templates.PostType{templates.PostTypeBlogPost, templates.PostTypeBlogComment},
|
models.ThreadTypeProjectArticle: {templates.PostTypeBlogPost, templates.PostTypeBlogComment},
|
||||||
models.CatKindForum: []templates.PostType{templates.PostTypeForumThread, templates.PostTypeForumReply},
|
models.ThreadTypeForumPost: {templates.PostTypeForumThread, templates.PostTypeForumReply},
|
||||||
models.CatKindWiki: []templates.PostType{templates.PostTypeWikiCreate, templates.PostTypeWikiTalk},
|
|
||||||
models.CatKindLibraryResource: []templates.PostType{templates.PostTypeLibraryComment, templates.PostTypeLibraryComment},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var PostTypePrefix = map[templates.PostType]string{
|
var PostTypePrefix = map[templates.PostType]string{
|
||||||
templates.PostTypeBlogPost: "New blog post",
|
templates.PostTypeBlogPost: "New blog post",
|
||||||
templates.PostTypeBlogComment: "Blog comment",
|
templates.PostTypeBlogComment: "Blog comment",
|
||||||
templates.PostTypeForumThread: "New forum thread",
|
templates.PostTypeForumThread: "New forum thread",
|
||||||
templates.PostTypeForumReply: "Forum reply",
|
templates.PostTypeForumReply: "Forum reply",
|
||||||
templates.PostTypeWikiCreate: "New wiki page",
|
|
||||||
templates.PostTypeWikiTalk: "Wiki comment",
|
|
||||||
templates.PostTypeWikiEdit: "Wiki edit",
|
|
||||||
templates.PostTypeLibraryComment: "Library comment",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
var result []templates.Breadcrumb
|
||||||
result = append(result, templates.Breadcrumb{
|
result = append(result, templates.Breadcrumb{
|
||||||
Name: project.Name,
|
Name: project.Name,
|
||||||
Url: hmnurl.BuildProjectHomepage(project.Slug),
|
Url: hmnurl.BuildProjectHomepage(project.Slug),
|
||||||
})
|
})
|
||||||
result = append(result, templates.Breadcrumb{
|
result = append(result, templates.Breadcrumb{
|
||||||
Name: CategoryKindDisplayNames[post.CategoryKind],
|
Name: ThreadTypeDisplayNames[thread.Type],
|
||||||
Url: BuildProjectMainCategoryUrl(project.Slug, post.CategoryKind),
|
Url: BuildProjectRootResourceUrl(project.Slug, thread.Type),
|
||||||
})
|
})
|
||||||
switch post.CategoryKind {
|
switch thread.Type {
|
||||||
case models.CatKindForum:
|
case models.ThreadTypeForumPost:
|
||||||
subforums := lineageBuilder.GetSubforumLineage(post.CategoryID)
|
subforums := lineageBuilder.GetSubforumLineage(*thread.SubforumID)
|
||||||
slugs := lineageBuilder.GetSubforumLineageSlugs(post.CategoryID)
|
slugs := lineageBuilder.GetSubforumLineageSlugs(*thread.SubforumID)
|
||||||
for i, subforum := range subforums {
|
for i, subforum := range subforums {
|
||||||
result = append(result, templates.Breadcrumb{
|
result = append(result, templates.Breadcrumb{
|
||||||
Name: *subforum.Name, // NOTE(asaf): All subforum categories must have names.
|
Name: subforum.Name,
|
||||||
Url: hmnurl.BuildForumCategory(project.Slug, slugs[0:i+1], 1),
|
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
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE(asaf): THIS DOESN'T HANDLE WIKI EDIT ITEMS. Wiki edits are PostTextVersions, not Posts.
|
func MakePostListItem(
|
||||||
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 {
|
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
|
var result templates.PostListItem
|
||||||
|
|
||||||
result.Title = thread.Title
|
result.Title = thread.Title
|
||||||
result.User = templates.UserToTemplate(user, currentTheme)
|
result.User = templates.UserToTemplate(user, currentTheme)
|
||||||
result.Date = post.PostDate
|
result.Date = post.PostDate
|
||||||
result.Unread = unread
|
result.Unread = unread
|
||||||
libraryResourceId := 0
|
result.Url = UrlForGenericPost(thread, post, lineageBuilder, project.Slug)
|
||||||
if libraryResource != nil {
|
|
||||||
libraryResourceId = libraryResource.ID
|
|
||||||
}
|
|
||||||
result.Url = UrlForGenericPost(post, lineageBuilder.GetSubforumLineageSlugs(post.CategoryID), thread.Title, libraryResourceId, project.Slug)
|
|
||||||
result.Preview = post.Preview
|
result.Preview = post.Preview
|
||||||
|
|
||||||
postType := templates.PostTypeUnknown
|
postType := templates.PostTypeUnknown
|
||||||
postTypeOptions, found := PostTypeMap[post.CategoryKind]
|
postTypeOptions, found := PostTypeMap[post.ThreadType]
|
||||||
if found {
|
if found {
|
||||||
var hasParent int
|
var hasParent int
|
||||||
if post.ParentID != nil {
|
if post.ParentID != nil {
|
||||||
|
@ -104,7 +86,7 @@ func MakePostListItem(lineageBuilder *models.CategoryLineageBuilder, project *mo
|
||||||
result.PostTypePrefix = PostTypePrefix[result.PostType]
|
result.PostTypePrefix = PostTypePrefix[result.PostType]
|
||||||
|
|
||||||
if includeBreadcrumbs {
|
if includeBreadcrumbs {
|
||||||
result.Breadcrumbs = PostBreadcrumbs(lineageBuilder, project, post, libraryResource)
|
result.Breadcrumbs = PostBreadcrumbs(lineageBuilder, project, thread)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
|
@ -339,9 +339,9 @@ func ProjectHomepage(c *RequestContext) ResponseData {
|
||||||
}
|
}
|
||||||
c.Perf.EndBlock()
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
c.Perf.StartBlock("SQL", "Fetch category tree")
|
c.Perf.StartBlock("SQL", "Fetch subforum tree")
|
||||||
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
|
subforumTree := models.GetFullSubforumTree(c.Context(), c.Conn)
|
||||||
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
lineageBuilder := models.MakeSubforumLineageBuilder(subforumTree)
|
||||||
c.Perf.EndBlock()
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
c.Perf.StartBlock("SQL", "Fetching project timeline")
|
c.Perf.StartBlock("SQL", "Fetching project timeline")
|
||||||
|
@ -431,7 +431,6 @@ func ProjectHomepage(c *RequestContext) ResponseData {
|
||||||
&post.(*postQuery).Post,
|
&post.(*postQuery).Post,
|
||||||
&post.(*postQuery).Thread,
|
&post.(*postQuery).Thread,
|
||||||
project,
|
project,
|
||||||
nil,
|
|
||||||
&post.(*postQuery).Author,
|
&post.(*postQuery).Author,
|
||||||
c.Theme,
|
c.Theme,
|
||||||
))
|
))
|
||||||
|
|
|
@ -156,8 +156,8 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt
|
||||||
mainRoutes.GET(hmnurl.RegexForumNewThread, authMiddleware(ForumNewThread))
|
mainRoutes.GET(hmnurl.RegexForumNewThread, authMiddleware(ForumNewThread))
|
||||||
mainRoutes.POST(hmnurl.RegexForumNewThreadSubmit, authMiddleware(csrfMiddleware(ForumNewThreadSubmit)))
|
mainRoutes.POST(hmnurl.RegexForumNewThreadSubmit, authMiddleware(csrfMiddleware(ForumNewThreadSubmit)))
|
||||||
mainRoutes.GET(hmnurl.RegexForumThread, ForumThread)
|
mainRoutes.GET(hmnurl.RegexForumThread, ForumThread)
|
||||||
mainRoutes.GET(hmnurl.RegexForumCategory, ForumCategory)
|
mainRoutes.GET(hmnurl.RegexForum, Forum)
|
||||||
mainRoutes.POST(hmnurl.RegexForumCategoryMarkRead, authMiddleware(csrfMiddleware(ForumCategoryMarkRead)))
|
mainRoutes.POST(hmnurl.RegexForumMarkRead, authMiddleware(csrfMiddleware(ForumMarkRead)))
|
||||||
mainRoutes.GET(hmnurl.RegexForumPost, ForumPostRedirect)
|
mainRoutes.GET(hmnurl.RegexForumPost, ForumPostRedirect)
|
||||||
mainRoutes.GET(hmnurl.RegexForumPostReply, authMiddleware(ForumPostReply))
|
mainRoutes.GET(hmnurl.RegexForumPostReply, authMiddleware(ForumPostReply))
|
||||||
mainRoutes.POST(hmnurl.RegexForumPostReply, authMiddleware(csrfMiddleware(ForumPostReplySubmit)))
|
mainRoutes.POST(hmnurl.RegexForumPostReply, authMiddleware(csrfMiddleware(ForumPostReplySubmit)))
|
||||||
|
@ -216,8 +216,7 @@ func getBaseData(c *RequestContext) templates.BaseData {
|
||||||
ProjectHomepageUrl: hmnurl.BuildProjectHomepage(c.CurrentProject.Slug),
|
ProjectHomepageUrl: hmnurl.BuildProjectHomepage(c.CurrentProject.Slug),
|
||||||
ProjectIndexUrl: hmnurl.BuildProjectIndex(1),
|
ProjectIndexUrl: hmnurl.BuildProjectIndex(1),
|
||||||
BlogUrl: hmnurl.BuildBlog(c.CurrentProject.Slug, 1),
|
BlogUrl: hmnurl.BuildBlog(c.CurrentProject.Slug, 1),
|
||||||
ForumsUrl: hmnurl.BuildForumCategory(c.CurrentProject.Slug, nil, 1),
|
ForumsUrl: hmnurl.BuildForum(c.CurrentProject.Slug, nil, 1),
|
||||||
WikiUrl: hmnurl.BuildWiki(c.CurrentProject.Slug),
|
|
||||||
LibraryUrl: hmnurl.BuildLibrary(c.CurrentProject.Slug),
|
LibraryUrl: hmnurl.BuildLibrary(c.CurrentProject.Slug),
|
||||||
ManifestoUrl: hmnurl.BuildManifesto(),
|
ManifestoUrl: hmnurl.BuildManifesto(),
|
||||||
EpisodeGuideUrl: hmnurl.BuildHomepage(), // TODO(asaf)
|
EpisodeGuideUrl: hmnurl.BuildHomepage(), // TODO(asaf)
|
||||||
|
@ -231,7 +230,7 @@ func getBaseData(c *RequestContext) templates.BaseData {
|
||||||
CodeOfConductUrl: hmnurl.BuildCodeOfConduct(),
|
CodeOfConductUrl: hmnurl.BuildCodeOfConduct(),
|
||||||
CommunicationGuidelinesUrl: hmnurl.BuildCommunicationGuidelines(),
|
CommunicationGuidelinesUrl: hmnurl.BuildCommunicationGuidelines(),
|
||||||
ProjectIndexUrl: hmnurl.BuildProjectIndex(1),
|
ProjectIndexUrl: hmnurl.BuildProjectIndex(1),
|
||||||
ForumsUrl: hmnurl.BuildForumCategory(models.HMNProjectSlug, nil, 1),
|
ForumsUrl: hmnurl.BuildForum(models.HMNProjectSlug, nil, 1),
|
||||||
ContactUrl: hmnurl.BuildContactPage(),
|
ContactUrl: hmnurl.BuildContactPage(),
|
||||||
SitemapUrl: hmnurl.BuildSiteMap(),
|
SitemapUrl: hmnurl.BuildSiteMap(),
|
||||||
},
|
},
|
||||||
|
|
|
@ -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",
|
||||||
|
}
|
|
@ -10,12 +10,10 @@ import (
|
||||||
"git.handmade.network/hmn/hmn/src/templates"
|
"git.handmade.network/hmn/hmn/src/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
var TimelineTypeMap = map[models.CategoryKind][]templates.TimelineType{
|
var TimelineTypeMap = map[models.ThreadType][]templates.TimelineType{
|
||||||
// { No parent , Has parent }
|
// { No parent , Has parent }
|
||||||
models.CatKindBlog: {templates.TimelineTypeBlogPost, templates.TimelineTypeBlogComment},
|
models.ThreadTypeProjectArticle: {templates.TimelineTypeBlogPost, templates.TimelineTypeBlogComment},
|
||||||
models.CatKindForum: {templates.TimelineTypeForumThread, templates.TimelineTypeForumReply},
|
models.ThreadTypeForumPost: {templates.TimelineTypeForumThread, templates.TimelineTypeForumReply},
|
||||||
models.CatKindWiki: {templates.TimelineTypeWikiCreate, templates.TimelineTypeWikiTalk},
|
|
||||||
models.CatKindLibraryResource: {templates.TimelineTypeLibraryComment, templates.TimelineTypeLibraryComment},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var TimelineItemClassMap = map[templates.TimelineType]string{
|
var TimelineItemClassMap = map[templates.TimelineType]string{
|
||||||
|
@ -27,12 +25,6 @@ var TimelineItemClassMap = map[templates.TimelineType]string{
|
||||||
templates.TimelineTypeBlogPost: "blogs",
|
templates.TimelineTypeBlogPost: "blogs",
|
||||||
templates.TimelineTypeBlogComment: "blogs",
|
templates.TimelineTypeBlogComment: "blogs",
|
||||||
|
|
||||||
templates.TimelineTypeWikiCreate: "wiki",
|
|
||||||
templates.TimelineTypeWikiEdit: "wiki",
|
|
||||||
templates.TimelineTypeWikiTalk: "wiki",
|
|
||||||
|
|
||||||
templates.TimelineTypeLibraryComment: "library",
|
|
||||||
|
|
||||||
templates.TimelineTypeSnippetImage: "snippets",
|
templates.TimelineTypeSnippetImage: "snippets",
|
||||||
templates.TimelineTypeSnippetVideo: "snippets",
|
templates.TimelineTypeSnippetVideo: "snippets",
|
||||||
templates.TimelineTypeSnippetAudio: "snippets",
|
templates.TimelineTypeSnippetAudio: "snippets",
|
||||||
|
@ -48,21 +40,15 @@ var TimelineTypeTitleMap = map[templates.TimelineType]string{
|
||||||
templates.TimelineTypeBlogPost: "New blog post",
|
templates.TimelineTypeBlogPost: "New blog post",
|
||||||
templates.TimelineTypeBlogComment: "Blog comment",
|
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.TimelineTypeSnippetImage: "Snippet",
|
||||||
templates.TimelineTypeSnippetVideo: "Snippet",
|
templates.TimelineTypeSnippetVideo: "Snippet",
|
||||||
templates.TimelineTypeSnippetAudio: "Snippet",
|
templates.TimelineTypeSnippetAudio: "Snippet",
|
||||||
templates.TimelineTypeSnippetYoutube: "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
|
itemType := templates.TimelineTypeUnknown
|
||||||
typeByCatKind, found := TimelineTypeMap[post.CategoryKind]
|
typeByCatKind, found := TimelineTypeMap[post.ThreadType]
|
||||||
if found {
|
if found {
|
||||||
hasParent := 0
|
hasParent := 0
|
||||||
if post.ParentID != nil {
|
if post.ParentID != nil {
|
||||||
|
@ -71,17 +57,12 @@ func PostToTimelineItem(lineageBuilder *models.CategoryLineageBuilder, post *mod
|
||||||
itemType = typeByCatKind[hasParent]
|
itemType = typeByCatKind[hasParent]
|
||||||
}
|
}
|
||||||
|
|
||||||
libraryResourceId := 0
|
|
||||||
if libraryResource != nil {
|
|
||||||
libraryResourceId = libraryResource.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
return templates.TimelineItem{
|
return templates.TimelineItem{
|
||||||
Type: itemType,
|
Type: itemType,
|
||||||
TypeTitle: TimelineTypeTitleMap[itemType],
|
TypeTitle: TimelineTypeTitleMap[itemType],
|
||||||
Class: TimelineItemClassMap[itemType],
|
Class: TimelineItemClassMap[itemType],
|
||||||
Date: post.PostDate,
|
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),
|
OwnerAvatarUrl: templates.UserAvatarUrl(owner, currentTheme),
|
||||||
OwnerName: templates.UserDisplayName(owner),
|
OwnerName: templates.UserDisplayName(owner),
|
||||||
|
@ -89,25 +70,7 @@ func PostToTimelineItem(lineageBuilder *models.CategoryLineageBuilder, post *mod
|
||||||
Description: "", // NOTE(asaf): No description for posts
|
Description: "", // NOTE(asaf): No description for posts
|
||||||
|
|
||||||
Title: thread.Title,
|
Title: thread.Title,
|
||||||
Breadcrumbs: PostBreadcrumbs(lineageBuilder, project, post, libraryResource),
|
Breadcrumbs: PostBreadcrumbs(lineageBuilder, project, thread),
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,16 +5,12 @@ import (
|
||||||
"git.handmade.network/hmn/hmn/src/models"
|
"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 {
|
switch kind {
|
||||||
case models.CatKindBlog:
|
case models.ThreadTypeProjectArticle:
|
||||||
return hmnurl.BuildBlog(projectSlug, 1)
|
return hmnurl.BuildBlog(projectSlug, 1)
|
||||||
case models.CatKindForum:
|
case models.ThreadTypeForumPost:
|
||||||
return hmnurl.BuildForumCategory(projectSlug, nil, 1)
|
return hmnurl.BuildForum(projectSlug, nil, 1)
|
||||||
case models.CatKindWiki:
|
|
||||||
return hmnurl.BuildWiki(projectSlug)
|
|
||||||
case models.CatKindLibraryResource:
|
|
||||||
return hmnurl.BuildLibrary(projectSlug)
|
|
||||||
}
|
}
|
||||||
return hmnurl.BuildProjectHomepage(projectSlug)
|
return hmnurl.BuildProjectHomepage(projectSlug)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,8 +20,6 @@ type UserProfileTemplateData struct {
|
||||||
TimelineItems []templates.TimelineItem
|
TimelineItems []templates.TimelineItem
|
||||||
NumForums int
|
NumForums int
|
||||||
NumBlogs int
|
NumBlogs int
|
||||||
NumWiki int
|
|
||||||
NumLibrary int
|
|
||||||
NumSnippets int
|
NumSnippets int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,10 +113,9 @@ func UserProfile(c *RequestContext) ResponseData {
|
||||||
c.Perf.EndBlock()
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
type postQuery struct {
|
type postQuery struct {
|
||||||
Post models.Post `db:"post"`
|
Post models.Post `db:"post"`
|
||||||
Thread models.Thread `db:"thread"`
|
Thread models.Thread `db:"thread"`
|
||||||
LibraryResource *models.LibraryResource `db:"lib_resource"`
|
Project models.Project `db:"project"`
|
||||||
Project models.Project `db:"project"`
|
|
||||||
}
|
}
|
||||||
c.Perf.StartBlock("SQL", "Fetch posts")
|
c.Perf.StartBlock("SQL", "Fetch posts")
|
||||||
postQueryResult, err := db.Query(c.Context(), c.Conn, postQuery{},
|
postQueryResult, err := db.Query(c.Context(), c.Conn, postQuery{},
|
||||||
|
@ -128,7 +125,6 @@ func UserProfile(c *RequestContext) ResponseData {
|
||||||
handmade_post AS post
|
handmade_post AS post
|
||||||
INNER JOIN handmade_thread AS thread ON thread.id = post.thread_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
|
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
|
WHERE
|
||||||
post.author_id = $1
|
post.author_id = $1
|
||||||
AND project.lifecycle = ANY ($2)
|
AND project.lifecycle = ANY ($2)
|
||||||
|
@ -142,37 +138,6 @@ func UserProfile(c *RequestContext) ResponseData {
|
||||||
postQuerySlice := postQueryResult.ToSlice()
|
postQuerySlice := postQueryResult.ToSlice()
|
||||||
c.Perf.EndBlock()
|
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 {
|
type snippetQuery struct {
|
||||||
Snippet models.Snippet `db:"snippet"`
|
Snippet models.Snippet `db:"snippet"`
|
||||||
Asset *models.Asset `db:"asset"`
|
Asset *models.Asset `db:"asset"`
|
||||||
|
@ -197,17 +162,15 @@ func UserProfile(c *RequestContext) ResponseData {
|
||||||
snippetQuerySlice := snippetQueryResult.ToSlice()
|
snippetQuerySlice := snippetQueryResult.ToSlice()
|
||||||
c.Perf.EndBlock()
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
c.Perf.StartBlock("SQL", "Fetch category tree")
|
c.Perf.StartBlock("SQL", "Fetch subforum tree")
|
||||||
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
|
subforumTree := models.GetFullSubforumTree(c.Context(), c.Conn)
|
||||||
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
lineageBuilder := models.MakeSubforumLineageBuilder(subforumTree)
|
||||||
c.Perf.EndBlock()
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
c.Perf.StartBlock("PROFILE", "Construct timeline items")
|
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
|
numForums := 0
|
||||||
numBlogs := 0
|
numBlogs := 0
|
||||||
numWiki := len(wikiEditQuerySlice)
|
|
||||||
numLibrary := 0
|
|
||||||
numSnippets := len(snippetQuerySlice)
|
numSnippets := len(snippetQuerySlice)
|
||||||
|
|
||||||
for _, postRow := range postQuerySlice {
|
for _, postRow := range postQuerySlice {
|
||||||
|
@ -217,7 +180,6 @@ func UserProfile(c *RequestContext) ResponseData {
|
||||||
&postData.Post,
|
&postData.Post,
|
||||||
&postData.Thread,
|
&postData.Thread,
|
||||||
&postData.Project,
|
&postData.Project,
|
||||||
postData.LibraryResource,
|
|
||||||
profileUser,
|
profileUser,
|
||||||
c.Theme,
|
c.Theme,
|
||||||
)
|
)
|
||||||
|
@ -226,10 +188,6 @@ func UserProfile(c *RequestContext) ResponseData {
|
||||||
numForums += 1
|
numForums += 1
|
||||||
case templates.TimelineTypeBlogPost, templates.TimelineTypeBlogComment:
|
case templates.TimelineTypeBlogPost, templates.TimelineTypeBlogComment:
|
||||||
numBlogs += 1
|
numBlogs += 1
|
||||||
case templates.TimelineTypeWikiCreate, templates.TimelineTypeWikiTalk:
|
|
||||||
numWiki += 1
|
|
||||||
case templates.TimelineTypeLibraryComment:
|
|
||||||
numLibrary += 1
|
|
||||||
}
|
}
|
||||||
if timelineItem.Type != templates.TimelineTypeUnknown {
|
if timelineItem.Type != templates.TimelineTypeUnknown {
|
||||||
timelineItems = append(timelineItems, timelineItem)
|
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 {
|
for _, snippetRow := range snippetQuerySlice {
|
||||||
snippetData := snippetRow.(*snippetQuery)
|
snippetData := snippetRow.(*snippetQuery)
|
||||||
timelineItem := SnippetToTimelineItem(
|
timelineItem := SnippetToTimelineItem(
|
||||||
|
@ -282,8 +226,6 @@ func UserProfile(c *RequestContext) ResponseData {
|
||||||
TimelineItems: timelineItems,
|
TimelineItems: timelineItems,
|
||||||
NumForums: numForums,
|
NumForums: numForums,
|
||||||
NumBlogs: numBlogs,
|
NumBlogs: numBlogs,
|
||||||
NumWiki: numWiki,
|
|
||||||
NumLibrary: numLibrary,
|
|
||||||
NumSnippets: numSnippets,
|
NumSnippets: numSnippets,
|
||||||
}, c.Perf)
|
}, c.Perf)
|
||||||
return res
|
return res
|
||||||
|
|
Loading…
Reference in New Issue