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,
|
||||
BLOG_POST: 3,
|
||||
BLOG_COMMENT: 4,
|
||||
WIKI_CREATE: 5,
|
||||
WIKI_EDIT: 6,
|
||||
WIKI_TALK: 7,
|
||||
LIBRARY_COMMENT: 8,
|
||||
SNIPPET_IMAGE: 9,
|
||||
SNIPPET_VIDEO: 10,
|
||||
SNIPPET_AUDIO: 11,
|
||||
SNIPPET_YOUTUBE: 12
|
||||
SNIPPET_IMAGE: 5,
|
||||
SNIPPET_VIDEO: 6,
|
||||
SNIPPET_AUDIO: 7,
|
||||
SNIPPET_YOUTUBE: 8
|
||||
};
|
||||
|
||||
const showcaseItemTemplate = makeTemplateCloner("showcase_item");
|
||||
|
|
|
@ -8634,11 +8634,6 @@ input[type=submit] {
|
|||
padding-left: 10px;
|
||||
max-width: 80em; }
|
||||
|
||||
.wiki .post {
|
||||
padding: 0;
|
||||
margin: auto;
|
||||
max-width: 70em; }
|
||||
|
||||
.post .contents h1, .post .contents h2 {
|
||||
margin: 20px 0px; }
|
||||
|
||||
|
@ -8750,56 +8745,11 @@ input[type=submit] {
|
|||
.blog .post-list .post:nth-child(even) {
|
||||
background-color: transparent; }
|
||||
|
||||
.wiki .post p {
|
||||
margin: 10px 0px; }
|
||||
|
||||
.wiki .toc {
|
||||
border-color: #aaa;
|
||||
border-color: var(--wiki-border-color);
|
||||
border-left-width: 1px; }
|
||||
.wiki .toc .toc-number {
|
||||
color: #333;
|
||||
color: var(--wiki-toc-number-color); }
|
||||
.wiki .toc ul {
|
||||
list-style-type: none;
|
||||
margin-left: 10px;
|
||||
margin-bottom: 5px; }
|
||||
.wiki .toc li {
|
||||
margin-left: 0px; }
|
||||
|
||||
.wiki .aside {
|
||||
border-color: #aaa;
|
||||
border-color: var(--wiki-border-color);
|
||||
border-left-width: 1px;
|
||||
margin-left: 20px; }
|
||||
.wiki .aside::before {
|
||||
margin-left: -20px;
|
||||
margin-right: 15px;
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
content: "\21b4 "; }
|
||||
.wiki .aside .aside-heading {
|
||||
padding: 2px;
|
||||
margin: 1px;
|
||||
border-radius: 3px;
|
||||
border-width: 0px;
|
||||
cursor: pointer;
|
||||
display: inline;
|
||||
background-color: transparent; }
|
||||
.wiki .aside > .aside-body {
|
||||
overflow: hidden;
|
||||
padding-left: 10px; }
|
||||
.wiki .aside.folded::before {
|
||||
content: "\2192 "; }
|
||||
.wiki .aside.folded > .aside-body {
|
||||
max-height: 0px; }
|
||||
|
||||
.featured-post .meta .avatar-icon {
|
||||
left: -60px;
|
||||
bottom: -5px; }
|
||||
|
||||
.blog .body blockquote,
|
||||
.wiki .body blockquote {
|
||||
.blog .body blockquote {
|
||||
padding-top: 1px;
|
||||
padding-bottom: 1px; }
|
||||
|
||||
|
@ -9446,9 +9396,6 @@ span.icon-rss::before {
|
|||
.timeline.no-blogs .blogs {
|
||||
display: none; }
|
||||
|
||||
.timeline.no-wiki .wiki {
|
||||
display: none; }
|
||||
|
||||
.timeline.no-library .library {
|
||||
display: none; }
|
||||
|
||||
|
|
|
@ -300,8 +300,6 @@ will throw an error.
|
|||
--irc-users-popout-background: #181818;
|
||||
--irc-users-popout-border-color-left: #444;
|
||||
--irc-users-popout-border-color-right: #333;
|
||||
--wiki-border-color: #444;
|
||||
--wiki-toc-number-color: #bbb;
|
||||
--code-line-number-color: #444;
|
||||
--library-star-btn-background: #252525;
|
||||
--library-star-btn-border-color: #bbb;
|
||||
|
|
|
@ -318,8 +318,6 @@ will throw an error.
|
|||
--irc-users-popout-background: #fff;
|
||||
--irc-users-popout-border-color-left: #bbb;
|
||||
--irc-users-popout-border-color-right: #ccc;
|
||||
--wiki-border-color: #aaa;
|
||||
--wiki-toc-number-color: #333;
|
||||
--code-line-number-color: #777;
|
||||
--library-star-btn-background: #fff;
|
||||
--library-star-btn-border-color: #999;
|
||||
|
|
|
@ -22,7 +22,7 @@ import (
|
|||
Values of these kinds are ok to query even if they are not directly understood by pgtype.
|
||||
This is common for custom types like:
|
||||
|
||||
type CategoryKind int
|
||||
type ThreadType int
|
||||
*/
|
||||
var queryableKinds = []reflect.Kind{
|
||||
reflect.Int,
|
||||
|
|
|
@ -145,22 +145,22 @@ func TestPodcastRSS(t *testing.T) {
|
|||
AssertRegexMatch(t, BuildPodcastRSS(""), RegexPodcastRSS, nil)
|
||||
}
|
||||
|
||||
func TestForumCategory(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildForumCategory("", nil, 1), RegexForumCategory, nil)
|
||||
AssertRegexMatch(t, BuildForumCategory("", []string{"wip"}, 2), RegexForumCategory, map[string]string{"cats": "wip", "page": "2"})
|
||||
AssertRegexMatch(t, BuildForumCategory("", []string{"sub", "wip"}, 2), RegexForumCategory, map[string]string{"cats": "sub/wip", "page": "2"})
|
||||
AssertSubdomain(t, BuildForumCategory("hmn", nil, 1), "")
|
||||
AssertSubdomain(t, BuildForumCategory("", nil, 1), "")
|
||||
AssertSubdomain(t, BuildForumCategory("hero", nil, 1), "hero")
|
||||
assert.Panics(t, func() { BuildForumCategory("", nil, 0) })
|
||||
assert.Panics(t, func() { BuildForumCategory("", []string{"", "wip"}, 1) })
|
||||
assert.Panics(t, func() { BuildForumCategory("", []string{" ", "wip"}, 1) })
|
||||
assert.Panics(t, func() { BuildForumCategory("", []string{"wip/jobs"}, 1) })
|
||||
func TestForum(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildForum("", nil, 1), RegexForum, nil)
|
||||
AssertRegexMatch(t, BuildForum("", []string{"wip"}, 2), RegexForum, map[string]string{"subforums": "wip", "page": "2"})
|
||||
AssertRegexMatch(t, BuildForum("", []string{"sub", "wip"}, 2), RegexForum, map[string]string{"subforums": "sub/wip", "page": "2"})
|
||||
AssertSubdomain(t, BuildForum("hmn", nil, 1), "")
|
||||
AssertSubdomain(t, BuildForum("", nil, 1), "")
|
||||
AssertSubdomain(t, BuildForum("hero", nil, 1), "hero")
|
||||
assert.Panics(t, func() { BuildForum("", nil, 0) })
|
||||
assert.Panics(t, func() { BuildForum("", []string{"", "wip"}, 1) })
|
||||
assert.Panics(t, func() { BuildForum("", []string{" ", "wip"}, 1) })
|
||||
assert.Panics(t, func() { BuildForum("", []string{"wip/jobs"}, 1) })
|
||||
}
|
||||
|
||||
func TestForumNewThread(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildForumNewThread("", []string{"sub", "wip"}, false), RegexForumNewThread, map[string]string{"cats": "sub/wip"})
|
||||
AssertRegexMatch(t, BuildForumNewThread("", []string{"sub", "wip"}, true), RegexForumNewThreadSubmit, map[string]string{"cats": "sub/wip"})
|
||||
AssertRegexMatch(t, BuildForumNewThread("", []string{"sub", "wip"}, false), RegexForumNewThread, map[string]string{"subforums": "sub/wip"})
|
||||
AssertRegexMatch(t, BuildForumNewThread("", []string{"sub", "wip"}, true), RegexForumNewThreadSubmit, map[string]string{"subforums": "sub/wip"})
|
||||
}
|
||||
|
||||
func TestForumThread(t *testing.T) {
|
||||
|
@ -197,12 +197,6 @@ func TestForumPostReply(t *testing.T) {
|
|||
AssertSubdomain(t, BuildForumPostReply("hero", nil, 1, 2), "hero")
|
||||
}
|
||||
|
||||
func TestForumPostQuote(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildForumPostQuote("", nil, 1, 2), RegexForumPostQuote, map[string]string{"threadid": "1", "postid": "2"})
|
||||
AssertRegexNoMatch(t, BuildForumPostQuote("", nil, 1, 2), RegexForumPost)
|
||||
AssertSubdomain(t, BuildForumPostQuote("hero", nil, 1, 2), "hero")
|
||||
}
|
||||
|
||||
func TestBlog(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildBlog("", 1), RegexBlog, nil)
|
||||
AssertRegexMatch(t, BuildBlog("", 2), RegexBlog, map[string]string{"page": "2"})
|
||||
|
@ -248,82 +242,6 @@ func TestBlogPostQuote(t *testing.T) {
|
|||
AssertSubdomain(t, BuildBlogPostQuote("hero", 1, 2), "hero")
|
||||
}
|
||||
|
||||
func TestWiki(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildWiki(""), RegexWiki, nil)
|
||||
AssertSubdomain(t, BuildWiki("hero"), "hero")
|
||||
}
|
||||
|
||||
func TestWikiIndex(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildWikiIndex(""), RegexWikiIndex, nil)
|
||||
AssertSubdomain(t, BuildWikiIndex("hero"), "hero")
|
||||
}
|
||||
|
||||
func TestWikiArticle(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildWikiArticle("", 1, ""), RegexWikiArticle, map[string]string{"articleid": "1"})
|
||||
AssertRegexMatch(t, BuildWikiArticle("", 1, "wiki/title/--"), RegexWikiArticle, map[string]string{"articleid": "1"})
|
||||
AssertRegexMatch(t, BuildWikiArticleWithSectionName("", 1, "wiki/title/--", "Hello world"), RegexWikiArticle, map[string]string{"articleid": "1"})
|
||||
AssertSubdomain(t, BuildWikiArticle("hero", 1, ""), "hero")
|
||||
}
|
||||
|
||||
func TestWikiArticleEdit(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildWikiArticleEdit("", 1), RegexWikiArticleEdit, map[string]string{"articleid": "1"})
|
||||
AssertSubdomain(t, BuildWikiArticleEdit("hero", 1), "hero")
|
||||
}
|
||||
|
||||
func TestWikiArticleDelete(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildWikiArticleDelete("", 1), RegexWikiArticleDelete, map[string]string{"articleid": "1"})
|
||||
AssertSubdomain(t, BuildWikiArticleDelete("hero", 1), "hero")
|
||||
}
|
||||
|
||||
func TestWikiArticleHistory(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildWikiArticleHistory("", 1, ""), RegexWikiArticleHistory, map[string]string{"articleid": "1"})
|
||||
AssertRegexMatch(t, BuildWikiArticleHistory("", 1, "wiki/title/--"), RegexWikiArticleHistory, map[string]string{"articleid": "1"})
|
||||
AssertSubdomain(t, BuildWikiArticleHistory("hero", 1, ""), "hero")
|
||||
}
|
||||
|
||||
func TestWikiTalk(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildWikiTalk("", 1, ""), RegexWikiTalk, map[string]string{"articleid": "1"})
|
||||
AssertRegexMatch(t, BuildWikiTalk("", 1, "wiki/title/--"), RegexWikiTalk, map[string]string{"articleid": "1"})
|
||||
AssertSubdomain(t, BuildWikiTalk("hero", 1, ""), "hero")
|
||||
}
|
||||
|
||||
func TestWikiRevision(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildWikiRevision("", 1, "", 2), RegexWikiRevision, map[string]string{"articleid": "1", "revisionid": "2"})
|
||||
AssertRegexMatch(t, BuildWikiRevision("", 1, "wiki/title/--", 2), RegexWikiRevision, map[string]string{"articleid": "1", "revisionid": "2"})
|
||||
AssertSubdomain(t, BuildWikiRevision("hero", 1, "", 2), "hero")
|
||||
}
|
||||
|
||||
func TestWikiDiff(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildWikiDiff("", 1, "", 2, 3), RegexWikiDiff, map[string]string{"articleid": "1", "revisionidold": "2", "revisionidnew": "3"})
|
||||
AssertRegexMatch(t, BuildWikiDiff("", 1, "wiki/title", 2, 3), RegexWikiDiff, map[string]string{"articleid": "1", "revisionidold": "2", "revisionidnew": "3"})
|
||||
AssertSubdomain(t, BuildWikiDiff("hero", 1, "wiki/title", 2, 3), "hero")
|
||||
}
|
||||
|
||||
func TestWikiTalkPost(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildWikiTalkPost("", 1, 2), RegexWikiTalkPost, map[string]string{"articleid": "1", "postid": "2"})
|
||||
AssertSubdomain(t, BuildWikiTalkPost("hero", 1, 2), "hero")
|
||||
}
|
||||
|
||||
func TestWikiTalkPostDelete(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildWikiTalkPostDelete("", 1, 2), RegexWikiTalkPostDelete, map[string]string{"articleid": "1", "postid": "2"})
|
||||
AssertSubdomain(t, BuildWikiTalkPostDelete("hero", 1, 2), "hero")
|
||||
}
|
||||
|
||||
func TestWikiTalkPostEdit(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildWikiTalkPostEdit("", 1, 2), RegexWikiTalkPostEdit, map[string]string{"articleid": "1", "postid": "2"})
|
||||
AssertSubdomain(t, BuildWikiTalkPostEdit("hero", 1, 2), "hero")
|
||||
}
|
||||
|
||||
func TestWikiTalkPostReply(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildWikiTalkPostReply("", 1, 2), RegexWikiTalkPostReply, map[string]string{"articleid": "1", "postid": "2"})
|
||||
AssertSubdomain(t, BuildWikiTalkPostReply("hero", 1, 2), "hero")
|
||||
}
|
||||
|
||||
func TestWikiTalkPostQuote(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildWikiTalkPostQuote("", 1, 2), RegexWikiTalkPostQuote, map[string]string{"articleid": "1", "postid": "2"})
|
||||
AssertSubdomain(t, BuildWikiTalkPostQuote("hero", 1, 2), "hero")
|
||||
}
|
||||
|
||||
func TestLibrary(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildLibrary(""), RegexLibrary, nil)
|
||||
AssertSubdomain(t, BuildLibrary("hero"), "hero")
|
||||
|
@ -344,38 +262,6 @@ func TestLibraryResource(t *testing.T) {
|
|||
AssertSubdomain(t, BuildLibraryResource("hero", 1), "hero")
|
||||
}
|
||||
|
||||
func TestLibraryDiscussion(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildLibraryDiscussion("", 1, 2, 1), RegexLibraryDiscussion, map[string]string{"resourceid": "1", "threadid": "2"})
|
||||
AssertRegexMatch(t, BuildLibraryDiscussion("", 1, 2, 3), RegexLibraryDiscussion, map[string]string{"resourceid": "1", "threadid": "2", "page": "3"})
|
||||
AssertRegexMatch(t, BuildLibraryDiscussionWithPostHash("", 1, 2, 3, 123), RegexLibraryDiscussion, map[string]string{"resourceid": "1", "threadid": "2", "page": "3"})
|
||||
AssertSubdomain(t, BuildLibraryDiscussion("hero", 1, 2, 3), "hero")
|
||||
}
|
||||
|
||||
func TestLibraryPost(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildLibraryPost("", 1, 2, 3), RegexLibraryPost, map[string]string{"resourceid": "1", "threadid": "2", "postid": "3"})
|
||||
AssertSubdomain(t, BuildLibraryPost("hero", 1, 2, 3), "hero")
|
||||
}
|
||||
|
||||
func TestLibraryPostDelete(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildLibraryPostDelete("", 1, 2, 3), RegexLibraryPostDelete, map[string]string{"resourceid": "1", "threadid": "2", "postid": "3"})
|
||||
AssertSubdomain(t, BuildLibraryPostDelete("hero", 1, 2, 3), "hero")
|
||||
}
|
||||
|
||||
func TestLibraryPostEdit(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildLibraryPostEdit("", 1, 2, 3), RegexLibraryPostEdit, map[string]string{"resourceid": "1", "threadid": "2", "postid": "3"})
|
||||
AssertSubdomain(t, BuildLibraryPostEdit("hero", 1, 2, 3), "hero")
|
||||
}
|
||||
|
||||
func TestLibraryPostReply(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildLibraryPostReply("", 1, 2, 3), RegexLibraryPostReply, map[string]string{"resourceid": "1", "threadid": "2", "postid": "3"})
|
||||
AssertSubdomain(t, BuildLibraryPostReply("hero", 1, 2, 3), "hero")
|
||||
}
|
||||
|
||||
func TestLibraryPostQuote(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildLibraryPostQuote("", 1, 2, 3), RegexLibraryPostQuote, map[string]string{"resourceid": "1", "threadid": "2", "postid": "3"})
|
||||
AssertSubdomain(t, BuildLibraryPostQuote("hero", 1, 2, 3), "hero")
|
||||
}
|
||||
|
||||
func TestProjectCSS(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildProjectCSS("000000"), RegexProjectCSS, nil)
|
||||
}
|
||||
|
@ -395,8 +281,8 @@ func TestPublic(t *testing.T) {
|
|||
AssertRegexMatch(t, BuildUserFile("mylogo.png"), RegexPublic, nil)
|
||||
}
|
||||
|
||||
func TestForumCategoryMarkRead(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildForumCategoryMarkRead(5), RegexForumCategoryMarkRead, map[string]string{"catid": "5"})
|
||||
func TestForumMarkRead(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildForumMarkRead(5), RegexForumMarkRead, map[string]string{"sfid": "5"})
|
||||
}
|
||||
|
||||
func AssertSubdomain(t *testing.T, fullUrl string, expectedSubdomain string) {
|
||||
|
@ -468,3 +354,9 @@ func AssertRegexNoMatch(t *testing.T, fullUrl string, regex *regexp.Regexp) {
|
|||
match := regex.FindStringSubmatch(requestPath)
|
||||
assert.Nilf(t, match, "Url matched regex: [%s] vs [%s]", requestPath, regex.String())
|
||||
}
|
||||
|
||||
func TestThingsThatDontNeedCoverage(t *testing.T) {
|
||||
// look the other way ಠ_ಠ
|
||||
BuildPodcastEpisodeFile("foo", "bar")
|
||||
BuildS3Asset("ha ha")
|
||||
}
|
||||
|
|
|
@ -305,17 +305,17 @@ func BuildPodcastEpisodeFile(projectSlug string, filename string) string {
|
|||
* Forums
|
||||
*/
|
||||
|
||||
// TODO(asaf): This also matches urls generated by BuildForumThread (/t/ is identified as a cat, and the threadid as a page)
|
||||
// This shouldn't be a problem since we will match Thread before Category in the router, but should we enforce it here?
|
||||
var RegexForumCategory = regexp.MustCompile(`^/forums(/(?P<cats>[^\d/]+(/[^\d]+)*))?(/(?P<page>\d+))?$`)
|
||||
// TODO(asaf): This also matches urls generated by BuildForumThread (/t/ is identified as a subforum, and the threadid as a page)
|
||||
// This shouldn't be a problem since we will match Thread before Subforum in the router, but should we enforce it here?
|
||||
var RegexForum = regexp.MustCompile(`^/forums(/(?P<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()
|
||||
if page < 1 {
|
||||
panic(oops.New(nil, "Invalid forum thread page (%d), must be >= 1", page))
|
||||
}
|
||||
|
||||
builder := buildForumCategoryPath(subforums)
|
||||
builder := buildSubforumPath(subforums)
|
||||
|
||||
if page > 1 {
|
||||
builder.WriteRune('/')
|
||||
|
@ -325,12 +325,12 @@ func BuildForumCategory(projectSlug string, subforums []string, page int) string
|
|||
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||
}
|
||||
|
||||
var RegexForumNewThread = regexp.MustCompile(`^/forums(/(?P<cats>[^\d/]+(/[^\d]+)*))?/t/new$`)
|
||||
var RegexForumNewThreadSubmit = regexp.MustCompile(`^/forums(/(?P<cats>[^\d/]+(/[^\d]+)*))?/t/new/submit$`)
|
||||
var RegexForumNewThread = regexp.MustCompile(`^/forums(/(?P<subforums>[^\d/]+(/[^\d]+)*))?/t/new$`)
|
||||
var RegexForumNewThreadSubmit = regexp.MustCompile(`^/forums(/(?P<subforums>[^\d/]+(/[^\d]+)*))?/t/new/submit$`)
|
||||
|
||||
func BuildForumNewThread(projectSlug string, subforums []string, submit bool) string {
|
||||
defer CatchPanic()
|
||||
builder := buildForumCategoryPath(subforums)
|
||||
builder := buildSubforumPath(subforums)
|
||||
builder.WriteString("/t/new")
|
||||
if submit {
|
||||
builder.WriteString("/submit")
|
||||
|
@ -339,7 +339,7 @@ func BuildForumNewThread(projectSlug string, subforums []string, submit bool) st
|
|||
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||
}
|
||||
|
||||
var RegexForumThread = regexp.MustCompile(`^/forums(/(?P<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 {
|
||||
defer CatchPanic()
|
||||
|
@ -355,7 +355,7 @@ func BuildForumThreadWithPostHash(projectSlug string, subforums []string, thread
|
|||
return ProjectUrlWithFragment(builder.String(), nil, projectSlug, strconv.Itoa(postId))
|
||||
}
|
||||
|
||||
var RegexForumPost = regexp.MustCompile(`^/forums(/(?P<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 {
|
||||
defer CatchPanic()
|
||||
|
@ -364,7 +364,7 @@ func BuildForumPost(projectSlug string, subforums []string, threadId int, postId
|
|||
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||
}
|
||||
|
||||
var RegexForumPostDelete = regexp.MustCompile(`^/forums(/(?P<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 {
|
||||
defer CatchPanic()
|
||||
|
@ -373,7 +373,7 @@ func BuildForumPostDelete(projectSlug string, subforums []string, threadId int,
|
|||
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||
}
|
||||
|
||||
var RegexForumPostEdit = regexp.MustCompile(`^/forums(/(?P<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 {
|
||||
defer CatchPanic()
|
||||
|
@ -382,7 +382,7 @@ func BuildForumPostEdit(projectSlug string, subforums []string, threadId int, po
|
|||
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||
}
|
||||
|
||||
var RegexForumPostReply = regexp.MustCompile(`^/forums(/(?P<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?
|
||||
func BuildForumPostReply(projectSlug string, subforums []string, threadId int, postId int) string {
|
||||
|
@ -472,163 +472,6 @@ func BuildBlogPostQuote(projectSlug string, threadId int, postId int) string {
|
|||
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||
}
|
||||
|
||||
/*
|
||||
* Wiki
|
||||
*/
|
||||
|
||||
var RegexWiki = regexp.MustCompile(`^/wiki$`)
|
||||
|
||||
func BuildWiki(projectSlug string) string {
|
||||
defer CatchPanic()
|
||||
return ProjectUrl("/wiki", nil, projectSlug)
|
||||
}
|
||||
|
||||
var RegexWikiIndex = regexp.MustCompile(`^/wiki/index$`)
|
||||
|
||||
func BuildWikiIndex(projectSlug string) string {
|
||||
defer CatchPanic()
|
||||
return ProjectUrl("/wiki/index", nil, projectSlug)
|
||||
}
|
||||
|
||||
var RegexWikiArticle = regexp.MustCompile(`^/wiki/(?P<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
|
||||
*/
|
||||
|
@ -671,74 +514,6 @@ func BuildLibraryResource(projectSlug string, resourceId int) string {
|
|||
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
|
||||
*/
|
||||
|
@ -803,18 +578,18 @@ func BuildUserFile(filepath string) string {
|
|||
* Other
|
||||
*/
|
||||
|
||||
var RegexForumCategoryMarkRead = regexp.MustCompile(`^/markread/(?P<catid>\d+)$`)
|
||||
var RegexForumMarkRead = regexp.MustCompile(`^/markread/(?P<sfid>\d+)$`)
|
||||
|
||||
// NOTE(asaf): categoryId == 0 means ALL CATEGORIES
|
||||
func BuildForumCategoryMarkRead(categoryId int) string {
|
||||
// NOTE(asaf): subforumId == 0 means ALL SUBFORUMS
|
||||
func BuildForumMarkRead(subforumId int) string {
|
||||
defer CatchPanic()
|
||||
if categoryId < 0 {
|
||||
panic(oops.New(nil, "Invalid category ID (%d), must be >= 0", categoryId))
|
||||
if subforumId < 0 {
|
||||
panic(oops.New(nil, "Invalid subforum ID (%d), must be >= 0", subforumId))
|
||||
}
|
||||
|
||||
var builder strings.Builder
|
||||
builder.WriteString("/markread/")
|
||||
builder.WriteString(strconv.Itoa(categoryId))
|
||||
builder.WriteString(strconv.Itoa(subforumId))
|
||||
|
||||
return Url(builder.String(), nil)
|
||||
}
|
||||
|
@ -825,7 +600,7 @@ var RegexCatchAll = regexp.MustCompile("")
|
|||
* Helper functions
|
||||
*/
|
||||
|
||||
func buildForumCategoryPath(subforums []string) *strings.Builder {
|
||||
func buildSubforumPath(subforums []string) *strings.Builder {
|
||||
for _, subforum := range subforums {
|
||||
if strings.Contains(subforum, "/") {
|
||||
panic(oops.New(nil, "Tried building forum url with / in subforum name"))
|
||||
|
@ -855,7 +630,7 @@ func buildForumThreadPath(subforums []string, threadId int, title string, page i
|
|||
panic(oops.New(nil, "Invalid forum thread ID (%d), must be >= 1", threadId))
|
||||
}
|
||||
|
||||
builder := buildForumCategoryPath(subforums)
|
||||
builder := buildSubforumPath(subforums)
|
||||
|
||||
builder.WriteString("/t/")
|
||||
builder.WriteString(strconv.Itoa(threadId))
|
||||
|
@ -880,7 +655,7 @@ func buildForumPostPath(subforums []string, threadId int, postId int) *strings.B
|
|||
panic(oops.New(nil, "Invalid forum post ID (%d), must be >= 1", postId))
|
||||
}
|
||||
|
||||
builder := buildForumCategoryPath(subforums)
|
||||
builder := buildSubforumPath(subforums)
|
||||
|
||||
builder.WriteString("/t/")
|
||||
builder.WriteString(strconv.Itoa(threadId))
|
||||
|
@ -934,34 +709,6 @@ func buildBlogPostPath(threadId int, postId int) *strings.Builder {
|
|||
return &builder
|
||||
}
|
||||
|
||||
func buildWikiArticlePath(articleId int, title string) *strings.Builder {
|
||||
if articleId < 1 {
|
||||
panic(oops.New(nil, "Invalid wiki article ID (%d), must be >= 1", articleId))
|
||||
}
|
||||
|
||||
var builder strings.Builder
|
||||
|
||||
builder.WriteString("/wiki/")
|
||||
builder.WriteString(strconv.Itoa(articleId))
|
||||
if len(title) > 0 {
|
||||
builder.WriteRune('-')
|
||||
builder.WriteString(PathSafeTitle(title))
|
||||
}
|
||||
|
||||
return &builder
|
||||
}
|
||||
|
||||
func buildWikiTalkPath(articleId int, postId int) *strings.Builder {
|
||||
if postId < 1 {
|
||||
panic(oops.New(nil, "Invalid wiki post ID (%d), must be >= 1", postId))
|
||||
}
|
||||
|
||||
builder := buildWikiArticlePath(articleId, "")
|
||||
builder.WriteString("/talk/")
|
||||
builder.WriteString(strconv.Itoa(postId))
|
||||
return builder
|
||||
}
|
||||
|
||||
func buildLibraryResourcePath(resourceId int) *strings.Builder {
|
||||
if resourceId < 1 {
|
||||
panic(oops.New(nil, "Invalid library resource ID (%d), must be >= 1", resourceId))
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"time"
|
||||
|
||||
"git.handmade.network/hmn/hmn/src/migration/types"
|
||||
"git.handmade.network/hmn/hmn/src/oops"
|
||||
"github.com/jackc/pgx/v4"
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
import "git.handmade.network/hmn/hmn/src/migration/types"
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"git.handmade.network/hmn/hmn/src/migration/types"
|
||||
"github.com/jackc/pgx/v4"
|
||||
)
|
||||
|
||||
var All map[types.MigrationVersion]types.Migration = make(map[types.MigrationVersion]types.Migration)
|
||||
|
||||
func registerMigration(m types.Migration) {
|
||||
All[m.Version()] = m
|
||||
}
|
||||
|
||||
func debugQuery(ctx context.Context, tx pgx.Tx, sql string) {
|
||||
rows, err := tx.Query(ctx, sql)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for rows.Next() {
|
||||
vals, _ := rows.Values()
|
||||
fmt.Println(vals)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,7 +3,6 @@ package models
|
|||
type LibraryResource struct {
|
||||
ID int `db:"id"`
|
||||
|
||||
CategoryID int `db:"category_id"`
|
||||
ProjectID *int `db:"project_id"`
|
||||
|
||||
Name string `db:"name"`
|
||||
|
|
|
@ -10,13 +10,12 @@ type Post struct {
|
|||
|
||||
// TODO: Document each of these
|
||||
AuthorID *int `db:"author_id"`
|
||||
CategoryID int `db:"category_id"`
|
||||
ParentID *int `db:"parent_id"`
|
||||
ThreadID int `db:"thread_id"`
|
||||
CurrentID int `db:"current_id"`
|
||||
ProjectID int `db:"project_id"`
|
||||
|
||||
CategoryKind CategoryKind `db:"category_kind"`
|
||||
ThreadType ThreadType `db:"thread_type"`
|
||||
|
||||
PostDate time.Time `db:"postdate"`
|
||||
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
|
||||
|
||||
type ThreadType int
|
||||
|
||||
const (
|
||||
ThreadTypeProjectArticle ThreadType = iota + 1
|
||||
ThreadTypeForumPost
|
||||
_ // formerly occupied by static pages, RIP
|
||||
_ // formerly occupied by who the hell knows what, RIP
|
||||
_ // formerly occupied by the wiki, RIP
|
||||
_ // formerly occupied by library discussions, RIP
|
||||
ThreadTypePersonalArticle
|
||||
)
|
||||
|
||||
type Thread struct {
|
||||
ID int `db:"id"`
|
||||
|
||||
CategoryID int `db:"category_id"`
|
||||
Type ThreadType `db:"type"`
|
||||
ProjectID int `db:"project_id"`
|
||||
SubforumID *int `db:"subforum_id"`
|
||||
PersonalArticleUserID *int `db:"personal_article_user_id"`
|
||||
|
||||
Title string `db:"title"`
|
||||
Sticky bool `db:"sticky"`
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// +build js
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
|
|
@ -157,13 +157,6 @@
|
|||
}
|
||||
|
||||
.post {
|
||||
.wiki &,
|
||||
{
|
||||
padding: 0;
|
||||
margin: auto;
|
||||
max-width: 70em;
|
||||
}
|
||||
|
||||
.contents {
|
||||
h1, h2 {
|
||||
margin: 20px 0px;
|
||||
|
@ -332,79 +325,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.wiki {
|
||||
.post p {
|
||||
margin: 10px 0px;
|
||||
}
|
||||
|
||||
.toc {
|
||||
@include usevar(border-color, 'wiki-border-color');
|
||||
|
||||
border-left-width: 1px;
|
||||
|
||||
.toc-number {
|
||||
@include usevar(color, 'wiki-toc-number-color');
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
margin-left: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-left: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.aside {
|
||||
@include usevar(border-color, 'wiki-border-color');
|
||||
|
||||
border-left-width: 1px;
|
||||
margin-left:20px;
|
||||
|
||||
&::before {
|
||||
margin-left:-20px;
|
||||
margin-right:15px;
|
||||
display:inline-block;
|
||||
width:10px;
|
||||
content:"\21b4 ";
|
||||
}
|
||||
|
||||
.aside-heading {
|
||||
padding:2px;
|
||||
margin:1px;
|
||||
border-radius:3px;
|
||||
border-width:0px;
|
||||
cursor:pointer;
|
||||
display:inline;
|
||||
background-color:transparent;
|
||||
}
|
||||
|
||||
> .aside-body {
|
||||
overflow:hidden;
|
||||
padding-left:10px;
|
||||
}
|
||||
|
||||
&.folded {
|
||||
&::before {
|
||||
content:"\2192 ";
|
||||
}
|
||||
|
||||
> .aside-body {
|
||||
max-height: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.featured-post .meta .avatar-icon {
|
||||
left:-60px;
|
||||
bottom:-5px;
|
||||
}
|
||||
|
||||
.blog .body blockquote,
|
||||
.wiki .body blockquote {
|
||||
.blog .body blockquote {
|
||||
padding-top:1px;
|
||||
padding-bottom:1px;
|
||||
}
|
||||
|
|
|
@ -5,9 +5,6 @@
|
|||
&.no-blogs .blogs {
|
||||
display: none;
|
||||
}
|
||||
&.no-wiki .wiki {
|
||||
display: none;
|
||||
}
|
||||
&.no-library .library {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -112,9 +112,6 @@ $vars: (
|
|||
irc-users-popout-border-color-left: #444,
|
||||
irc-users-popout-border-color-right: #333,
|
||||
|
||||
wiki-border-color: #444,
|
||||
wiki-toc-number-color: #bbb,
|
||||
|
||||
code-line-number-color: #444,
|
||||
|
||||
library-star-btn-background: #252525,
|
||||
|
|
|
@ -112,9 +112,6 @@ $vars: (
|
|||
irc-users-popout-border-color-left: #bbb,
|
||||
irc-users-popout-border-color-right: #ccc,
|
||||
|
||||
wiki-border-color: #aaa,
|
||||
wiki-toc-number-color: #333,
|
||||
|
||||
code-line-number-color: #777,
|
||||
|
||||
library-star-btn-background: #fff,
|
||||
|
|
|
@ -99,7 +99,6 @@ func ProjectToTemplate(p *models.Project, theme string) Project {
|
|||
|
||||
HasBlog: true, // TODO: Check flag sets or whatever
|
||||
HasForum: true,
|
||||
HasWiki: true,
|
||||
HasLibrary: true,
|
||||
|
||||
DateApproved: p.DateApproved,
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
{{ define "content" }}
|
||||
<div class="content-block">
|
||||
{{ range .Subcategories }}
|
||||
{{ range .Subforums }}
|
||||
<div class="pv3">
|
||||
<h2 class="ma0 ph3 pb2">
|
||||
<a href="{{ .Url }}">
|
||||
|
@ -21,18 +21,18 @@
|
|||
</div>
|
||||
{{ end }}
|
||||
<div class="optionbar">
|
||||
{{ template "forum_category_options" . }}
|
||||
{{ template "subforum_options" . }}
|
||||
</div>
|
||||
{{ range .Threads }}
|
||||
{{ template "thread_list_item.html" . }}
|
||||
{{ end }}
|
||||
<div class="optionbar bottom">
|
||||
{{ template "forum_category_options" . }}
|
||||
{{ template "subforum_options" . }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ define "forum_category_options" }}
|
||||
{{ define "subforum_options" }}
|
||||
<div class="options">
|
||||
{{ if .User }}
|
||||
<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="optionbar">
|
||||
<div class="options">
|
||||
<a class="button" href="{{ .CategoryUrl }}">← Back to index</a>
|
||||
<a class="button" href="{{ .SubforumUrl }}">← Back to index</a>
|
||||
</div>
|
||||
<div class="options">
|
||||
{{ template "pagination.html" .Pagination }}
|
||||
|
@ -118,7 +118,7 @@
|
|||
{{ end }}
|
||||
<div class="optionbar bottom">
|
||||
<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 }}
|
||||
<span>Thread is locked.</span>
|
||||
{{ else if .User }}
|
||||
|
|
|
@ -51,9 +51,6 @@
|
|||
{{ if .Project.HasForum }}
|
||||
<a href="{{ .Header.ForumsUrl }}" class="forums">Forums</a>
|
||||
{{ end }}
|
||||
{{ if .Project.HasWiki }}
|
||||
<a href="{{ .Header.WikiUrl }}" class="wiki">Wiki</a>
|
||||
{{ end }}
|
||||
{{ if .Project.HasLibrary }}
|
||||
<a href="{{ .Header.LibraryUrl }}" class="library">Library</a>
|
||||
{{ end }}
|
||||
|
|
|
@ -172,166 +172,6 @@
|
|||
</div>
|
||||
{{ 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" }}
|
||||
{{/* Call this template with a LandingPageFeaturedPost. */}}
|
||||
<div class="flex items-start ph3 pv2 {{ if .Unread }}unread{{ else }}read{{ end }}">
|
||||
|
|
|
@ -25,12 +25,6 @@
|
|||
{{ 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>
|
||||
{{ 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 }}
|
||||
<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 }}
|
||||
|
@ -61,7 +55,7 @@
|
|||
|
||||
<div class="pair flex flex-wrap">
|
||||
<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>
|
||||
|
||||
{{ if .ProfileUser.Email }}
|
||||
|
|
|
@ -218,11 +218,7 @@ var HMNTemplateFuncs = template.FuncMap{
|
|||
if item.Type == TimelineTypeForumThread ||
|
||||
item.Type == TimelineTypeForumReply ||
|
||||
item.Type == TimelineTypeBlogPost ||
|
||||
item.Type == TimelineTypeBlogComment ||
|
||||
item.Type == TimelineTypeWikiCreate ||
|
||||
item.Type == TimelineTypeWikiEdit ||
|
||||
item.Type == TimelineTypeWikiTalk ||
|
||||
item.Type == TimelineTypeLibraryComment {
|
||||
item.Type == TimelineTypeBlogComment {
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -39,7 +39,6 @@ type Header struct {
|
|||
ProjectIndexUrl string
|
||||
BlogUrl string
|
||||
ForumsUrl string
|
||||
WikiUrl string
|
||||
LibraryUrl string
|
||||
ManifestoUrl string
|
||||
EpisodeGuideUrl string
|
||||
|
@ -109,7 +108,6 @@ type Project struct {
|
|||
|
||||
HasBlog bool
|
||||
HasForum bool
|
||||
HasWiki bool
|
||||
HasLibrary bool
|
||||
|
||||
UUID string
|
||||
|
@ -202,10 +200,6 @@ const (
|
|||
PostTypeBlogComment
|
||||
PostTypeForumThread
|
||||
PostTypeForumReply
|
||||
PostTypeWikiCreate
|
||||
PostTypeWikiTalk
|
||||
PostTypeWikiEdit
|
||||
PostTypeLibraryComment
|
||||
)
|
||||
|
||||
// Data from post_list_item.html
|
||||
|
@ -253,12 +247,6 @@ const (
|
|||
TimelineTypeBlogPost
|
||||
TimelineTypeBlogComment
|
||||
|
||||
TimelineTypeWikiCreate
|
||||
TimelineTypeWikiEdit
|
||||
TimelineTypeWikiTalk
|
||||
|
||||
TimelineTypeLibraryComment
|
||||
|
||||
TimelineTypeSnippetImage
|
||||
TimelineTypeSnippetVideo
|
||||
TimelineTypeSnippetAudio
|
||||
|
|
|
@ -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
|
||||
handmade_post AS post
|
||||
WHERE
|
||||
post.category_kind = ANY ($1)
|
||||
post.thread_type = ANY ($1)
|
||||
AND deleted = FALSE
|
||||
AND post.thread_id IS NOT NULL
|
||||
`,
|
||||
[]models.CategoryKind{models.CatKindForum, models.CatKindBlog, models.CatKindWiki, models.CatKindLibraryResource},
|
||||
[]models.ThreadType{models.ThreadTypeForumPost, models.ThreadTypeProjectArticle},
|
||||
)
|
||||
c.Perf.EndBlock()
|
||||
if err != nil {
|
||||
|
@ -80,9 +80,9 @@ func Feed(c *RequestContext) ResponseData {
|
|||
currentUserId = &c.CurrentUser.ID
|
||||
}
|
||||
|
||||
c.Perf.StartBlock("SQL", "Fetch category tree")
|
||||
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
|
||||
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
||||
c.Perf.StartBlock("SQL", "Fetch subforum tree")
|
||||
subforumTree := models.GetFullSubforumTree(c.Context(), c.Conn)
|
||||
lineageBuilder := models.MakeSubforumLineageBuilder(subforumTree)
|
||||
c.Perf.EndBlock()
|
||||
|
||||
posts, err := fetchAllPosts(c, lineageBuilder, currentUserId, howManyPostsToSkip, postsPerPage)
|
||||
|
@ -98,7 +98,7 @@ func Feed(c *RequestContext) ResponseData {
|
|||
BaseData: baseData,
|
||||
|
||||
AtomFeedUrl: hmnurl.BuildAtomFeed(),
|
||||
MarkAllReadUrl: hmnurl.BuildForumCategoryMarkRead(0),
|
||||
MarkAllReadUrl: hmnurl.BuildForumMarkRead(0),
|
||||
Posts: posts,
|
||||
Pagination: pagination,
|
||||
}, c.Perf)
|
||||
|
@ -158,9 +158,9 @@ func AtomFeed(c *RequestContext) ResponseData {
|
|||
feedData.AtomFeedUrl = hmnurl.BuildAtomFeed()
|
||||
feedData.FeedUrl = hmnurl.BuildFeed()
|
||||
|
||||
c.Perf.StartBlock("SQL", "Fetch category tree")
|
||||
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
|
||||
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
||||
c.Perf.StartBlock("SQL", "Fetch subforum tree")
|
||||
subforumTree := models.GetFullSubforumTree(c.Context(), c.Conn)
|
||||
lineageBuilder := models.MakeSubforumLineageBuilder(subforumTree)
|
||||
c.Perf.EndBlock()
|
||||
|
||||
posts, err := fetchAllPosts(c, lineageBuilder, nil, 0, itemsPerFeed)
|
||||
|
@ -303,18 +303,16 @@ func AtomFeed(c *RequestContext) ResponseData {
|
|||
return res
|
||||
}
|
||||
|
||||
func fetchAllPosts(c *RequestContext, lineageBuilder *models.CategoryLineageBuilder, currentUserID *int, offset int, limit int) ([]templates.PostListItem, error) {
|
||||
func fetchAllPosts(c *RequestContext, lineageBuilder *models.SubforumLineageBuilder, currentUserID *int, offset int, limit int) ([]templates.PostListItem, error) {
|
||||
c.Perf.StartBlock("SQL", "Fetch posts")
|
||||
type feedPostQuery struct {
|
||||
Post models.Post `db:"post"`
|
||||
PostVersion models.PostVersion `db:"version"`
|
||||
Thread models.Thread `db:"thread"`
|
||||
Cat models.Category `db:"cat"`
|
||||
Proj models.Project `db:"proj"`
|
||||
LibraryResource *models.LibraryResource `db:"lib_resource"`
|
||||
User models.User `db:"auth_user"`
|
||||
ThreadLastReadTime *time.Time `db:"tlri.lastread"`
|
||||
CatLastReadTime *time.Time `db:"clri.lastread"`
|
||||
SubforumLastReadTime *time.Time `db:"slri.lastread"`
|
||||
}
|
||||
posts, err := db.Query(c.Context(), c.Conn, feedPostQuery{},
|
||||
`
|
||||
|
@ -323,27 +321,25 @@ func fetchAllPosts(c *RequestContext, lineageBuilder *models.CategoryLineageBuil
|
|||
handmade_post AS post
|
||||
JOIN handmade_postversion AS version ON version.id = post.current_id
|
||||
JOIN handmade_thread AS thread ON thread.id = post.thread_id
|
||||
JOIN handmade_category AS cat ON cat.id = post.category_id
|
||||
JOIN handmade_project AS proj ON proj.id = post.project_id
|
||||
LEFT JOIN handmade_threadlastreadinfo AS tlri ON (
|
||||
tlri.thread_id = post.thread_id
|
||||
AND tlri.user_id = $1
|
||||
)
|
||||
LEFT JOIN handmade_categorylastreadinfo AS clri ON (
|
||||
clri.category_id = post.category_id
|
||||
AND clri.user_id = $1
|
||||
LEFT JOIN handmade_subforumlastreadinfo AS slri ON (
|
||||
slri.subforum_id = thread.subforum_id
|
||||
AND slri.user_id = $1
|
||||
)
|
||||
LEFT JOIN auth_user ON post.author_id = auth_user.id
|
||||
LEFT JOIN handmade_libraryresource as lib_resource ON lib_resource.category_id = post.category_id
|
||||
WHERE
|
||||
post.category_kind = ANY ($2)
|
||||
thread.type = ANY ($2)
|
||||
AND post.deleted = FALSE
|
||||
AND post.thread_id IS NOT NULL
|
||||
ORDER BY postdate DESC
|
||||
LIMIT $3 OFFSET $4
|
||||
`,
|
||||
currentUserID,
|
||||
[]models.CategoryKind{models.CatKindForum, models.CatKindBlog, models.CatKindWiki, models.CatKindLibraryResource},
|
||||
[]models.ThreadType{models.ThreadTypeForumPost, models.ThreadTypeProjectArticle},
|
||||
limit,
|
||||
offset,
|
||||
)
|
||||
|
@ -360,7 +356,7 @@ func fetchAllPosts(c *RequestContext, lineageBuilder *models.CategoryLineageBuil
|
|||
hasRead := false
|
||||
if postResult.ThreadLastReadTime != nil && postResult.ThreadLastReadTime.After(postResult.Post.PostDate) {
|
||||
hasRead = true
|
||||
} else if postResult.CatLastReadTime != nil && postResult.CatLastReadTime.After(postResult.Post.PostDate) {
|
||||
} else if postResult.SubforumLastReadTime != nil && postResult.SubforumLastReadTime.After(postResult.Post.PostDate) {
|
||||
hasRead = true
|
||||
}
|
||||
|
||||
|
@ -370,7 +366,6 @@ func fetchAllPosts(c *RequestContext, lineageBuilder *models.CategoryLineageBuil
|
|||
&postResult.Thread,
|
||||
&postResult.Post,
|
||||
&postResult.User,
|
||||
postResult.LibraryResource,
|
||||
!hasRead,
|
||||
true,
|
||||
c.Theme,
|
||||
|
|
|
@ -21,17 +21,17 @@ import (
|
|||
"github.com/jackc/pgx/v4"
|
||||
)
|
||||
|
||||
type forumCategoryData struct {
|
||||
type forumData struct {
|
||||
templates.BaseData
|
||||
|
||||
NewThreadUrl string
|
||||
MarkReadUrl string
|
||||
Threads []templates.ThreadListItem
|
||||
Pagination templates.Pagination
|
||||
Subcategories []forumSubcategoryData
|
||||
Subforums []forumSubforumData
|
||||
}
|
||||
|
||||
type forumSubcategoryData struct {
|
||||
type forumSubforumData struct {
|
||||
Name string
|
||||
Url string
|
||||
Threads []templates.ThreadListItem
|
||||
|
@ -50,7 +50,7 @@ type editorData struct {
|
|||
PostReplyingTo *templates.Post
|
||||
}
|
||||
|
||||
func ForumCategory(c *RequestContext) ResponseData {
|
||||
func Forum(c *RequestContext) ResponseData {
|
||||
const threadsPerPage = 25
|
||||
|
||||
cd, ok := getCommonForumData(c)
|
||||
|
@ -58,7 +58,7 @@ func ForumCategory(c *RequestContext) ResponseData {
|
|||
return FourOhFour(c)
|
||||
}
|
||||
|
||||
currentSubforumSlugs := cd.LineageBuilder.GetSubforumLineageSlugs(cd.CatID)
|
||||
currentSubforumSlugs := cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID)
|
||||
|
||||
c.Perf.StartBlock("SQL", "Fetch count of page threads")
|
||||
numThreads, err := db.QueryInt(c.Context(), c.Conn,
|
||||
|
@ -66,10 +66,10 @@ func ForumCategory(c *RequestContext) ResponseData {
|
|||
SELECT COUNT(*)
|
||||
FROM handmade_thread AS thread
|
||||
WHERE
|
||||
thread.category_id = $1
|
||||
thread.subforum_id = $1
|
||||
AND NOT thread.deleted
|
||||
`,
|
||||
cd.CatID,
|
||||
cd.SubforumID,
|
||||
)
|
||||
if err != nil {
|
||||
panic(oops.New(err, "failed to get count of threads"))
|
||||
|
@ -84,11 +84,11 @@ func ForumCategory(c *RequestContext) ResponseData {
|
|||
if pageParsed, err := strconv.Atoi(pageString); err == nil {
|
||||
page = pageParsed
|
||||
} else {
|
||||
return c.Redirect(hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs, 1), http.StatusSeeOther)
|
||||
return c.Redirect(hmnurl.BuildForum(c.CurrentProject.Slug, currentSubforumSlugs, 1), http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
if page < 1 || numPages < page {
|
||||
return c.Redirect(hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs, utils.IntClamp(1, page, numPages)), http.StatusSeeOther)
|
||||
return c.Redirect(hmnurl.BuildForum(c.CurrentProject.Slug, currentSubforumSlugs, utils.IntClamp(1, page, numPages)), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
howManyThreadsToSkip := (page - 1) * threadsPerPage
|
||||
|
@ -106,7 +106,7 @@ func ForumCategory(c *RequestContext) ResponseData {
|
|||
FirstUser *models.User `db:"firstuser"`
|
||||
LastUser *models.User `db:"lastuser"`
|
||||
ThreadLastReadTime *time.Time `db:"tlri.lastread"`
|
||||
CatLastReadTime *time.Time `db:"clri.lastread"`
|
||||
ForumLastReadTime *time.Time `db:"slri.lastread"`
|
||||
}
|
||||
itMainThreads, err := db.Query(c.Context(), c.Conn, threadQueryResult{},
|
||||
`
|
||||
|
@ -121,17 +121,17 @@ func ForumCategory(c *RequestContext) ResponseData {
|
|||
tlri.thread_id = thread.id
|
||||
AND tlri.user_id = $2
|
||||
)
|
||||
LEFT JOIN handmade_categorylastreadinfo AS clri ON (
|
||||
clri.category_id = $1
|
||||
AND clri.user_id = $2
|
||||
LEFT JOIN handmade_subforumlastreadinfo AS slri ON (
|
||||
slri.subforum_id = $1
|
||||
AND slri.user_id = $2
|
||||
)
|
||||
WHERE
|
||||
thread.category_id = $1
|
||||
thread.subforum_id = $1
|
||||
AND NOT thread.deleted
|
||||
ORDER BY lastpost.postdate DESC
|
||||
LIMIT $3 OFFSET $4
|
||||
`,
|
||||
cd.CatID,
|
||||
cd.SubforumID,
|
||||
currentUserId,
|
||||
threadsPerPage,
|
||||
howManyThreadsToSkip,
|
||||
|
@ -146,13 +146,13 @@ func ForumCategory(c *RequestContext) ResponseData {
|
|||
hasRead := false
|
||||
if row.ThreadLastReadTime != nil && row.ThreadLastReadTime.After(row.LastPost.PostDate) {
|
||||
hasRead = true
|
||||
} else if row.CatLastReadTime != nil && row.CatLastReadTime.After(row.LastPost.PostDate) {
|
||||
} else if row.ForumLastReadTime != nil && row.ForumLastReadTime.After(row.LastPost.PostDate) {
|
||||
hasRead = true
|
||||
}
|
||||
|
||||
return templates.ThreadListItem{
|
||||
Title: row.Thread.Title,
|
||||
Url: hmnurl.BuildForumThread(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(row.Thread.CategoryID), row.Thread.ID, row.Thread.Title, 1),
|
||||
Url: hmnurl.BuildForumThread(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(*row.Thread.SubforumID), row.Thread.ID, row.Thread.Title, 1),
|
||||
FirstUser: templates.UserToTemplate(row.FirstUser, c.Theme),
|
||||
FirstDate: row.FirstPost.PostDate,
|
||||
LastUser: templates.UserToTemplate(row.LastUser, c.Theme),
|
||||
|
@ -169,32 +169,32 @@ func ForumCategory(c *RequestContext) ResponseData {
|
|||
}
|
||||
|
||||
// ---------------------
|
||||
// Subcategory things
|
||||
// Subforum things
|
||||
// ---------------------
|
||||
|
||||
var subcats []forumSubcategoryData
|
||||
var subforums []forumSubforumData
|
||||
if page == 1 {
|
||||
subcatNodes := cd.CategoryTree[cd.CatID].Children
|
||||
subforumNodes := cd.SubforumTree[cd.SubforumID].Children
|
||||
|
||||
for _, catNode := range subcatNodes {
|
||||
c.Perf.StartBlock("SQL", "Fetch count of subcategory threads")
|
||||
// TODO(asaf): [PERF] [MINOR] Consider replacing querying count per subcat with a single query for all cats with GROUP BY.
|
||||
for _, sfNode := range subforumNodes {
|
||||
c.Perf.StartBlock("SQL", "Fetch count of subforum threads")
|
||||
// TODO(asaf): [PERF] [MINOR] Consider replacing querying count per subforum with a single query for all subforums with GROUP BY.
|
||||
numThreads, err := db.QueryInt(c.Context(), c.Conn,
|
||||
`
|
||||
SELECT COUNT(*)
|
||||
FROM handmade_thread AS thread
|
||||
WHERE
|
||||
thread.category_id = $1
|
||||
thread.subforum_id = $1
|
||||
AND NOT thread.deleted
|
||||
`,
|
||||
catNode.ID,
|
||||
sfNode.ID,
|
||||
)
|
||||
if err != nil {
|
||||
panic(oops.New(err, "failed to get count of threads"))
|
||||
}
|
||||
c.Perf.EndBlock()
|
||||
|
||||
c.Perf.StartBlock("SQL", "Fetch subcategory threads")
|
||||
c.Perf.StartBlock("SQL", "Fetch subforum threads")
|
||||
// TODO(asaf): [PERF] [MINOR] Consider batching these.
|
||||
itThreads, err := db.Query(c.Context(), c.Conn, threadQueryResult{},
|
||||
`
|
||||
|
@ -209,17 +209,17 @@ func ForumCategory(c *RequestContext) ResponseData {
|
|||
tlri.thread_id = thread.id
|
||||
AND tlri.user_id = $2
|
||||
)
|
||||
LEFT JOIN handmade_categorylastreadinfo AS clri ON (
|
||||
clri.category_id = $1
|
||||
AND clri.user_id = $2
|
||||
LEFT JOIN handmade_subforumlastreadinfo AS slri ON (
|
||||
slri.subforum_id = $1
|
||||
AND slri.user_id = $2
|
||||
)
|
||||
WHERE
|
||||
thread.category_id = $1
|
||||
thread.subforum_id = $1
|
||||
AND NOT thread.deleted
|
||||
ORDER BY lastpost.postdate DESC
|
||||
LIMIT 3
|
||||
`,
|
||||
catNode.ID,
|
||||
sfNode.ID,
|
||||
currentUserId,
|
||||
)
|
||||
if err != nil {
|
||||
|
@ -234,9 +234,9 @@ func ForumCategory(c *RequestContext) ResponseData {
|
|||
threads = append(threads, makeThreadListItem(threadRow))
|
||||
}
|
||||
|
||||
subcats = append(subcats, forumSubcategoryData{
|
||||
Name: *catNode.Name,
|
||||
Url: hmnurl.BuildForumCategory(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(catNode.ID), 1),
|
||||
subforums = append(subforums, forumSubforumData{
|
||||
Name: sfNode.Name,
|
||||
Url: hmnurl.BuildForum(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(sfNode.ID), 1),
|
||||
Threads: threads,
|
||||
TotalThreads: numThreads,
|
||||
})
|
||||
|
@ -249,53 +249,53 @@ func ForumCategory(c *RequestContext) ResponseData {
|
|||
|
||||
baseData := getBaseData(c)
|
||||
baseData.Title = c.CurrentProject.Name + " Forums"
|
||||
baseData.Breadcrumbs = []templates.Breadcrumb{ // TODO(ben): This is wrong; it needs to account for subcategories.
|
||||
baseData.Breadcrumbs = []templates.Breadcrumb{ // TODO(ben): This is wrong; it needs to account for subforums.
|
||||
{
|
||||
Name: c.CurrentProject.Name,
|
||||
Url: hmnurl.BuildProjectHomepage(c.CurrentProject.Slug),
|
||||
},
|
||||
{
|
||||
Name: "Forums",
|
||||
Url: hmnurl.BuildForumCategory(c.CurrentProject.Slug, nil, 1),
|
||||
Url: hmnurl.BuildForum(c.CurrentProject.Slug, nil, 1),
|
||||
Current: true,
|
||||
},
|
||||
}
|
||||
|
||||
currentSubforums := cd.LineageBuilder.GetSubforumLineage(cd.CatID)
|
||||
currentSubforums := cd.LineageBuilder.GetSubforumLineage(cd.SubforumID)
|
||||
for i, subforum := range currentSubforums {
|
||||
baseData.Breadcrumbs = append(baseData.Breadcrumbs, templates.Breadcrumb{
|
||||
Name: *subforum.Name, // NOTE(asaf): All subforum categories must have names.
|
||||
Url: hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs[0:i+1], 1),
|
||||
Name: subforum.Name,
|
||||
Url: hmnurl.BuildForum(c.CurrentProject.Slug, currentSubforumSlugs[0:i+1], 1),
|
||||
})
|
||||
}
|
||||
|
||||
var res ResponseData
|
||||
res.MustWriteTemplate("forum_category.html", forumCategoryData{
|
||||
res.MustWriteTemplate("forum.html", forumData{
|
||||
BaseData: baseData,
|
||||
NewThreadUrl: hmnurl.BuildForumNewThread(c.CurrentProject.Slug, currentSubforumSlugs, false),
|
||||
MarkReadUrl: hmnurl.BuildForumCategoryMarkRead(cd.CatID),
|
||||
MarkReadUrl: hmnurl.BuildForumMarkRead(cd.SubforumID),
|
||||
Threads: threads,
|
||||
Pagination: templates.Pagination{
|
||||
Current: page,
|
||||
Total: numPages,
|
||||
|
||||
FirstUrl: hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs, 1),
|
||||
LastUrl: hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs, numPages),
|
||||
NextUrl: hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs, utils.IntClamp(1, page+1, numPages)),
|
||||
PreviousUrl: hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs, utils.IntClamp(1, page-1, numPages)),
|
||||
FirstUrl: hmnurl.BuildForum(c.CurrentProject.Slug, currentSubforumSlugs, 1),
|
||||
LastUrl: hmnurl.BuildForum(c.CurrentProject.Slug, currentSubforumSlugs, numPages),
|
||||
NextUrl: hmnurl.BuildForum(c.CurrentProject.Slug, currentSubforumSlugs, utils.IntClamp(1, page+1, numPages)),
|
||||
PreviousUrl: hmnurl.BuildForum(c.CurrentProject.Slug, currentSubforumSlugs, utils.IntClamp(1, page-1, numPages)),
|
||||
},
|
||||
Subcategories: subcats,
|
||||
Subforums: subforums,
|
||||
}, c.Perf)
|
||||
return res
|
||||
}
|
||||
|
||||
func ForumCategoryMarkRead(c *RequestContext) ResponseData {
|
||||
c.Perf.StartBlock("SQL", "Fetch category tree")
|
||||
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
|
||||
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
||||
func ForumMarkRead(c *RequestContext) ResponseData {
|
||||
c.Perf.StartBlock("SQL", "Fetch subforum tree")
|
||||
subforumTree := models.GetFullSubforumTree(c.Context(), c.Conn)
|
||||
lineageBuilder := models.MakeSubforumLineageBuilder(subforumTree)
|
||||
c.Perf.EndBlock()
|
||||
|
||||
catId, err := strconv.Atoi(c.PathParams["catid"])
|
||||
sfId, err := strconv.Atoi(c.PathParams["sfid"])
|
||||
if err != nil {
|
||||
return FourOhFour(c)
|
||||
}
|
||||
|
@ -307,28 +307,28 @@ func ForumCategoryMarkRead(c *RequestContext) ResponseData {
|
|||
defer tx.Rollback(c.Context())
|
||||
|
||||
// TODO(ben): Rework this logic when we rework blogs, threads, etc.
|
||||
catIds := []int{catId}
|
||||
if catId == 0 {
|
||||
sfIds := []int{sfId}
|
||||
if sfId == 0 {
|
||||
// Select all categories
|
||||
type catIdResult struct {
|
||||
CatID int `db:"id"`
|
||||
type sfIdResult struct {
|
||||
SubforumID int `db:"id"`
|
||||
}
|
||||
cats, err := db.Query(c.Context(), tx, catIdResult{},
|
||||
cats, err := db.Query(c.Context(), tx, sfIdResult{},
|
||||
`
|
||||
SELECT $columns
|
||||
FROM handmade_category
|
||||
FROM handmade_subforum
|
||||
WHERE kind = ANY ($1)
|
||||
`,
|
||||
[]models.CategoryKind{models.CatKindBlog, models.CatKindForum, models.CatKindWiki, models.CatKindLibraryResource},
|
||||
[]models.ThreadType{models.ThreadTypeProjectArticle, models.ThreadTypeForumPost},
|
||||
)
|
||||
if err != nil {
|
||||
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch category IDs for CLRI"))
|
||||
}
|
||||
|
||||
catIdResults := cats.ToSlice()
|
||||
catIds = make([]int, len(catIdResults))
|
||||
sfIds = make([]int, len(catIdResults))
|
||||
for i, res := range catIdResults {
|
||||
catIds[i] = res.(*catIdResult).CatID
|
||||
sfIds[i] = res.(*sfIdResult).SubforumID
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -342,7 +342,7 @@ func ForumCategoryMarkRead(c *RequestContext) ResponseData {
|
|||
ON CONFLICT (category_id, user_id) DO UPDATE
|
||||
SET lastread = EXCLUDED.lastread
|
||||
`,
|
||||
catIds,
|
||||
sfIds,
|
||||
c.CurrentUser.ID,
|
||||
time.Now(),
|
||||
)
|
||||
|
@ -364,7 +364,7 @@ func ForumCategoryMarkRead(c *RequestContext) ResponseData {
|
|||
category_id = ANY ($1)
|
||||
)
|
||||
`,
|
||||
catIds,
|
||||
sfIds,
|
||||
c.CurrentUser.ID,
|
||||
)
|
||||
c.Perf.EndBlock()
|
||||
|
@ -378,10 +378,10 @@ func ForumCategoryMarkRead(c *RequestContext) ResponseData {
|
|||
}
|
||||
|
||||
var redirUrl string
|
||||
if catId == 0 {
|
||||
if sfId == 0 {
|
||||
redirUrl = hmnurl.BuildFeed()
|
||||
} else {
|
||||
redirUrl = hmnurl.BuildForumCategory(c.CurrentProject.Slug, lineageBuilder.GetSubforumLineageSlugs(catId), 1)
|
||||
redirUrl = hmnurl.BuildForum(c.CurrentProject.Slug, lineageBuilder.GetSubforumLineageSlugs(sfId), 1)
|
||||
}
|
||||
return c.Redirect(redirUrl, http.StatusSeeOther)
|
||||
}
|
||||
|
@ -392,7 +392,7 @@ type forumThreadData struct {
|
|||
Thread templates.Thread
|
||||
Posts []templates.Post
|
||||
|
||||
CategoryUrl string
|
||||
SubforumUrl string
|
||||
ReplyUrl string
|
||||
Pagination templates.Pagination
|
||||
}
|
||||
|
@ -405,7 +405,7 @@ func ForumThread(c *RequestContext) ResponseData {
|
|||
return FourOhFour(c)
|
||||
}
|
||||
|
||||
currentSubforumSlugs := cd.LineageBuilder.GetSubforumLineageSlugs(cd.CatID)
|
||||
currentSubforumSlugs := cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID)
|
||||
|
||||
thread := cd.FetchThread(c.Context(), c.Conn)
|
||||
|
||||
|
@ -519,7 +519,7 @@ func ForumThread(c *RequestContext) ResponseData {
|
|||
BaseData: baseData,
|
||||
Thread: templates.ThreadToTemplate(thread),
|
||||
Posts: posts,
|
||||
CategoryUrl: hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs, 1),
|
||||
SubforumUrl: hmnurl.BuildForum(c.CurrentProject.Slug, currentSubforumSlugs, 1),
|
||||
ReplyUrl: hmnurl.BuildForumPostReply(c.CurrentProject.Slug, currentSubforumSlugs, thread.ID, *thread.FirstID),
|
||||
Pagination: pagination,
|
||||
}, c.Perf)
|
||||
|
@ -542,12 +542,10 @@ func ForumPostRedirect(c *RequestContext) ResponseData {
|
|||
FROM
|
||||
handmade_post AS post
|
||||
WHERE
|
||||
post.category_id = $1
|
||||
AND post.thread_id = $2
|
||||
post.thread_id = $1
|
||||
AND NOT post.deleted
|
||||
ORDER BY postdate
|
||||
`,
|
||||
cd.CatID,
|
||||
cd.ThreadID,
|
||||
)
|
||||
if err != nil {
|
||||
|
@ -588,7 +586,7 @@ func ForumPostRedirect(c *RequestContext) ResponseData {
|
|||
|
||||
return c.Redirect(hmnurl.BuildForumThreadWithPostHash(
|
||||
c.CurrentProject.Slug,
|
||||
cd.LineageBuilder.GetSubforumLineageSlugs(cd.CatID),
|
||||
cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID),
|
||||
cd.ThreadID,
|
||||
threadTitle,
|
||||
page,
|
||||
|
@ -610,7 +608,7 @@ func ForumNewThread(c *RequestContext) ResponseData {
|
|||
var res ResponseData
|
||||
res.MustWriteTemplate("editor.html", editorData{
|
||||
BaseData: baseData,
|
||||
SubmitUrl: hmnurl.BuildForumNewThread(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.CatID), true),
|
||||
SubmitUrl: hmnurl.BuildForumNewThread(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID), true),
|
||||
SubmitLabel: "Post New Thread",
|
||||
}, c.Perf)
|
||||
return res
|
||||
|
@ -643,13 +641,15 @@ func ForumNewThreadSubmit(c *RequestContext) ResponseData {
|
|||
var threadId int
|
||||
err = tx.QueryRow(c.Context(),
|
||||
`
|
||||
INSERT INTO handmade_thread (title, sticky, category_id, first_id, last_id)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
INSERT INTO handmade_thread (title, sticky, type, project_id, subforum_id, first_id, last_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id
|
||||
`,
|
||||
title,
|
||||
sticky,
|
||||
cd.CatID,
|
||||
models.ThreadTypeForumPost,
|
||||
c.CurrentProject.ID,
|
||||
cd.SubforumID,
|
||||
-1,
|
||||
-1,
|
||||
).Scan(&threadId)
|
||||
|
@ -657,7 +657,7 @@ func ForumNewThreadSubmit(c *RequestContext) ResponseData {
|
|||
panic(oops.New(err, "failed to create thread"))
|
||||
}
|
||||
|
||||
postId, _ := createNewForumPostAndVersion(c.Context(), tx, cd.CatID, threadId, c.CurrentUser.ID, c.CurrentProject.ID, unparsed, c.Req.Host, nil)
|
||||
postId, _ := createNewForumPostAndVersion(c.Context(), tx, threadId, c.CurrentUser.ID, c.CurrentProject.ID, unparsed, c.Req.Host, nil)
|
||||
|
||||
// Update thread with post id
|
||||
_, err = tx.Exec(c.Context(),
|
||||
|
@ -680,7 +680,7 @@ func ForumNewThreadSubmit(c *RequestContext) ResponseData {
|
|||
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to create new forum thread"))
|
||||
}
|
||||
|
||||
newThreadUrl := hmnurl.BuildForumThread(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.CatID), threadId, title, 1)
|
||||
newThreadUrl := hmnurl.BuildForumThread(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID), threadId, title, 1)
|
||||
return c.Redirect(newThreadUrl, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
|
@ -693,7 +693,7 @@ func ForumPostReply(c *RequestContext) ResponseData {
|
|||
postData := cd.FetchPostAndStuff(c.Context(), c.Conn)
|
||||
|
||||
baseData := getBaseData(c)
|
||||
baseData.Title = fmt.Sprintf("Replying to \"%s\" | %s", postData.Thread.Title, *cd.CategoryTree[cd.CatID].Name)
|
||||
baseData.Title = fmt.Sprintf("Replying to \"%s\" | %s", postData.Thread.Title, cd.SubforumTree[cd.SubforumID].Name)
|
||||
baseData.MathjaxEnabled = true
|
||||
// TODO(ben): Set breadcrumbs
|
||||
|
||||
|
@ -703,7 +703,7 @@ func ForumPostReply(c *RequestContext) ResponseData {
|
|||
var res ResponseData
|
||||
res.MustWriteTemplate("editor.html", editorData{
|
||||
BaseData: baseData,
|
||||
SubmitUrl: hmnurl.BuildForumPostReply(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.CatID), cd.ThreadID, cd.PostID),
|
||||
SubmitUrl: hmnurl.BuildForumPostReply(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID), cd.ThreadID, cd.PostID),
|
||||
SubmitLabel: "Submit Reply",
|
||||
|
||||
Title: "Replying to post",
|
||||
|
@ -729,14 +729,14 @@ func ForumPostReplySubmit(c *RequestContext) ResponseData {
|
|||
|
||||
unparsed := c.Req.Form.Get("body")
|
||||
|
||||
newPostId, _ := createNewForumPostAndVersion(c.Context(), tx, cd.CatID, cd.ThreadID, c.CurrentUser.ID, c.CurrentProject.ID, unparsed, c.Req.Host, &cd.PostID)
|
||||
newPostId, _ := createNewForumPostAndVersion(c.Context(), tx, cd.ThreadID, c.CurrentUser.ID, c.CurrentProject.ID, unparsed, c.Req.Host, &cd.PostID)
|
||||
|
||||
err = tx.Commit(c.Context())
|
||||
if err != nil {
|
||||
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to reply to forum post"))
|
||||
}
|
||||
|
||||
newPostUrl := hmnurl.BuildForumPost(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.CatID), cd.ThreadID, newPostId)
|
||||
newPostUrl := hmnurl.BuildForumPost(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID), cd.ThreadID, newPostId)
|
||||
return c.Redirect(newPostUrl, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
|
@ -753,7 +753,7 @@ func ForumPostEdit(c *RequestContext) ResponseData {
|
|||
postData := cd.FetchPostAndStuff(c.Context(), c.Conn)
|
||||
|
||||
baseData := getBaseData(c)
|
||||
baseData.Title = fmt.Sprintf("Editing \"%s\" | %s", postData.Thread.Title, *cd.CategoryTree[cd.CatID].Name)
|
||||
baseData.Title = fmt.Sprintf("Editing \"%s\" | %s", postData.Thread.Title, cd.SubforumTree[cd.SubforumID].Name)
|
||||
baseData.MathjaxEnabled = true
|
||||
// TODO(ben): Set breadcrumbs
|
||||
|
||||
|
@ -763,7 +763,7 @@ func ForumPostEdit(c *RequestContext) ResponseData {
|
|||
var res ResponseData
|
||||
res.MustWriteTemplate("editor.html", editorData{
|
||||
BaseData: baseData,
|
||||
SubmitUrl: hmnurl.BuildForumPostEdit(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.CatID), cd.ThreadID, cd.PostID),
|
||||
SubmitUrl: hmnurl.BuildForumPostEdit(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID), cd.ThreadID, cd.PostID),
|
||||
Title: postData.Thread.Title,
|
||||
SubmitLabel: "Submit Edited Post",
|
||||
|
||||
|
@ -801,7 +801,7 @@ func ForumPostEditSubmit(c *RequestContext) ResponseData {
|
|||
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to edit forum post"))
|
||||
}
|
||||
|
||||
postUrl := hmnurl.BuildForumPost(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.CatID), cd.ThreadID, cd.PostID)
|
||||
postUrl := hmnurl.BuildForumPost(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID), cd.ThreadID, cd.PostID)
|
||||
return c.Redirect(postUrl, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
|
@ -818,7 +818,7 @@ func ForumPostDelete(c *RequestContext) ResponseData {
|
|||
postData := cd.FetchPostAndStuff(c.Context(), c.Conn)
|
||||
|
||||
baseData := getBaseData(c)
|
||||
baseData.Title = fmt.Sprintf("Deleting post in \"%s\" | %s", postData.Thread.Title, *cd.CategoryTree[cd.CatID].Name)
|
||||
baseData.Title = fmt.Sprintf("Deleting post in \"%s\" | %s", postData.Thread.Title, cd.SubforumTree[cd.SubforumID].Name)
|
||||
baseData.MathjaxEnabled = true
|
||||
// TODO(ben): Set breadcrumbs
|
||||
|
||||
|
@ -834,7 +834,7 @@ func ForumPostDelete(c *RequestContext) ResponseData {
|
|||
var res ResponseData
|
||||
res.MustWriteTemplate("forum_post_delete.html", forumPostDeleteData{
|
||||
BaseData: baseData,
|
||||
SubmitUrl: hmnurl.BuildForumPostDelete(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.CatID), cd.ThreadID, cd.PostID),
|
||||
SubmitUrl: hmnurl.BuildForumPostDelete(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID), cd.ThreadID, cd.PostID),
|
||||
Post: templatePost,
|
||||
}, c.Perf)
|
||||
return res
|
||||
|
@ -895,7 +895,7 @@ func ForumPostDeleteSubmit(c *RequestContext) ResponseData {
|
|||
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete thread and posts when deleting the first post"))
|
||||
}
|
||||
|
||||
forumUrl := hmnurl.BuildForumCategory(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.CatID), 1)
|
||||
forumUrl := hmnurl.BuildForum(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID), 1)
|
||||
return c.Redirect(forumUrl, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
|
@ -926,24 +926,23 @@ func ForumPostDeleteSubmit(c *RequestContext) ResponseData {
|
|||
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete post"))
|
||||
}
|
||||
|
||||
threadUrl := hmnurl.BuildForumThread(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.CatID), cd.ThreadID, "", 1) // TODO: Go to the last page of the thread? Or the post before the post we just deleted?
|
||||
threadUrl := hmnurl.BuildForumThread(c.CurrentProject.Slug, cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID), cd.ThreadID, "", 1) // TODO: Go to the last page of the thread? Or the post before the post we just deleted?
|
||||
return c.Redirect(threadUrl, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func createNewForumPostAndVersion(ctx context.Context, tx pgx.Tx, catId, threadId, userId, projectId int, unparsedContent string, ipString string, replyId *int) (postId, versionId int) {
|
||||
func createNewForumPostAndVersion(ctx context.Context, tx pgx.Tx, threadId, userId, projectId int, unparsedContent string, ipString string, replyId *int) (postId, versionId int) {
|
||||
// Create post
|
||||
err := tx.QueryRow(ctx,
|
||||
`
|
||||
INSERT INTO handmade_post (postdate, category_id, thread_id, current_id, author_id, category_kind, project_id, reply_id, preview)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
INSERT INTO handmade_post (postdate, thread_id, thread_type, current_id, author_id, project_id, reply_id, preview)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING id
|
||||
`,
|
||||
time.Now(),
|
||||
catId,
|
||||
threadId,
|
||||
models.ThreadTypeForumPost,
|
||||
-1,
|
||||
userId,
|
||||
models.CatKindForum,
|
||||
projectId,
|
||||
replyId,
|
||||
"", // empty preview, will be updated later
|
||||
|
@ -1064,16 +1063,16 @@ func fixThreadPostIds(ctx context.Context, tx pgx.Tx, threadId int) error {
|
|||
type commonForumData struct {
|
||||
c *RequestContext
|
||||
|
||||
CatID int
|
||||
SubforumID int
|
||||
ThreadID int
|
||||
PostID int
|
||||
|
||||
CategoryTree models.CategoryTree
|
||||
LineageBuilder *models.CategoryLineageBuilder
|
||||
SubforumTree models.SubforumTree
|
||||
LineageBuilder *models.SubforumLineageBuilder
|
||||
}
|
||||
|
||||
/*
|
||||
Gets data that is used on basically every forums-related route. Parses path params for category,
|
||||
Gets data that is used on basically every forums-related route. Parses path params for subforum,
|
||||
thread, and post ids and validates that all those resources do in fact exist.
|
||||
|
||||
Returns false if any data is invalid and you should return a 404.
|
||||
|
@ -1082,25 +1081,25 @@ func getCommonForumData(c *RequestContext) (commonForumData, bool) {
|
|||
c.Perf.StartBlock("FORUMS", "Fetch common forum data")
|
||||
defer c.Perf.EndBlock()
|
||||
|
||||
c.Perf.StartBlock("SQL", "Fetch category tree")
|
||||
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
|
||||
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
||||
c.Perf.StartBlock("SQL", "Fetch subforum tree")
|
||||
subforumTree := models.GetFullSubforumTree(c.Context(), c.Conn)
|
||||
lineageBuilder := models.MakeSubforumLineageBuilder(subforumTree)
|
||||
c.Perf.EndBlock()
|
||||
|
||||
res := commonForumData{
|
||||
c: c,
|
||||
CategoryTree: categoryTree,
|
||||
SubforumTree: subforumTree,
|
||||
LineageBuilder: lineageBuilder,
|
||||
}
|
||||
|
||||
if cats, hasCats := c.PathParams["cats"]; hasCats {
|
||||
catId, valid := validateSubforums(lineageBuilder, c.CurrentProject, cats)
|
||||
if subforums, hasSubforums := c.PathParams["subforums"]; hasSubforums {
|
||||
sfId, valid := validateSubforums(lineageBuilder, c.CurrentProject, subforums)
|
||||
if !valid {
|
||||
return commonForumData{}, false
|
||||
}
|
||||
res.CatID = catId
|
||||
res.SubforumID = sfId
|
||||
|
||||
// No need to validate cat here; it's handled by validateSubforums.
|
||||
// No need to validate that subforum exists here; it's handled by validateSubforums.
|
||||
}
|
||||
|
||||
if threadIdStr, hasThreadId := c.PathParams["threadid"]; hasThreadId {
|
||||
|
@ -1117,10 +1116,10 @@ func getCommonForumData(c *RequestContext) (commonForumData, bool) {
|
|||
FROM handmade_thread
|
||||
WHERE
|
||||
id = $1
|
||||
AND category_id = $2
|
||||
AND subforum_id = $2
|
||||
`,
|
||||
res.ThreadID,
|
||||
res.CatID,
|
||||
res.SubforumID,
|
||||
)
|
||||
c.Perf.EndBlock()
|
||||
if err != nil {
|
||||
|
@ -1146,11 +1145,9 @@ func getCommonForumData(c *RequestContext) (commonForumData, bool) {
|
|||
WHERE
|
||||
id = $1
|
||||
AND thread_id = $2
|
||||
AND category_id = $3
|
||||
`,
|
||||
res.PostID,
|
||||
res.ThreadID,
|
||||
res.CatID,
|
||||
)
|
||||
c.Perf.EndBlock()
|
||||
if err != nil {
|
||||
|
@ -1180,14 +1177,14 @@ func (cd *commonForumData) FetchThread(ctx context.Context, connOrTx db.ConnOrTx
|
|||
SELECT $columns
|
||||
FROM
|
||||
handmade_thread AS thread
|
||||
JOIN handmade_category AS cat ON cat.id = thread.category_id
|
||||
JOIN handmade_subforum AS sf ON sf.id = thread.subforum_id
|
||||
WHERE
|
||||
thread.id = $1
|
||||
AND NOT thread.deleted
|
||||
AND cat.id = $2
|
||||
AND sf.id = $2
|
||||
`,
|
||||
cd.ThreadID,
|
||||
cd.CatID, // NOTE(asaf): This verifies that the requested thread is under the requested subforum.
|
||||
cd.SubforumID, // NOTE(asaf): This verifies that the requested thread is under the requested subforum.
|
||||
)
|
||||
cd.c.Perf.EndBlock()
|
||||
if err != nil {
|
||||
|
@ -1225,12 +1222,10 @@ func (cd *commonForumData) FetchPostAndStuff(ctx context.Context, connOrTx db.Co
|
|||
LEFT JOIN auth_user AS author ON post.author_id = author.id
|
||||
LEFT JOIN auth_user AS editor ON ver.editor_id = editor.id
|
||||
WHERE
|
||||
post.category_id = $1
|
||||
AND post.thread_id = $2
|
||||
AND post.id = $3
|
||||
post.thread_id = $1
|
||||
AND post.id = $2
|
||||
AND NOT post.deleted
|
||||
`,
|
||||
cd.CatID,
|
||||
cd.ThreadID,
|
||||
cd.PostID,
|
||||
)
|
||||
|
@ -1274,27 +1269,27 @@ func (cd *commonForumData) UserCanEditPost(ctx context.Context, connOrTx db.Conn
|
|||
return result.AuthorID != nil && *result.AuthorID == user.ID
|
||||
}
|
||||
|
||||
func validateSubforums(lineageBuilder *models.CategoryLineageBuilder, project *models.Project, catPath string) (int, bool) {
|
||||
func validateSubforums(lineageBuilder *models.SubforumLineageBuilder, project *models.Project, sfPath string) (int, bool) {
|
||||
if project.ForumID == nil {
|
||||
return -1, false
|
||||
}
|
||||
|
||||
subforumCatId := *project.ForumID
|
||||
if len(catPath) == 0 {
|
||||
return subforumCatId, true
|
||||
subforumId := *project.ForumID
|
||||
if len(sfPath) == 0 {
|
||||
return subforumId, true
|
||||
}
|
||||
|
||||
catPath = strings.ToLower(catPath)
|
||||
sfPath = strings.ToLower(sfPath)
|
||||
valid := false
|
||||
catSlugs := strings.Split(catPath, "/")
|
||||
lastSlug := catSlugs[len(catSlugs)-1]
|
||||
sfSlugs := strings.Split(sfPath, "/")
|
||||
lastSlug := sfSlugs[len(sfSlugs)-1]
|
||||
if len(lastSlug) > 0 {
|
||||
lastSlugCatId := lineageBuilder.FindIdBySlug(project.ID, lastSlug)
|
||||
if lastSlugCatId != -1 {
|
||||
subforumSlugs := lineageBuilder.GetSubforumLineageSlugs(lastSlugCatId)
|
||||
lastSlugSfId := lineageBuilder.FindIdBySlug(project.ID, lastSlug)
|
||||
if lastSlugSfId != -1 {
|
||||
subforumSlugs := lineageBuilder.GetSubforumLineageSlugs(lastSlugSfId)
|
||||
allMatch := true
|
||||
for i, subforum := range subforumSlugs {
|
||||
if subforum != catSlugs[i] {
|
||||
if subforum != sfSlugs[i] {
|
||||
allMatch = false
|
||||
break
|
||||
}
|
||||
|
@ -1302,8 +1297,8 @@ func validateSubforums(lineageBuilder *models.CategoryLineageBuilder, project *m
|
|||
valid = allMatch
|
||||
}
|
||||
if valid {
|
||||
subforumCatId = lastSlugCatId
|
||||
subforumId = lastSlugSfId
|
||||
}
|
||||
}
|
||||
return subforumCatId, valid
|
||||
return subforumId, valid
|
||||
}
|
||||
|
|
|
@ -74,9 +74,9 @@ func Index(c *RequestContext) ResponseData {
|
|||
allProjects := iterProjects.ToSlice()
|
||||
c.Perf.EndBlock()
|
||||
|
||||
c.Perf.StartBlock("SQL", "Fetch category tree")
|
||||
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
|
||||
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
||||
c.Perf.StartBlock("SQL", "Fetch subforum tree")
|
||||
subforumTree := models.GetFullSubforumTree(c.Context(), c.Conn)
|
||||
lineageBuilder := models.MakeSubforumLineageBuilder(subforumTree)
|
||||
c.Perf.EndBlock()
|
||||
|
||||
var currentUserId *int
|
||||
|
@ -93,9 +93,8 @@ func Index(c *RequestContext) ResponseData {
|
|||
Post models.Post `db:"post"`
|
||||
Thread models.Thread `db:"thread"`
|
||||
User models.User `db:"auth_user"`
|
||||
LibraryResource *models.LibraryResource `db:"lib_resource"`
|
||||
ThreadLastReadTime *time.Time `db:"tlri.lastread"`
|
||||
CatLastReadTime *time.Time `db:"clri.lastread"`
|
||||
SubforumLastReadTime *time.Time `db:"slri.lastread"`
|
||||
}
|
||||
projectPostIter, err := db.Query(c.Context(), c.Conn, projectPostQuery{},
|
||||
`
|
||||
|
@ -107,22 +106,21 @@ func Index(c *RequestContext) ResponseData {
|
|||
tlri.thread_id = post.thread_id
|
||||
AND tlri.user_id = $1
|
||||
)
|
||||
LEFT JOIN handmade_categorylastreadinfo AS clri ON (
|
||||
clri.category_id = post.category_id
|
||||
AND clri.user_id = $1
|
||||
LEFT JOIN handmade_subforumlastreadinfo AS slri ON (
|
||||
slri.subforum_id = thread.subforum_id
|
||||
AND slri.user_id = $1
|
||||
)
|
||||
LEFT JOIN auth_user ON post.author_id = auth_user.id
|
||||
LEFT JOIN handmade_libraryresource as lib_resource ON lib_resource.category_id = post.category_id
|
||||
WHERE
|
||||
post.project_id = $2
|
||||
AND post.category_kind IN ($3, $4, $5, $6)
|
||||
AND post.thread_type = ANY ($3)
|
||||
AND post.deleted = FALSE
|
||||
ORDER BY postdate DESC
|
||||
LIMIT $7
|
||||
LIMIT $4
|
||||
`,
|
||||
currentUserId,
|
||||
proj.ID,
|
||||
models.CatKindBlog, models.CatKindForum, models.CatKindWiki, models.CatKindLibraryResource,
|
||||
[]models.ThreadType{models.ThreadTypeProjectArticle, models.ThreadTypeForumPost},
|
||||
maxPosts,
|
||||
)
|
||||
c.Perf.EndBlock()
|
||||
|
@ -134,7 +132,7 @@ func Index(c *RequestContext) ResponseData {
|
|||
|
||||
forumsUrl := ""
|
||||
if proj.ForumID != nil {
|
||||
forumsUrl = hmnurl.BuildForumCategory(proj.Slug, lineageBuilder.GetSubforumLineageSlugs(*proj.ForumID), 1)
|
||||
forumsUrl = hmnurl.BuildForum(proj.Slug, lineageBuilder.GetSubforumLineageSlugs(*proj.ForumID), 1)
|
||||
} else {
|
||||
c.Logger.Error().Int("ProjectID", proj.ID).Str("ProjectName", proj.Name).Msg("Project fetched by landing page but it doesn't have forums")
|
||||
}
|
||||
|
@ -150,12 +148,12 @@ func Index(c *RequestContext) ResponseData {
|
|||
hasRead := false
|
||||
if projectPost.ThreadLastReadTime != nil && projectPost.ThreadLastReadTime.After(projectPost.Post.PostDate) {
|
||||
hasRead = true
|
||||
} else if projectPost.CatLastReadTime != nil && projectPost.CatLastReadTime.After(projectPost.Post.PostDate) {
|
||||
} else if projectPost.SubforumLastReadTime != nil && projectPost.SubforumLastReadTime.After(projectPost.Post.PostDate) {
|
||||
hasRead = true
|
||||
}
|
||||
|
||||
featurable := (!proj.IsHMN() &&
|
||||
projectPost.Post.CategoryKind == models.CatKindBlog &&
|
||||
projectPost.Post.ThreadType == models.ThreadTypeProjectArticle &&
|
||||
projectPost.Post.ParentID == nil &&
|
||||
landingPageProject.FeaturedPost == nil)
|
||||
|
||||
|
@ -197,7 +195,6 @@ func Index(c *RequestContext) ResponseData {
|
|||
&projectPost.Thread,
|
||||
&projectPost.Post,
|
||||
&projectPost.User,
|
||||
projectPost.LibraryResource,
|
||||
!hasRead,
|
||||
false,
|
||||
c.Theme,
|
||||
|
@ -263,14 +260,14 @@ func Index(c *RequestContext) ResponseData {
|
|||
JOIN handmade_postversion AS ver ON post.current_id = ver.id
|
||||
WHERE
|
||||
post.project_id = $1
|
||||
AND post.category_kind = $2
|
||||
AND thread.type = $2
|
||||
AND post.id = thread.first_id
|
||||
AND NOT thread.deleted
|
||||
ORDER BY post.postdate DESC
|
||||
LIMIT 1
|
||||
`,
|
||||
models.HMNProjectID,
|
||||
models.CatKindBlog,
|
||||
models.ThreadTypeProjectArticle,
|
||||
)
|
||||
if err != nil {
|
||||
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch news post"))
|
||||
|
@ -294,6 +291,7 @@ func Index(c *RequestContext) ResponseData {
|
|||
LEFT JOIN handmade_asset AS asset ON asset.id = snippet.asset_id
|
||||
LEFT JOIN handmade_discordmessage AS discord_message ON discord_message.id = snippet.discord_message_id
|
||||
ORDER BY snippet.when DESC
|
||||
LIMIT 20
|
||||
`,
|
||||
)
|
||||
if err != nil {
|
||||
|
|
|
@ -7,33 +7,20 @@ import (
|
|||
)
|
||||
|
||||
// NOTE(asaf): Please don't use this if you already know the kind of the post beforehand. Just call the appropriate build function.
|
||||
// You may pass 0 for `libraryResourceId` if the post is not a library resource post.
|
||||
func UrlForGenericPost(post *models.Post, subforums []string, threadTitle string, libraryResourceId int, projectSlug string) string {
|
||||
switch post.CategoryKind {
|
||||
case models.CatKindBlog:
|
||||
func UrlForGenericPost(thread *models.Thread, post *models.Post, lineageBuilder *models.SubforumLineageBuilder, projectSlug string) string {
|
||||
switch post.ThreadType {
|
||||
case models.ThreadTypeProjectArticle:
|
||||
return hmnurl.BuildBlogPost(projectSlug, post.ThreadID, post.ID)
|
||||
case models.CatKindForum:
|
||||
return hmnurl.BuildForumPost(projectSlug, subforums, post.ThreadID, post.ID)
|
||||
case models.CatKindWiki:
|
||||
if post.ParentID == nil {
|
||||
// NOTE(asaf): First post on a wiki "thread" is the wiki article itself
|
||||
return hmnurl.BuildWikiArticle(projectSlug, post.ThreadID, threadTitle)
|
||||
} else {
|
||||
// NOTE(asaf): Subsequent posts on a wiki "thread" are wiki talk posts
|
||||
return hmnurl.BuildWikiTalkPost(projectSlug, post.ThreadID, post.ID)
|
||||
}
|
||||
case models.CatKindLibraryResource:
|
||||
return hmnurl.BuildLibraryPost(projectSlug, libraryResourceId, post.ThreadID, post.ID)
|
||||
case models.ThreadTypeForumPost:
|
||||
return hmnurl.BuildForumPost(projectSlug, lineageBuilder.GetSubforumLineageSlugs(*thread.SubforumID), post.ThreadID, post.ID)
|
||||
}
|
||||
|
||||
return hmnurl.BuildProjectHomepage(projectSlug)
|
||||
}
|
||||
|
||||
var PostTypeMap = map[models.CategoryKind][]templates.PostType{
|
||||
models.CatKindBlog: []templates.PostType{templates.PostTypeBlogPost, templates.PostTypeBlogComment},
|
||||
models.CatKindForum: []templates.PostType{templates.PostTypeForumThread, templates.PostTypeForumReply},
|
||||
models.CatKindWiki: []templates.PostType{templates.PostTypeWikiCreate, templates.PostTypeWikiTalk},
|
||||
models.CatKindLibraryResource: []templates.PostType{templates.PostTypeLibraryComment, templates.PostTypeLibraryComment},
|
||||
var PostTypeMap = map[models.ThreadType][]templates.PostType{
|
||||
models.ThreadTypeProjectArticle: {templates.PostTypeBlogPost, templates.PostTypeBlogComment},
|
||||
models.ThreadTypeForumPost: {templates.PostTypeForumThread, templates.PostTypeForumReply},
|
||||
}
|
||||
|
||||
var PostTypePrefix = map[templates.PostType]string{
|
||||
|
@ -41,58 +28,53 @@ var PostTypePrefix = map[templates.PostType]string{
|
|||
templates.PostTypeBlogComment: "Blog comment",
|
||||
templates.PostTypeForumThread: "New forum thread",
|
||||
templates.PostTypeForumReply: "Forum reply",
|
||||
templates.PostTypeWikiCreate: "New wiki page",
|
||||
templates.PostTypeWikiTalk: "Wiki comment",
|
||||
templates.PostTypeWikiEdit: "Wiki edit",
|
||||
templates.PostTypeLibraryComment: "Library comment",
|
||||
}
|
||||
|
||||
func PostBreadcrumbs(lineageBuilder *models.CategoryLineageBuilder, project *models.Project, post *models.Post, libraryResource *models.LibraryResource) []templates.Breadcrumb {
|
||||
func PostBreadcrumbs(lineageBuilder *models.SubforumLineageBuilder, project *models.Project, thread *models.Thread) []templates.Breadcrumb {
|
||||
var result []templates.Breadcrumb
|
||||
result = append(result, templates.Breadcrumb{
|
||||
Name: project.Name,
|
||||
Url: hmnurl.BuildProjectHomepage(project.Slug),
|
||||
})
|
||||
result = append(result, templates.Breadcrumb{
|
||||
Name: CategoryKindDisplayNames[post.CategoryKind],
|
||||
Url: BuildProjectMainCategoryUrl(project.Slug, post.CategoryKind),
|
||||
Name: ThreadTypeDisplayNames[thread.Type],
|
||||
Url: BuildProjectRootResourceUrl(project.Slug, thread.Type),
|
||||
})
|
||||
switch post.CategoryKind {
|
||||
case models.CatKindForum:
|
||||
subforums := lineageBuilder.GetSubforumLineage(post.CategoryID)
|
||||
slugs := lineageBuilder.GetSubforumLineageSlugs(post.CategoryID)
|
||||
switch thread.Type {
|
||||
case models.ThreadTypeForumPost:
|
||||
subforums := lineageBuilder.GetSubforumLineage(*thread.SubforumID)
|
||||
slugs := lineageBuilder.GetSubforumLineageSlugs(*thread.SubforumID)
|
||||
for i, subforum := range subforums {
|
||||
result = append(result, templates.Breadcrumb{
|
||||
Name: *subforum.Name, // NOTE(asaf): All subforum categories must have names.
|
||||
Url: hmnurl.BuildForumCategory(project.Slug, slugs[0:i+1], 1),
|
||||
Name: subforum.Name,
|
||||
Url: hmnurl.BuildForum(project.Slug, slugs[0:i+1], 1),
|
||||
})
|
||||
}
|
||||
case models.CatKindLibraryResource:
|
||||
result = append(result, templates.Breadcrumb{
|
||||
Name: libraryResource.Name,
|
||||
Url: hmnurl.BuildLibraryResource(project.Slug, libraryResource.ID),
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// NOTE(asaf): THIS DOESN'T HANDLE WIKI EDIT ITEMS. Wiki edits are PostTextVersions, not Posts.
|
||||
func MakePostListItem(lineageBuilder *models.CategoryLineageBuilder, project *models.Project, thread *models.Thread, post *models.Post, user *models.User, libraryResource *models.LibraryResource, unread bool, includeBreadcrumbs bool, currentTheme string) templates.PostListItem {
|
||||
func MakePostListItem(
|
||||
lineageBuilder *models.SubforumLineageBuilder,
|
||||
project *models.Project,
|
||||
thread *models.Thread,
|
||||
post *models.Post,
|
||||
user *models.User,
|
||||
unread bool,
|
||||
includeBreadcrumbs bool,
|
||||
currentTheme string,
|
||||
) templates.PostListItem {
|
||||
var result templates.PostListItem
|
||||
|
||||
result.Title = thread.Title
|
||||
result.User = templates.UserToTemplate(user, currentTheme)
|
||||
result.Date = post.PostDate
|
||||
result.Unread = unread
|
||||
libraryResourceId := 0
|
||||
if libraryResource != nil {
|
||||
libraryResourceId = libraryResource.ID
|
||||
}
|
||||
result.Url = UrlForGenericPost(post, lineageBuilder.GetSubforumLineageSlugs(post.CategoryID), thread.Title, libraryResourceId, project.Slug)
|
||||
result.Url = UrlForGenericPost(thread, post, lineageBuilder, project.Slug)
|
||||
result.Preview = post.Preview
|
||||
|
||||
postType := templates.PostTypeUnknown
|
||||
postTypeOptions, found := PostTypeMap[post.CategoryKind]
|
||||
postTypeOptions, found := PostTypeMap[post.ThreadType]
|
||||
if found {
|
||||
var hasParent int
|
||||
if post.ParentID != nil {
|
||||
|
@ -104,7 +86,7 @@ func MakePostListItem(lineageBuilder *models.CategoryLineageBuilder, project *mo
|
|||
result.PostTypePrefix = PostTypePrefix[result.PostType]
|
||||
|
||||
if includeBreadcrumbs {
|
||||
result.Breadcrumbs = PostBreadcrumbs(lineageBuilder, project, post, libraryResource)
|
||||
result.Breadcrumbs = PostBreadcrumbs(lineageBuilder, project, thread)
|
||||
}
|
||||
|
||||
return result
|
||||
|
|
|
@ -339,9 +339,9 @@ func ProjectHomepage(c *RequestContext) ResponseData {
|
|||
}
|
||||
c.Perf.EndBlock()
|
||||
|
||||
c.Perf.StartBlock("SQL", "Fetch category tree")
|
||||
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
|
||||
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
||||
c.Perf.StartBlock("SQL", "Fetch subforum tree")
|
||||
subforumTree := models.GetFullSubforumTree(c.Context(), c.Conn)
|
||||
lineageBuilder := models.MakeSubforumLineageBuilder(subforumTree)
|
||||
c.Perf.EndBlock()
|
||||
|
||||
c.Perf.StartBlock("SQL", "Fetching project timeline")
|
||||
|
@ -431,7 +431,6 @@ func ProjectHomepage(c *RequestContext) ResponseData {
|
|||
&post.(*postQuery).Post,
|
||||
&post.(*postQuery).Thread,
|
||||
project,
|
||||
nil,
|
||||
&post.(*postQuery).Author,
|
||||
c.Theme,
|
||||
))
|
||||
|
|
|
@ -156,8 +156,8 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt
|
|||
mainRoutes.GET(hmnurl.RegexForumNewThread, authMiddleware(ForumNewThread))
|
||||
mainRoutes.POST(hmnurl.RegexForumNewThreadSubmit, authMiddleware(csrfMiddleware(ForumNewThreadSubmit)))
|
||||
mainRoutes.GET(hmnurl.RegexForumThread, ForumThread)
|
||||
mainRoutes.GET(hmnurl.RegexForumCategory, ForumCategory)
|
||||
mainRoutes.POST(hmnurl.RegexForumCategoryMarkRead, authMiddleware(csrfMiddleware(ForumCategoryMarkRead)))
|
||||
mainRoutes.GET(hmnurl.RegexForum, Forum)
|
||||
mainRoutes.POST(hmnurl.RegexForumMarkRead, authMiddleware(csrfMiddleware(ForumMarkRead)))
|
||||
mainRoutes.GET(hmnurl.RegexForumPost, ForumPostRedirect)
|
||||
mainRoutes.GET(hmnurl.RegexForumPostReply, authMiddleware(ForumPostReply))
|
||||
mainRoutes.POST(hmnurl.RegexForumPostReply, authMiddleware(csrfMiddleware(ForumPostReplySubmit)))
|
||||
|
@ -216,8 +216,7 @@ func getBaseData(c *RequestContext) templates.BaseData {
|
|||
ProjectHomepageUrl: hmnurl.BuildProjectHomepage(c.CurrentProject.Slug),
|
||||
ProjectIndexUrl: hmnurl.BuildProjectIndex(1),
|
||||
BlogUrl: hmnurl.BuildBlog(c.CurrentProject.Slug, 1),
|
||||
ForumsUrl: hmnurl.BuildForumCategory(c.CurrentProject.Slug, nil, 1),
|
||||
WikiUrl: hmnurl.BuildWiki(c.CurrentProject.Slug),
|
||||
ForumsUrl: hmnurl.BuildForum(c.CurrentProject.Slug, nil, 1),
|
||||
LibraryUrl: hmnurl.BuildLibrary(c.CurrentProject.Slug),
|
||||
ManifestoUrl: hmnurl.BuildManifesto(),
|
||||
EpisodeGuideUrl: hmnurl.BuildHomepage(), // TODO(asaf)
|
||||
|
@ -231,7 +230,7 @@ func getBaseData(c *RequestContext) templates.BaseData {
|
|||
CodeOfConductUrl: hmnurl.BuildCodeOfConduct(),
|
||||
CommunicationGuidelinesUrl: hmnurl.BuildCommunicationGuidelines(),
|
||||
ProjectIndexUrl: hmnurl.BuildProjectIndex(1),
|
||||
ForumsUrl: hmnurl.BuildForumCategory(models.HMNProjectSlug, nil, 1),
|
||||
ForumsUrl: hmnurl.BuildForum(models.HMNProjectSlug, nil, 1),
|
||||
ContactUrl: hmnurl.BuildContactPage(),
|
||||
SitemapUrl: hmnurl.BuildSiteMap(),
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
||||
var TimelineTypeMap = map[models.CategoryKind][]templates.TimelineType{
|
||||
var TimelineTypeMap = map[models.ThreadType][]templates.TimelineType{
|
||||
// { No parent , Has parent }
|
||||
models.CatKindBlog: {templates.TimelineTypeBlogPost, templates.TimelineTypeBlogComment},
|
||||
models.CatKindForum: {templates.TimelineTypeForumThread, templates.TimelineTypeForumReply},
|
||||
models.CatKindWiki: {templates.TimelineTypeWikiCreate, templates.TimelineTypeWikiTalk},
|
||||
models.CatKindLibraryResource: {templates.TimelineTypeLibraryComment, templates.TimelineTypeLibraryComment},
|
||||
models.ThreadTypeProjectArticle: {templates.TimelineTypeBlogPost, templates.TimelineTypeBlogComment},
|
||||
models.ThreadTypeForumPost: {templates.TimelineTypeForumThread, templates.TimelineTypeForumReply},
|
||||
}
|
||||
|
||||
var TimelineItemClassMap = map[templates.TimelineType]string{
|
||||
|
@ -27,12 +25,6 @@ var TimelineItemClassMap = map[templates.TimelineType]string{
|
|||
templates.TimelineTypeBlogPost: "blogs",
|
||||
templates.TimelineTypeBlogComment: "blogs",
|
||||
|
||||
templates.TimelineTypeWikiCreate: "wiki",
|
||||
templates.TimelineTypeWikiEdit: "wiki",
|
||||
templates.TimelineTypeWikiTalk: "wiki",
|
||||
|
||||
templates.TimelineTypeLibraryComment: "library",
|
||||
|
||||
templates.TimelineTypeSnippetImage: "snippets",
|
||||
templates.TimelineTypeSnippetVideo: "snippets",
|
||||
templates.TimelineTypeSnippetAudio: "snippets",
|
||||
|
@ -48,21 +40,15 @@ var TimelineTypeTitleMap = map[templates.TimelineType]string{
|
|||
templates.TimelineTypeBlogPost: "New blog post",
|
||||
templates.TimelineTypeBlogComment: "Blog comment",
|
||||
|
||||
templates.TimelineTypeWikiCreate: "New wiki article",
|
||||
templates.TimelineTypeWikiEdit: "Wiki edit",
|
||||
templates.TimelineTypeWikiTalk: "Wiki talk",
|
||||
|
||||
templates.TimelineTypeLibraryComment: "Library comment",
|
||||
|
||||
templates.TimelineTypeSnippetImage: "Snippet",
|
||||
templates.TimelineTypeSnippetVideo: "Snippet",
|
||||
templates.TimelineTypeSnippetAudio: "Snippet",
|
||||
templates.TimelineTypeSnippetYoutube: "Snippet",
|
||||
}
|
||||
|
||||
func PostToTimelineItem(lineageBuilder *models.CategoryLineageBuilder, post *models.Post, thread *models.Thread, project *models.Project, libraryResource *models.LibraryResource, owner *models.User, currentTheme string) templates.TimelineItem {
|
||||
func PostToTimelineItem(lineageBuilder *models.SubforumLineageBuilder, post *models.Post, thread *models.Thread, project *models.Project, owner *models.User, currentTheme string) templates.TimelineItem {
|
||||
itemType := templates.TimelineTypeUnknown
|
||||
typeByCatKind, found := TimelineTypeMap[post.CategoryKind]
|
||||
typeByCatKind, found := TimelineTypeMap[post.ThreadType]
|
||||
if found {
|
||||
hasParent := 0
|
||||
if post.ParentID != nil {
|
||||
|
@ -71,17 +57,12 @@ func PostToTimelineItem(lineageBuilder *models.CategoryLineageBuilder, post *mod
|
|||
itemType = typeByCatKind[hasParent]
|
||||
}
|
||||
|
||||
libraryResourceId := 0
|
||||
if libraryResource != nil {
|
||||
libraryResourceId = libraryResource.ID
|
||||
}
|
||||
|
||||
return templates.TimelineItem{
|
||||
Type: itemType,
|
||||
TypeTitle: TimelineTypeTitleMap[itemType],
|
||||
Class: TimelineItemClassMap[itemType],
|
||||
Date: post.PostDate,
|
||||
Url: UrlForGenericPost(post, lineageBuilder.GetSubforumLineageSlugs(post.CategoryID), thread.Title, libraryResourceId, project.Slug),
|
||||
Url: UrlForGenericPost(thread, post, lineageBuilder, project.Slug),
|
||||
|
||||
OwnerAvatarUrl: templates.UserAvatarUrl(owner, currentTheme),
|
||||
OwnerName: templates.UserDisplayName(owner),
|
||||
|
@ -89,25 +70,7 @@ func PostToTimelineItem(lineageBuilder *models.CategoryLineageBuilder, post *mod
|
|||
Description: "", // NOTE(asaf): No description for posts
|
||||
|
||||
Title: thread.Title,
|
||||
Breadcrumbs: PostBreadcrumbs(lineageBuilder, project, post, libraryResource),
|
||||
}
|
||||
}
|
||||
|
||||
func PostVersionToWikiTimelineItem(lineageBuilder *models.CategoryLineageBuilder, version *models.PostVersion, post *models.Post, thread *models.Thread, project *models.Project, owner *models.User, currentTheme string) templates.TimelineItem {
|
||||
return templates.TimelineItem{
|
||||
Type: templates.TimelineTypeWikiEdit,
|
||||
TypeTitle: TimelineTypeTitleMap[templates.TimelineTypeWikiEdit],
|
||||
Class: TimelineItemClassMap[templates.TimelineTypeWikiEdit],
|
||||
Date: version.Date,
|
||||
Url: hmnurl.BuildWikiArticle(project.Slug, thread.ID, thread.Title),
|
||||
|
||||
OwnerAvatarUrl: templates.UserAvatarUrl(owner, currentTheme),
|
||||
OwnerName: templates.UserDisplayName(owner),
|
||||
OwnerUrl: hmnurl.BuildUserProfile(owner.Username),
|
||||
Description: "", // NOTE(asaf): No description for posts
|
||||
|
||||
Title: thread.Title,
|
||||
Breadcrumbs: PostBreadcrumbs(lineageBuilder, project, post, nil),
|
||||
Breadcrumbs: PostBreadcrumbs(lineageBuilder, project, thread),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,16 +5,12 @@ import (
|
|||
"git.handmade.network/hmn/hmn/src/models"
|
||||
)
|
||||
|
||||
func BuildProjectMainCategoryUrl(projectSlug string, kind models.CategoryKind) string {
|
||||
func BuildProjectRootResourceUrl(projectSlug string, kind models.ThreadType) string {
|
||||
switch kind {
|
||||
case models.CatKindBlog:
|
||||
case models.ThreadTypeProjectArticle:
|
||||
return hmnurl.BuildBlog(projectSlug, 1)
|
||||
case models.CatKindForum:
|
||||
return hmnurl.BuildForumCategory(projectSlug, nil, 1)
|
||||
case models.CatKindWiki:
|
||||
return hmnurl.BuildWiki(projectSlug)
|
||||
case models.CatKindLibraryResource:
|
||||
return hmnurl.BuildLibrary(projectSlug)
|
||||
case models.ThreadTypeForumPost:
|
||||
return hmnurl.BuildForum(projectSlug, nil, 1)
|
||||
}
|
||||
return hmnurl.BuildProjectHomepage(projectSlug)
|
||||
}
|
||||
|
|
|
@ -20,8 +20,6 @@ type UserProfileTemplateData struct {
|
|||
TimelineItems []templates.TimelineItem
|
||||
NumForums int
|
||||
NumBlogs int
|
||||
NumWiki int
|
||||
NumLibrary int
|
||||
NumSnippets int
|
||||
}
|
||||
|
||||
|
@ -117,7 +115,6 @@ func UserProfile(c *RequestContext) ResponseData {
|
|||
type postQuery struct {
|
||||
Post models.Post `db:"post"`
|
||||
Thread models.Thread `db:"thread"`
|
||||
LibraryResource *models.LibraryResource `db:"lib_resource"`
|
||||
Project models.Project `db:"project"`
|
||||
}
|
||||
c.Perf.StartBlock("SQL", "Fetch posts")
|
||||
|
@ -128,7 +125,6 @@ func UserProfile(c *RequestContext) ResponseData {
|
|||
handmade_post AS post
|
||||
INNER JOIN handmade_thread AS thread ON thread.id = post.thread_id
|
||||
INNER JOIN handmade_project AS project ON project.id = post.project_id
|
||||
LEFT JOIN handmade_libraryresource AS lib_resource ON lib_resource.category_id = post.category_id
|
||||
WHERE
|
||||
post.author_id = $1
|
||||
AND project.lifecycle = ANY ($2)
|
||||
|
@ -142,37 +138,6 @@ func UserProfile(c *RequestContext) ResponseData {
|
|||
postQuerySlice := postQueryResult.ToSlice()
|
||||
c.Perf.EndBlock()
|
||||
|
||||
type wikiEditQuery struct {
|
||||
PostVersion models.PostVersion `db:"version"`
|
||||
Post models.Post `db:"post"`
|
||||
Thread models.Thread `db:"thread"`
|
||||
Project models.Project `db:"project"`
|
||||
}
|
||||
c.Perf.StartBlock("SQL", "Fetch wiki edits")
|
||||
wikiEditQueryResult, err := db.Query(c.Context(), c.Conn, wikiEditQuery{},
|
||||
`
|
||||
SELECT $columns
|
||||
FROM
|
||||
handmade_postversion AS version
|
||||
INNER JOIN handmade_post AS post ON post.id = version.post_id
|
||||
INNER JOIN handmade_thread AS thread on thread.id = post.thread_id
|
||||
INNER JOIN handmade_project AS project ON project.id = post.project_id
|
||||
WHERE
|
||||
version.editor_id = $1
|
||||
AND post.parent_id IS NULL
|
||||
AND post.category_kind = $2
|
||||
AND project.lifecycle = ANY ($3)
|
||||
`,
|
||||
profileUser.ID,
|
||||
models.CatKindWiki,
|
||||
models.VisibleProjectLifecycles,
|
||||
)
|
||||
if err != nil {
|
||||
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch wiki edits for user: %s", username))
|
||||
}
|
||||
wikiEditQuerySlice := wikiEditQueryResult.ToSlice()
|
||||
c.Perf.EndBlock()
|
||||
|
||||
type snippetQuery struct {
|
||||
Snippet models.Snippet `db:"snippet"`
|
||||
Asset *models.Asset `db:"asset"`
|
||||
|
@ -197,17 +162,15 @@ func UserProfile(c *RequestContext) ResponseData {
|
|||
snippetQuerySlice := snippetQueryResult.ToSlice()
|
||||
c.Perf.EndBlock()
|
||||
|
||||
c.Perf.StartBlock("SQL", "Fetch category tree")
|
||||
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
|
||||
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
||||
c.Perf.StartBlock("SQL", "Fetch subforum tree")
|
||||
subforumTree := models.GetFullSubforumTree(c.Context(), c.Conn)
|
||||
lineageBuilder := models.MakeSubforumLineageBuilder(subforumTree)
|
||||
c.Perf.EndBlock()
|
||||
|
||||
c.Perf.StartBlock("PROFILE", "Construct timeline items")
|
||||
timelineItems := make([]templates.TimelineItem, 0, len(postQuerySlice)+len(wikiEditQuerySlice)+len(snippetQuerySlice))
|
||||
timelineItems := make([]templates.TimelineItem, 0, len(postQuerySlice)+len(snippetQuerySlice))
|
||||
numForums := 0
|
||||
numBlogs := 0
|
||||
numWiki := len(wikiEditQuerySlice)
|
||||
numLibrary := 0
|
||||
numSnippets := len(snippetQuerySlice)
|
||||
|
||||
for _, postRow := range postQuerySlice {
|
||||
|
@ -217,7 +180,6 @@ func UserProfile(c *RequestContext) ResponseData {
|
|||
&postData.Post,
|
||||
&postData.Thread,
|
||||
&postData.Project,
|
||||
postData.LibraryResource,
|
||||
profileUser,
|
||||
c.Theme,
|
||||
)
|
||||
|
@ -226,10 +188,6 @@ func UserProfile(c *RequestContext) ResponseData {
|
|||
numForums += 1
|
||||
case templates.TimelineTypeBlogPost, templates.TimelineTypeBlogComment:
|
||||
numBlogs += 1
|
||||
case templates.TimelineTypeWikiCreate, templates.TimelineTypeWikiTalk:
|
||||
numWiki += 1
|
||||
case templates.TimelineTypeLibraryComment:
|
||||
numLibrary += 1
|
||||
}
|
||||
if timelineItem.Type != templates.TimelineTypeUnknown {
|
||||
timelineItems = append(timelineItems, timelineItem)
|
||||
|
@ -238,20 +196,6 @@ func UserProfile(c *RequestContext) ResponseData {
|
|||
}
|
||||
}
|
||||
|
||||
for _, wikiEditRow := range wikiEditQuerySlice {
|
||||
wikiEditData := wikiEditRow.(*wikiEditQuery)
|
||||
timelineItem := PostVersionToWikiTimelineItem(
|
||||
lineageBuilder,
|
||||
&wikiEditData.PostVersion,
|
||||
&wikiEditData.Post,
|
||||
&wikiEditData.Thread,
|
||||
&wikiEditData.Project,
|
||||
profileUser,
|
||||
c.Theme,
|
||||
)
|
||||
timelineItems = append(timelineItems, timelineItem)
|
||||
}
|
||||
|
||||
for _, snippetRow := range snippetQuerySlice {
|
||||
snippetData := snippetRow.(*snippetQuery)
|
||||
timelineItem := SnippetToTimelineItem(
|
||||
|
@ -282,8 +226,6 @@ func UserProfile(c *RequestContext) ResponseData {
|
|||
TimelineItems: timelineItems,
|
||||
NumForums: numForums,
|
||||
NumBlogs: numBlogs,
|
||||
NumWiki: numWiki,
|
||||
NumLibrary: numLibrary,
|
||||
NumSnippets: numSnippets,
|
||||
}, c.Perf)
|
||||
return res
|
||||
|
|
Loading…
Reference in New Issue