URL rework!
This commit is contained in:
parent
4e946cd476
commit
02938bbf2c
|
@ -1,7 +1,9 @@
|
||||||
package hmnurl
|
package hmnurl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.handmade.network/hmn/hmn/src/config"
|
"git.handmade.network/hmn/hmn/src/config"
|
||||||
"git.handmade.network/hmn/hmn/src/models"
|
"git.handmade.network/hmn/hmn/src/models"
|
||||||
|
@ -17,9 +19,11 @@ type Q struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
var baseUrlParsed url.URL
|
var baseUrlParsed url.URL
|
||||||
|
var cacheBust string
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
SetGlobalBaseUrl(config.Config.BaseUrl)
|
SetGlobalBaseUrl(config.Config.BaseUrl)
|
||||||
|
SetCacheBust(fmt.Sprint(time.Now().Unix()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetGlobalBaseUrl(fullBaseUrl string) {
|
func SetGlobalBaseUrl(fullBaseUrl string) {
|
||||||
|
@ -34,11 +38,19 @@ func SetGlobalBaseUrl(fullBaseUrl string) {
|
||||||
baseUrlParsed = *parsed
|
baseUrlParsed = *parsed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SetCacheBust(newCacheBust string) {
|
||||||
|
cacheBust = newCacheBust
|
||||||
|
}
|
||||||
|
|
||||||
func Url(path string, query []Q) string {
|
func Url(path string, query []Q) string {
|
||||||
return ProjectUrl(path, query, "")
|
return ProjectUrl(path, query, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProjectUrl(path string, query []Q, slug string) string {
|
func ProjectUrl(path string, query []Q, slug string) string {
|
||||||
|
return ProjectUrlWithFragment(path, query, slug, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProjectUrlWithFragment(path string, query []Q, slug string, fragment string) string {
|
||||||
subdomain := slug
|
subdomain := slug
|
||||||
if slug == models.HMNProjectSlug {
|
if slug == models.HMNProjectSlug {
|
||||||
subdomain = ""
|
subdomain = ""
|
||||||
|
@ -54,6 +66,7 @@ func ProjectUrl(path string, query []Q, slug string) string {
|
||||||
Host: host,
|
Host: host,
|
||||||
Path: trim(path),
|
Path: trim(path),
|
||||||
RawQuery: encodeQuery(query),
|
RawQuery: encodeQuery(query),
|
||||||
|
Fragment: fragment,
|
||||||
}
|
}
|
||||||
|
|
||||||
return url.String()
|
return url.String()
|
||||||
|
|
|
@ -27,14 +27,32 @@ func TestUrl(t *testing.T) {
|
||||||
|
|
||||||
func TestHomepage(t *testing.T) {
|
func TestHomepage(t *testing.T) {
|
||||||
AssertRegexMatch(t, BuildHomepage(), RegexHomepage, nil)
|
AssertRegexMatch(t, BuildHomepage(), RegexHomepage, nil)
|
||||||
|
AssertRegexMatch(t, BuildProjectHomepage("hero"), RegexHomepage, nil)
|
||||||
|
AssertSubdomain(t, BuildProjectHomepage("hero"), "hero")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLogin(t *testing.T) {
|
func TestProjectIndex(t *testing.T) {
|
||||||
AssertRegexMatch(t, BuildLogin(), RegexLogin, nil)
|
AssertRegexMatch(t, BuildProjectIndex(), RegexProjectIndex, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLogout(t *testing.T) {
|
func TestSiteMap(t *testing.T) {
|
||||||
AssertRegexMatch(t, BuildLogout(), RegexLogout, nil)
|
AssertRegexMatch(t, BuildSiteMap(), RegexSiteMap, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAtomFeed(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildAtomFeed(), RegexAtomFeed, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoginAction(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildLoginAction(""), RegexLoginAction, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoginPage(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildLoginPage(""), RegexLoginPage, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogoutAction(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildLogoutAction(), RegexLogoutAction, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStaticPages(t *testing.T) {
|
func TestStaticPages(t *testing.T) {
|
||||||
|
@ -47,6 +65,10 @@ func TestStaticPages(t *testing.T) {
|
||||||
AssertRegexMatch(t, BuildProjectSubmissionGuidelines(), RegexProjectSubmissionGuidelines, nil)
|
AssertRegexMatch(t, BuildProjectSubmissionGuidelines(), RegexProjectSubmissionGuidelines, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMember(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildMember("test"), RegexMember, map[string]string{"member": "test"})
|
||||||
|
}
|
||||||
|
|
||||||
func TestFeed(t *testing.T) {
|
func TestFeed(t *testing.T) {
|
||||||
AssertRegexMatch(t, BuildFeed(), RegexFeed, nil)
|
AssertRegexMatch(t, BuildFeed(), RegexFeed, nil)
|
||||||
assert.Equal(t, BuildFeed(), BuildFeedWithPage(1))
|
assert.Equal(t, BuildFeed(), BuildFeedWithPage(1))
|
||||||
|
@ -57,30 +79,236 @@ func TestFeed(t *testing.T) {
|
||||||
assert.Panics(t, func() { BuildFeedWithPage(0) })
|
assert.Panics(t, func() { BuildFeedWithPage(0) })
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestForumThread(t *testing.T) {
|
|
||||||
AssertRegexMatch(t, BuildForumThread("", nil, 1, 1), RegexForumThread, map[string]string{"threadid": "1"})
|
|
||||||
AssertRegexMatch(t, BuildForumThread("", []string{"wip"}, 1, 2), RegexForumThread, map[string]string{"cats": "forums/wip", "page": "2", "threadid": "1"})
|
|
||||||
AssertRegexMatch(t, BuildForumThread("", []string{"sub", "wip"}, 1, 2), RegexForumThread, map[string]string{"cats": "forums/sub/wip", "page": "2", "threadid": "1"})
|
|
||||||
AssertSubdomain(t, BuildForumThread("hmn", nil, 1, 1), "")
|
|
||||||
AssertSubdomain(t, BuildForumThread("", nil, 1, 1), "")
|
|
||||||
AssertSubdomain(t, BuildForumThread("hero", nil, 1, 1), "hero")
|
|
||||||
assert.Panics(t, func() { BuildForumThread("", []string{"", "wip"}, 1, 1) })
|
|
||||||
assert.Panics(t, func() { BuildForumThread("", []string{" ", "wip"}, 1, 1) })
|
|
||||||
assert.Panics(t, func() { BuildForumThread("", []string{"wip/jobs"}, 1, 1) })
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestForumCategory(t *testing.T) {
|
func TestForumCategory(t *testing.T) {
|
||||||
AssertRegexMatch(t, BuildForumCategory("", nil, 1), RegexForumCategory, nil)
|
AssertRegexMatch(t, BuildForumCategory("", nil, 1), RegexForumCategory, nil)
|
||||||
AssertRegexMatch(t, BuildForumCategory("", []string{"wip"}, 2), RegexForumCategory, map[string]string{"cats": "forums/wip", "page": "2"})
|
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": "forums/sub/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("hmn", nil, 1), "")
|
||||||
AssertSubdomain(t, BuildForumCategory("", nil, 1), "")
|
AssertSubdomain(t, BuildForumCategory("", nil, 1), "")
|
||||||
AssertSubdomain(t, BuildForumCategory("hero", nil, 1), "hero")
|
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"}, 1) })
|
assert.Panics(t, func() { BuildForumCategory("", []string{" ", "wip"}, 1) })
|
||||||
assert.Panics(t, func() { BuildForumCategory("", []string{"wip/jobs"}, 1) })
|
assert.Panics(t, func() { BuildForumCategory("", []string{"wip/jobs"}, 1) })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestForumNewThread(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildForumNewThread("", []string{"sub", "wip"}), RegexForumNewThread, map[string]string{"cats": "sub/wip"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForumThread(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildForumThread("", nil, 1, "", 1), RegexForumThread, map[string]string{"threadid": "1"})
|
||||||
|
AssertRegexMatch(t, BuildForumThread("", nil, 1, "thread/title/123http://", 2), RegexForumThread, map[string]string{"threadid": "1", "page": "2"})
|
||||||
|
AssertRegexMatch(t, BuildForumThreadWithPostHash("", nil, 1, "thread/title/123http://", 2, 123), RegexForumThread, map[string]string{"threadid": "1", "page": "2"})
|
||||||
|
AssertSubdomain(t, BuildForumThread("hero", nil, 1, "", 1), "hero")
|
||||||
|
assert.Panics(t, func() { BuildForumThread("", nil, -1, "", 1) })
|
||||||
|
assert.Panics(t, func() { BuildForumThread("", nil, 1, "", -1) })
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForumPost(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildForumPost("", nil, 1, 2), RegexForumPost, map[string]string{"threadid": "1", "postid": "2"})
|
||||||
|
AssertRegexNoMatch(t, BuildForumPost("", nil, 1, 2), RegexForumThread)
|
||||||
|
AssertSubdomain(t, BuildForumPost("hero", nil, 1, 2), "hero")
|
||||||
|
assert.Panics(t, func() { BuildForumPost("", nil, 1, -1) })
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForumPostDelete(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildForumPostDelete("", nil, 1, 2), RegexForumPostDelete, map[string]string{"threadid": "1", "postid": "2"})
|
||||||
|
AssertRegexNoMatch(t, BuildForumPostDelete("", nil, 1, 2), RegexForumPost)
|
||||||
|
AssertSubdomain(t, BuildForumPostDelete("hero", nil, 1, 2), "hero")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForumPostEdit(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildForumPostEdit("", nil, 1, 2), RegexForumPostEdit, map[string]string{"threadid": "1", "postid": "2"})
|
||||||
|
AssertRegexNoMatch(t, BuildForumPostEdit("", nil, 1, 2), RegexForumPost)
|
||||||
|
AssertSubdomain(t, BuildForumPostEdit("hero", nil, 1, 2), "hero")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForumPostReply(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildForumPostReply("", nil, 1, 2), RegexForumPostReply, map[string]string{"threadid": "1", "postid": "2"})
|
||||||
|
AssertRegexNoMatch(t, BuildForumPostReply("", nil, 1, 2), RegexForumPost)
|
||||||
|
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"})
|
||||||
|
AssertSubdomain(t, BuildBlog("hero", 1), "hero")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBlogThread(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildBlogThread("", 1, "", 1), RegexBlogThread, map[string]string{"threadid": "1"})
|
||||||
|
AssertRegexMatch(t, BuildBlogThread("", 1, "", 2), RegexBlogThread, map[string]string{"threadid": "1", "page": "2"})
|
||||||
|
AssertRegexMatch(t, BuildBlogThread("", 1, "title/bla/http://", 2), RegexBlogThread, map[string]string{"threadid": "1", "page": "2"})
|
||||||
|
AssertRegexMatch(t, BuildBlogThreadWithPostHash("", 1, "title/bla/http://", 2, 123), RegexBlogThread, map[string]string{"threadid": "1", "page": "2"})
|
||||||
|
AssertRegexNoMatch(t, BuildBlogThread("", 1, "", 2), RegexBlog)
|
||||||
|
AssertSubdomain(t, BuildBlogThread("hero", 1, "", 1), "hero")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBlogPost(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildBlogPost("", 1, 2), RegexBlogPost, map[string]string{"threadid": "1", "postid": "2"})
|
||||||
|
AssertRegexNoMatch(t, BuildBlogPost("", 1, 2), RegexBlogThread)
|
||||||
|
AssertSubdomain(t, BuildBlogPost("hero", 1, 2), "hero")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBlogPostDelete(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildBlogPostDelete("", 1, 2), RegexBlogPostDelete, map[string]string{"threadid": "1", "postid": "2"})
|
||||||
|
AssertRegexNoMatch(t, BuildBlogPostDelete("", 1, 2), RegexBlogPost)
|
||||||
|
AssertSubdomain(t, BuildBlogPostDelete("hero", 1, 2), "hero")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBlogPostEdit(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildBlogPostEdit("", 1, 2), RegexBlogPostEdit, map[string]string{"threadid": "1", "postid": "2"})
|
||||||
|
AssertRegexNoMatch(t, BuildBlogPostEdit("", 1, 2), RegexBlogPost)
|
||||||
|
AssertSubdomain(t, BuildBlogPostEdit("hero", 1, 2), "hero")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBlogPostReply(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildBlogPostReply("", 1, 2), RegexBlogPostReply, map[string]string{"threadid": "1", "postid": "2"})
|
||||||
|
AssertRegexNoMatch(t, BuildBlogPostReply("", 1, 2), RegexBlogPost)
|
||||||
|
AssertSubdomain(t, BuildBlogPostReply("hero", 1, 2), "hero")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBlogPostQuote(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildBlogPostQuote("", 1, 2), RegexBlogPostQuote, map[string]string{"threadid": "1", "postid": "2"})
|
||||||
|
AssertRegexNoMatch(t, BuildBlogPostQuote("", 1, 2), RegexBlogPost)
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLibraryAll(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildLibraryAll(""), RegexLibraryAll, nil)
|
||||||
|
AssertSubdomain(t, BuildLibraryAll("hero"), "hero")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLibraryTopic(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildLibraryTopic("", 1), RegexLibraryTopic, map[string]string{"topicid": "1"})
|
||||||
|
AssertSubdomain(t, BuildLibraryTopic("hero", 1), "hero")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLibraryResource(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildLibraryResource("", 1), RegexLibraryResource, map[string]string{"resourceid": "1"})
|
||||||
|
AssertSubdomain(t, BuildLibraryResource("hero", 1), "hero")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLibraryDiscussion(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildLibraryDiscussion("", 1, 2, 1), RegexLibraryDiscussion, map[string]string{"resourceid": "1", "threadid": "2"})
|
||||||
|
AssertRegexMatch(t, BuildLibraryDiscussion("", 1, 2, 3), RegexLibraryDiscussion, map[string]string{"resourceid": "1", "threadid": "2", "page": "3"})
|
||||||
|
AssertRegexMatch(t, BuildLibraryDiscussionWithPostHash("", 1, 2, 3, 123), RegexLibraryDiscussion, map[string]string{"resourceid": "1", "threadid": "2", "page": "3"})
|
||||||
|
AssertSubdomain(t, BuildLibraryDiscussion("hero", 1, 2, 3), "hero")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLibraryPost(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildLibraryPost("", 1, 2, 3), RegexLibraryPost, map[string]string{"resourceid": "1", "threadid": "2", "postid": "3"})
|
||||||
|
AssertSubdomain(t, BuildLibraryPost("hero", 1, 2, 3), "hero")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLibraryPostDelete(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildLibraryPostDelete("", 1, 2, 3), RegexLibraryPostDelete, map[string]string{"resourceid": "1", "threadid": "2", "postid": "3"})
|
||||||
|
AssertSubdomain(t, BuildLibraryPostDelete("hero", 1, 2, 3), "hero")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLibraryPostEdit(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildLibraryPostEdit("", 1, 2, 3), RegexLibraryPostEdit, map[string]string{"resourceid": "1", "threadid": "2", "postid": "3"})
|
||||||
|
AssertSubdomain(t, BuildLibraryPostEdit("hero", 1, 2, 3), "hero")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLibraryPostReply(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildLibraryPostReply("", 1, 2, 3), RegexLibraryPostReply, map[string]string{"resourceid": "1", "threadid": "2", "postid": "3"})
|
||||||
|
AssertSubdomain(t, BuildLibraryPostReply("hero", 1, 2, 3), "hero")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLibraryPostQuote(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildLibraryPostQuote("", 1, 2, 3), RegexLibraryPostQuote, map[string]string{"resourceid": "1", "threadid": "2", "postid": "3"})
|
||||||
|
AssertSubdomain(t, BuildLibraryPostQuote("hero", 1, 2, 3), "hero")
|
||||||
|
}
|
||||||
|
|
||||||
func TestProjectCSS(t *testing.T) {
|
func TestProjectCSS(t *testing.T) {
|
||||||
AssertRegexMatch(t, BuildProjectCSS("000000"), RegexProjectCSS, nil)
|
AssertRegexMatch(t, BuildProjectCSS("000000"), RegexProjectCSS, nil)
|
||||||
}
|
}
|
||||||
|
@ -96,6 +324,10 @@ func TestPublic(t *testing.T) {
|
||||||
assert.Panics(t, func() { BuildPublic("/thing/ /image.png") })
|
assert.Panics(t, func() { BuildPublic("/thing/ /image.png") })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMarkRead(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildMarkRead(5), RegexMarkRead, map[string]string{"catid": "5"})
|
||||||
|
}
|
||||||
|
|
||||||
func AssertSubdomain(t *testing.T, fullUrl string, expectedSubdomain string) {
|
func AssertSubdomain(t *testing.T, fullUrl string, expectedSubdomain string) {
|
||||||
parsed, err := url.Parse(fullUrl)
|
parsed, err := url.Parse(fullUrl)
|
||||||
ok := assert.Nilf(t, err, "Full url could not be parsed: %s", fullUrl)
|
ok := assert.Nilf(t, err, "Full url could not be parsed: %s", fullUrl)
|
||||||
|
@ -144,3 +376,18 @@ func AssertRegexMatch(t *testing.T, fullUrl string, regex *regexp.Regexp, params
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AssertRegexNoMatch(t *testing.T, fullUrl string, regex *regexp.Regexp) {
|
||||||
|
parsed, err := url.Parse(fullUrl)
|
||||||
|
ok := assert.Nilf(t, err, "Full url could not be parsed: %s", fullUrl)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
requestPath := parsed.Path
|
||||||
|
if len(requestPath) == 0 {
|
||||||
|
requestPath = "/"
|
||||||
|
}
|
||||||
|
match := regex.FindStringSubmatch(requestPath)
|
||||||
|
assert.Nilf(t, match, "Url matched regex: [%s] vs [%s]", requestPath, regex.String())
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package hmnurl
|
package hmnurl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -8,24 +9,70 @@ import (
|
||||||
"git.handmade.network/hmn/hmn/src/oops"
|
"git.handmade.network/hmn/hmn/src/oops"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO(asaf): Make this whole file only crash in Dev
|
||||||
|
|
||||||
var RegexHomepage = regexp.MustCompile("^/$")
|
var RegexHomepage = regexp.MustCompile("^/$")
|
||||||
|
|
||||||
func BuildHomepage() string {
|
func BuildHomepage() string {
|
||||||
return Url("/", nil)
|
return Url("/", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
var RegexLogin = regexp.MustCompile("^/login$")
|
func BuildProjectHomepage(projectSlug string) string {
|
||||||
|
return ProjectUrl("/", nil, projectSlug)
|
||||||
func BuildLogin() string {
|
|
||||||
return Url("/login", nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var RegexLogout = regexp.MustCompile("^/logout$")
|
var RegexProjectIndex = regexp.MustCompile("^/projects$")
|
||||||
|
|
||||||
func BuildLogout() string {
|
func BuildProjectIndex() string {
|
||||||
|
return Url("/projects", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexShowcase = regexp.MustCompile("^/showcase$")
|
||||||
|
|
||||||
|
func BuildShowcase() string {
|
||||||
|
return Url("/showcase", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexStreams = regexp.MustCompile("^/streams$")
|
||||||
|
|
||||||
|
func BuildStreams() string {
|
||||||
|
return Url("/streams", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexSiteMap = regexp.MustCompile("^/sitemap$")
|
||||||
|
|
||||||
|
func BuildSiteMap() string {
|
||||||
|
return Url("/sitemap", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexAtomFeed = regexp.MustCompile("^/atom$")
|
||||||
|
|
||||||
|
func BuildAtomFeed() string {
|
||||||
|
return Url("/atom", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexLoginAction = regexp.MustCompile("^/login$")
|
||||||
|
|
||||||
|
func BuildLoginAction(redirectTo string) string {
|
||||||
|
return Url("/login", []Q{{Name: "redirect", Value: redirectTo}})
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexLoginPage = regexp.MustCompile("^/_login$")
|
||||||
|
|
||||||
|
func BuildLoginPage(redirectTo string) string {
|
||||||
|
return Url("/_login", []Q{{Name: "redirect", Value: redirectTo}})
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexLogoutAction = regexp.MustCompile("^/logout$")
|
||||||
|
|
||||||
|
func BuildLogoutAction() string {
|
||||||
return Url("/logout", nil)
|
return Url("/logout", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Static Pages
|
||||||
|
*/
|
||||||
|
|
||||||
var RegexManifesto = regexp.MustCompile("^/manifesto$")
|
var RegexManifesto = regexp.MustCompile("^/manifesto$")
|
||||||
|
|
||||||
func BuildManifesto() string {
|
func BuildManifesto() string {
|
||||||
|
@ -68,6 +115,23 @@ func BuildProjectSubmissionGuidelines() string {
|
||||||
return Url("/project-guidelines", nil)
|
return Url("/project-guidelines", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Member
|
||||||
|
*/
|
||||||
|
|
||||||
|
var RegexMember = regexp.MustCompile(`^/m/(?P<member>[^/]+)$`)
|
||||||
|
|
||||||
|
func BuildMember(username string) string {
|
||||||
|
if len(username) == 0 {
|
||||||
|
panic(oops.New(nil, "Username must not be blank"))
|
||||||
|
}
|
||||||
|
return Url("/m/"+username, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Feed
|
||||||
|
*/
|
||||||
|
|
||||||
var RegexFeed = regexp.MustCompile(`^/feed(/(?P<page>.+)?)?$`)
|
var RegexFeed = regexp.MustCompile(`^/feed(/(?P<page>.+)?)?$`)
|
||||||
|
|
||||||
func BuildFeed() string {
|
func BuildFeed() string {
|
||||||
|
@ -84,56 +148,31 @@ func BuildFeedWithPage(page int) string {
|
||||||
return Url("/feed/"+strconv.Itoa(page), nil)
|
return Url("/feed/"+strconv.Itoa(page), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
var RegexForumThread = regexp.MustCompile(`^/(?P<cats>forums(/[^\d]+?)*)/t/(?P<threadid>\d+)(/(?P<page>\d+))?$`)
|
/*
|
||||||
|
* Podcast
|
||||||
|
*/
|
||||||
|
|
||||||
func BuildForumThread(projectSlug string, subforums []string, threadId int, page int) string {
|
var RegexPodcast = regexp.MustCompile(`^/podcast$`)
|
||||||
if page < 1 {
|
|
||||||
panic(oops.New(nil, "Invalid forum thread page (%d), must be >= 1", page))
|
|
||||||
}
|
|
||||||
|
|
||||||
var builder strings.Builder
|
func BuildPodcast(projectSlug string) string {
|
||||||
builder.WriteString("/forums")
|
return ProjectUrl("/podcast", nil, projectSlug)
|
||||||
for _, subforum := range subforums {
|
|
||||||
subforum = strings.TrimSpace(subforum)
|
|
||||||
if strings.Contains(subforum, "/") {
|
|
||||||
panic(oops.New(nil, "Tried building forum thread url with / in subforum name"))
|
|
||||||
}
|
|
||||||
if len(subforum) == 0 {
|
|
||||||
panic(oops.New(nil, "Tried building forum thread url with blank subforum"))
|
|
||||||
}
|
|
||||||
builder.WriteRune('/')
|
|
||||||
builder.WriteString(subforum)
|
|
||||||
}
|
|
||||||
builder.WriteString("/t/")
|
|
||||||
builder.WriteString(strconv.Itoa(threadId))
|
|
||||||
if page > 1 {
|
|
||||||
builder.WriteRune('/')
|
|
||||||
builder.WriteString(strconv.Itoa(page))
|
|
||||||
}
|
|
||||||
|
|
||||||
return ProjectUrl(builder.String(), nil, projectSlug)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var RegexForumCategory = regexp.MustCompile(`^/(?P<cats>forums(/[^\d]+?)*)(/(?P<page>\d+))?$`)
|
/*
|
||||||
|
* 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 be enforce it here?
|
||||||
|
var RegexForumCategory = regexp.MustCompile(`^/forums(/(?P<cats>[^\d/]+(/[^\d]+)*))?(/(?P<page>\d+))?$`)
|
||||||
|
|
||||||
func BuildForumCategory(projectSlug string, subforums []string, page int) string {
|
func BuildForumCategory(projectSlug string, subforums []string, page int) string {
|
||||||
if page < 1 {
|
if page < 1 {
|
||||||
panic(oops.New(nil, "Invalid forum thread page (%d), must be >= 1", page))
|
panic(oops.New(nil, "Invalid forum thread page (%d), must be >= 1", page))
|
||||||
}
|
}
|
||||||
|
|
||||||
var builder strings.Builder
|
builder := buildForumCategoryPath(subforums)
|
||||||
builder.WriteString("/forums")
|
|
||||||
for _, subforum := range subforums {
|
|
||||||
subforum = strings.TrimSpace(subforum)
|
|
||||||
if strings.Contains(subforum, "/") {
|
|
||||||
panic(oops.New(nil, "Tried building forum thread url with / in subforum name"))
|
|
||||||
}
|
|
||||||
if len(subforum) == 0 {
|
|
||||||
panic(oops.New(nil, "Tried building forum thread url with blank subforum"))
|
|
||||||
}
|
|
||||||
builder.WriteRune('/')
|
|
||||||
builder.WriteString(subforum)
|
|
||||||
}
|
|
||||||
if page > 1 {
|
if page > 1 {
|
||||||
builder.WriteRune('/')
|
builder.WriteRune('/')
|
||||||
builder.WriteString(strconv.Itoa(page))
|
builder.WriteString(strconv.Itoa(page))
|
||||||
|
@ -142,55 +181,387 @@ func BuildForumCategory(projectSlug string, subforums []string, page int) string
|
||||||
return ProjectUrl(builder.String(), nil, projectSlug)
|
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||||
}
|
}
|
||||||
|
|
||||||
var RegexForumPost = regexp.MustCompile(``) // TODO(asaf): Complete this and test it
|
var RegexForumNewThread = regexp.MustCompile(`^/forums(/(?P<cats>[^\d/]+(/[^\d]+)*))?/new?$`)
|
||||||
|
|
||||||
func BuildForumPost(projectSlug string, subforums []string, threadId int, postId int) string {
|
func BuildForumNewThread(projectSlug string, subforums []string) string {
|
||||||
var builder strings.Builder
|
builder := buildForumCategoryPath(subforums)
|
||||||
builder.WriteString("/forums")
|
builder.WriteString("/new")
|
||||||
for _, subforum := range subforums {
|
|
||||||
subforum = strings.TrimSpace(subforum)
|
|
||||||
if strings.Contains(subforum, "/") {
|
|
||||||
panic(oops.New(nil, "Tried building forum thread url with / in subforum name"))
|
|
||||||
}
|
|
||||||
if len(subforum) == 0 {
|
|
||||||
panic(oops.New(nil, "Tried building forum thread url with blank subforum"))
|
|
||||||
}
|
|
||||||
builder.WriteRune('/')
|
|
||||||
builder.WriteString(subforum)
|
|
||||||
}
|
|
||||||
builder.WriteString("/t/")
|
|
||||||
builder.WriteString(strconv.Itoa(threadId))
|
|
||||||
builder.WriteString("/p/")
|
|
||||||
builder.WriteString(strconv.Itoa(postId))
|
|
||||||
|
|
||||||
return ProjectUrl(builder.String(), nil, projectSlug)
|
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||||
}
|
}
|
||||||
|
|
||||||
var RegexForumPostDelete = regexp.MustCompile(``) // TODO
|
var RegexForumThread = regexp.MustCompile(`^/forums(/(?P<cats>[^\d/]+(/[^\d]+)*))?/t/(?P<threadid>\d+)(-([^/]+))?(/(?P<page>\d+))?$`)
|
||||||
|
|
||||||
|
func BuildForumThread(projectSlug string, subforums []string, threadId int, title string, page int) string {
|
||||||
|
builder := buildForumThreadPath(subforums, threadId, title, page)
|
||||||
|
|
||||||
|
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildForumThreadWithPostHash(projectSlug string, subforums []string, threadId int, title string, page int, postId int) string {
|
||||||
|
builder := buildForumThreadPath(subforums, threadId, title, page)
|
||||||
|
|
||||||
|
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+)$`)
|
||||||
|
|
||||||
|
func BuildForumPost(projectSlug string, subforums []string, threadId int, postId int) string {
|
||||||
|
builder := buildForumPostPath(subforums, threadId, 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$`)
|
||||||
|
|
||||||
func BuildForumPostDelete(projectSlug string, subforums []string, threadId int, postId int) string {
|
func BuildForumPostDelete(projectSlug string, subforums []string, threadId int, postId int) string {
|
||||||
return BuildForumPost(projectSlug, subforums, threadId, postId) + "/delete"
|
builder := buildForumPostPath(subforums, threadId, postId)
|
||||||
|
builder.WriteString("/delete")
|
||||||
|
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||||
}
|
}
|
||||||
|
|
||||||
var RegexForumPostEdit = regexp.MustCompile(``) // TODO
|
var RegexForumPostEdit = regexp.MustCompile(`^/forums(/(?P<cats>[^\d/]+(/[^\d]+)*))?/t/(?P<threadid>\d+)/p/(?P<postid>\d+)/edit$`)
|
||||||
|
|
||||||
func BuildForumPostEdit(projectSlug string, subforums []string, threadId int, postId int) string {
|
func BuildForumPostEdit(projectSlug string, subforums []string, threadId int, postId int) string {
|
||||||
return BuildForumPost(projectSlug, subforums, threadId, postId) + "/edit"
|
builder := buildForumPostPath(subforums, threadId, postId)
|
||||||
|
builder.WriteString("/edit")
|
||||||
|
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||||
}
|
}
|
||||||
|
|
||||||
var RegexForumPostReply = regexp.MustCompile(``) // TODO(asaf): Ha ha! I, Ben, have played a trick on you, and forced you to do this regex as well!
|
var RegexForumPostReply = regexp.MustCompile(`^/forums(/(?P<cats>[^\d/]+(/[^\d]+)*))?/t/(?P<threadid>\d+)/p/(?P<postid>\d+)/reply$`)
|
||||||
|
|
||||||
// TODO: It's kinda weird that we have "replies" to a specific post. That's not how the data works. I guess this just affects what you see as the "post you're replying to" on the post composer page?
|
// TODO: It's kinda weird that we have "replies" to a specific post. That's not how the data works. I guess this just affects what you see as the "post you're replying to" on the post composer page?
|
||||||
func BuildForumPostReply(projectSlug string, subforums []string, threadId int, postId int) string {
|
func BuildForumPostReply(projectSlug string, subforums []string, threadId int, postId int) string {
|
||||||
return BuildForumPost(projectSlug, subforums, threadId, postId) + "/reply"
|
builder := buildForumPostPath(subforums, threadId, postId)
|
||||||
|
builder.WriteString("/reply")
|
||||||
|
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||||
}
|
}
|
||||||
|
|
||||||
var RegexForumPostQuote = regexp.MustCompile(``) // TODO
|
var RegexForumPostQuote = regexp.MustCompile(`^/forums(/(?P<cats>[^\d/]+(/[^\d]+)*))?/t/(?P<threadid>\d+)/p/(?P<postid>\d+)/quote$`)
|
||||||
|
|
||||||
func BuildForumPostQuote(projectSlug string, subforums []string, threadId int, postId int) string {
|
func BuildForumPostQuote(projectSlug string, subforums []string, threadId int, postId int) string {
|
||||||
return BuildForumPost(projectSlug, subforums, threadId, postId) + "/quote"
|
builder := buildForumPostPath(subforums, threadId, postId)
|
||||||
|
builder.WriteString("/quote")
|
||||||
|
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Blog
|
||||||
|
*/
|
||||||
|
|
||||||
|
var RegexBlogsRedirect = regexp.MustCompile(`^/blogs`)
|
||||||
|
|
||||||
|
var RegexBlog = regexp.MustCompile(`^/blog(/(?P<page>\d+))?$`)
|
||||||
|
|
||||||
|
func BuildBlog(projectSlug string, page int) string {
|
||||||
|
if page < 1 {
|
||||||
|
panic(oops.New(nil, "Invalid blog page (%d), must be >= 1", page))
|
||||||
|
}
|
||||||
|
path := "/blog"
|
||||||
|
|
||||||
|
if page > 1 {
|
||||||
|
path += "/" + strconv.Itoa(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProjectUrl(path, nil, projectSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexBlogThread = regexp.MustCompile(`^/blog/p/(?P<threadid>\d+)(-([^/]+))?(/(?P<page>\d+))?$`)
|
||||||
|
|
||||||
|
func BuildBlogThread(projectSlug string, threadId int, title string, page int) string {
|
||||||
|
builder := buildBlogThreadPath(threadId, title, page)
|
||||||
|
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildBlogThreadWithPostHash(projectSlug string, threadId int, title string, page int, postId int) string {
|
||||||
|
builder := buildBlogThreadPath(threadId, title, page)
|
||||||
|
return ProjectUrlWithFragment(builder.String(), nil, projectSlug, strconv.Itoa(postId))
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexBlogPost = regexp.MustCompile(`^/blog/p/(?P<threadid>\d+)/e/(?P<postid>\d+)$`)
|
||||||
|
|
||||||
|
func BuildBlogPost(projectSlug string, threadId int, postId int) string {
|
||||||
|
builder := buildBlogPostPath(threadId, postId)
|
||||||
|
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexBlogPostDelete = regexp.MustCompile(`^/blog/p/(?P<threadid>\d+)/e/(?P<postid>\d+)/delete$`)
|
||||||
|
|
||||||
|
func BuildBlogPostDelete(projectSlug string, threadId int, postId int) string {
|
||||||
|
builder := buildBlogPostPath(threadId, postId)
|
||||||
|
builder.WriteString("/delete")
|
||||||
|
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexBlogPostEdit = regexp.MustCompile(`^/blog/p/(?P<threadid>\d+)/e/(?P<postid>\d+)/edit$`)
|
||||||
|
|
||||||
|
func BuildBlogPostEdit(projectSlug string, threadId int, postId int) string {
|
||||||
|
builder := buildBlogPostPath(threadId, postId)
|
||||||
|
builder.WriteString("/edit")
|
||||||
|
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexBlogPostReply = regexp.MustCompile(`^/blog/p/(?P<threadid>\d+)/e/(?P<postid>\d+)/reply$`)
|
||||||
|
|
||||||
|
func BuildBlogPostReply(projectSlug string, threadId int, postId int) string {
|
||||||
|
builder := buildBlogPostPath(threadId, postId)
|
||||||
|
builder.WriteString("/reply")
|
||||||
|
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexBlogPostQuote = regexp.MustCompile(`^/blog/p/(?P<threadid>\d+)/e/(?P<postid>\d+)/quote$`)
|
||||||
|
|
||||||
|
func BuildBlogPostQuote(projectSlug string, threadId int, postId int) string {
|
||||||
|
builder := buildBlogPostPath(threadId, postId)
|
||||||
|
builder.WriteString("/quote")
|
||||||
|
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Wiki
|
||||||
|
*/
|
||||||
|
|
||||||
|
var RegexWiki = regexp.MustCompile(`^/wiki$`)
|
||||||
|
|
||||||
|
func BuildWiki(projectSlug string) string {
|
||||||
|
return ProjectUrl("/wiki", nil, projectSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexWikiIndex = regexp.MustCompile(`^/wiki/index$`)
|
||||||
|
|
||||||
|
func BuildWikiIndex(projectSlug string) string {
|
||||||
|
return ProjectUrl("/wiki/index", nil, projectSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexWikiArticle = regexp.MustCompile(`^/wiki/(?P<articleid>\d+)(-([^/])+)?$`)
|
||||||
|
|
||||||
|
func BuildWikiArticle(projectSlug string, articleId int, title string) string {
|
||||||
|
builder := buildWikiArticlePath(articleId, title)
|
||||||
|
|
||||||
|
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildWikiArticleWithSectionName(projectSlug string, articleId int, title string, sectionName string) string {
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
builder := buildWikiTalkPath(articleId, postId)
|
||||||
|
builder.WriteString("/quote")
|
||||||
|
|
||||||
|
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Library
|
||||||
|
*/
|
||||||
|
|
||||||
|
var RegexLibrary = regexp.MustCompile(`^/library$`)
|
||||||
|
|
||||||
|
func BuildLibrary(projectSlug string) string {
|
||||||
|
return ProjectUrl("/library", nil, projectSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexLibraryAll = regexp.MustCompile(`^/library/all$`)
|
||||||
|
|
||||||
|
func BuildLibraryAll(projectSlug string) string {
|
||||||
|
return ProjectUrl("/library/all", nil, projectSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexLibraryTopic = regexp.MustCompile(`^/library/topic/(?P<topicid>\d+)$`)
|
||||||
|
|
||||||
|
func BuildLibraryTopic(projectSlug string, topicId int) string {
|
||||||
|
if topicId < 1 {
|
||||||
|
panic(oops.New(nil, "Invalid library topic ID (%d), must be >= 1", topicId))
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder strings.Builder
|
||||||
|
builder.WriteString("/library/topic/")
|
||||||
|
builder.WriteString(strconv.Itoa(topicId))
|
||||||
|
|
||||||
|
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexLibraryResource = regexp.MustCompile(`^/library/resource/(?P<resourceid>\d+)$`)
|
||||||
|
|
||||||
|
func BuildLibraryResource(projectSlug string, resourceId int) string {
|
||||||
|
builder := buildLibraryResourcePath(resourceId)
|
||||||
|
|
||||||
|
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 {
|
||||||
|
builder := buildLibraryDiscussionPath(resourceId, threadId, page)
|
||||||
|
|
||||||
|
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildLibraryDiscussionWithPostHash(projectSlug string, resourceId int, threadId int, page int, postId int) string {
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
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 {
|
||||||
|
builder := buildLibraryPostPath(resourceId, threadId, postId)
|
||||||
|
builder.WriteString("/quote")
|
||||||
|
|
||||||
|
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Assets
|
||||||
|
*/
|
||||||
|
|
||||||
var RegexProjectCSS = regexp.MustCompile("^/assets/project.css$")
|
var RegexProjectCSS = regexp.MustCompile("^/assets/project.css$")
|
||||||
|
|
||||||
func BuildProjectCSS(color string) string {
|
func BuildProjectCSS(color string) string {
|
||||||
|
@ -218,4 +589,219 @@ func BuildPublic(filepath string) string {
|
||||||
return Url(builder.String(), nil)
|
return Url(builder.String(), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Other
|
||||||
|
*/
|
||||||
|
|
||||||
|
var RegexMarkRead = regexp.MustCompile(`^/_markread/(?P<catid>\d+)$`)
|
||||||
|
|
||||||
|
// NOTE(asaf): categoryId == 0 means ALL CATEGORIES
|
||||||
|
func BuildMarkRead(categoryId int) string {
|
||||||
|
if categoryId < 0 {
|
||||||
|
panic(oops.New(nil, "Invalid category ID (%d), must be >= 0", categoryId))
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder strings.Builder
|
||||||
|
builder.WriteString("/_markread/")
|
||||||
|
builder.WriteString(strconv.Itoa(categoryId))
|
||||||
|
|
||||||
|
return Url(builder.String(), nil)
|
||||||
|
}
|
||||||
|
|
||||||
var RegexCatchAll = regexp.MustCompile("")
|
var RegexCatchAll = regexp.MustCompile("")
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Helper functions
|
||||||
|
*/
|
||||||
|
|
||||||
|
func buildForumCategoryPath(subforums []string) *strings.Builder {
|
||||||
|
for _, subforum := range subforums {
|
||||||
|
if strings.Contains(subforum, "/") {
|
||||||
|
panic(oops.New(nil, "Tried building forum url with / in subforum name"))
|
||||||
|
}
|
||||||
|
subforum = strings.TrimSpace(subforum)
|
||||||
|
if len(subforum) == 0 {
|
||||||
|
panic(oops.New(nil, "Tried building forum url with blank subforum"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder strings.Builder
|
||||||
|
builder.WriteString("/forums")
|
||||||
|
for _, subforum := range subforums {
|
||||||
|
builder.WriteRune('/')
|
||||||
|
builder.WriteString(subforum)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &builder
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildForumThreadPath(subforums []string, threadId int, title string, page int) *strings.Builder {
|
||||||
|
if page < 1 {
|
||||||
|
panic(oops.New(nil, "Invalid forum thread page (%d), must be >= 1", page))
|
||||||
|
}
|
||||||
|
|
||||||
|
if threadId < 1 {
|
||||||
|
panic(oops.New(nil, "Invalid forum thread ID (%d), must be >= 1", threadId))
|
||||||
|
}
|
||||||
|
|
||||||
|
builder := buildForumCategoryPath(subforums)
|
||||||
|
|
||||||
|
builder.WriteString("/t/")
|
||||||
|
builder.WriteString(strconv.Itoa(threadId))
|
||||||
|
if len(title) > 0 {
|
||||||
|
builder.WriteRune('-')
|
||||||
|
builder.WriteString(PathSafeTitle(title))
|
||||||
|
}
|
||||||
|
if page > 1 {
|
||||||
|
builder.WriteRune('/')
|
||||||
|
builder.WriteString(strconv.Itoa(page))
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildForumPostPath(subforums []string, threadId int, postId int) *strings.Builder {
|
||||||
|
if threadId < 1 {
|
||||||
|
panic(oops.New(nil, "Invalid forum thread ID (%d), must be >= 1", threadId))
|
||||||
|
}
|
||||||
|
|
||||||
|
if postId < 1 {
|
||||||
|
panic(oops.New(nil, "Invalid forum post ID (%d), must be >= 1", postId))
|
||||||
|
}
|
||||||
|
|
||||||
|
builder := buildForumCategoryPath(subforums)
|
||||||
|
|
||||||
|
builder.WriteString("/t/")
|
||||||
|
builder.WriteString(strconv.Itoa(threadId))
|
||||||
|
builder.WriteString("/p/")
|
||||||
|
builder.WriteString(strconv.Itoa(postId))
|
||||||
|
|
||||||
|
return builder
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildBlogThreadPath(threadId int, title string, page int) *strings.Builder {
|
||||||
|
if page < 1 {
|
||||||
|
panic(oops.New(nil, "Invalid blog thread page (%d), must be >= 1", page))
|
||||||
|
}
|
||||||
|
|
||||||
|
if threadId < 1 {
|
||||||
|
panic(oops.New(nil, "Invalid blog thread ID (%d), must be >= 1", threadId))
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
builder.WriteString("/blog/p/")
|
||||||
|
builder.WriteString(strconv.Itoa(threadId))
|
||||||
|
if len(title) > 0 {
|
||||||
|
builder.WriteRune('-')
|
||||||
|
builder.WriteString(PathSafeTitle(title))
|
||||||
|
}
|
||||||
|
if page > 1 {
|
||||||
|
builder.WriteRune('/')
|
||||||
|
builder.WriteString(strconv.Itoa(page))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &builder
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildBlogPostPath(threadId int, postId int) *strings.Builder {
|
||||||
|
if threadId < 1 {
|
||||||
|
panic(oops.New(nil, "Invalid blog thread ID (%d), must be >= 1", threadId))
|
||||||
|
}
|
||||||
|
|
||||||
|
if postId < 1 {
|
||||||
|
panic(oops.New(nil, "Invalid blog post ID (%d), must be >= 1", postId))
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder strings.Builder
|
||||||
|
|
||||||
|
builder.WriteString("/blog/p/")
|
||||||
|
builder.WriteString(strconv.Itoa(threadId))
|
||||||
|
builder.WriteString("/e/")
|
||||||
|
builder.WriteString(strconv.Itoa(postId))
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder strings.Builder
|
||||||
|
builder.WriteString("/library/resource/")
|
||||||
|
builder.WriteString(strconv.Itoa(resourceId))
|
||||||
|
|
||||||
|
return &builder
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildLibraryDiscussionPath(resourceId int, threadId int, page int) *strings.Builder {
|
||||||
|
if page < 1 {
|
||||||
|
panic(oops.New(nil, "Invalid page number (%d), must be >= 1", page))
|
||||||
|
}
|
||||||
|
if threadId < 1 {
|
||||||
|
panic(oops.New(nil, "Invalid library thread ID (%d), must be >= 1", threadId))
|
||||||
|
}
|
||||||
|
builder := buildLibraryResourcePath(resourceId)
|
||||||
|
builder.WriteString("/d/")
|
||||||
|
builder.WriteString(strconv.Itoa(threadId))
|
||||||
|
if page > 1 {
|
||||||
|
builder.WriteRune('/')
|
||||||
|
builder.WriteString(strconv.Itoa(page))
|
||||||
|
}
|
||||||
|
return builder
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildLibraryPostPath(resourceId int, threadId int, postId int) *strings.Builder {
|
||||||
|
if threadId < 1 {
|
||||||
|
panic(oops.New(nil, "Invalid library thread ID (%d), must be >= 1", threadId))
|
||||||
|
}
|
||||||
|
if postId < 1 {
|
||||||
|
panic(oops.New(nil, "Invalid library post ID (%d), must be >= 1", postId))
|
||||||
|
}
|
||||||
|
builder := buildLibraryResourcePath(resourceId)
|
||||||
|
builder.WriteString("/d/")
|
||||||
|
builder.WriteString(strconv.Itoa(threadId))
|
||||||
|
builder.WriteString("/p/")
|
||||||
|
builder.WriteString(strconv.Itoa(postId))
|
||||||
|
return builder
|
||||||
|
}
|
||||||
|
|
||||||
|
var PathCharsToClear = regexp.MustCompile("[$&`<>{}()\\[\\]\"+#%@;=?\\\\^|~‘]")
|
||||||
|
var PathCharsToReplace = regexp.MustCompile("[ :/\\\\]")
|
||||||
|
|
||||||
|
func PathSafeTitle(title string) string {
|
||||||
|
title = strings.ToLower(title)
|
||||||
|
title = PathCharsToReplace.ReplaceAllLiteralString(title, "_")
|
||||||
|
title = PathCharsToClear.ReplaceAllLiteralString(title, "")
|
||||||
|
title = url.PathEscape(title)
|
||||||
|
return title
|
||||||
|
}
|
||||||
|
|
|
@ -38,7 +38,8 @@ type CategoryTree map[int]*CategoryTreeNode
|
||||||
|
|
||||||
type CategoryTreeNode struct {
|
type CategoryTreeNode struct {
|
||||||
Category
|
Category
|
||||||
Parent *CategoryTreeNode
|
Parent *CategoryTreeNode
|
||||||
|
Children []*CategoryTreeNode
|
||||||
}
|
}
|
||||||
|
|
||||||
func (node *CategoryTreeNode) GetLineage() []*Category {
|
func (node *CategoryTreeNode) GetLineage() []*Category {
|
||||||
|
@ -84,6 +85,15 @@ func GetFullCategoryTree(ctx context.Context, conn *pgxpool.Pool) CategoryTree {
|
||||||
node.Parent = catTreeMap[*node.ParentID]
|
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
|
return catTreeMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,6 +119,10 @@ func (cl *CategoryLineageBuilder) GetLineage(catId int) []*Category {
|
||||||
return cl.CategoryCache[catId]
|
return cl.CategoryCache[catId]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cl *CategoryLineageBuilder) GetSubforumLineage(catId int) []*Category {
|
||||||
|
return cl.GetLineage(catId)[1:]
|
||||||
|
}
|
||||||
|
|
||||||
func (cl *CategoryLineageBuilder) GetLineageSlugs(catId int) []string {
|
func (cl *CategoryLineageBuilder) GetLineageSlugs(catId int) []string {
|
||||||
_, ok := cl.SlugCache[catId]
|
_, ok := cl.SlugCache[catId]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -125,3 +139,16 @@ func (cl *CategoryLineageBuilder) GetLineageSlugs(catId int) []string {
|
||||||
}
|
}
|
||||||
return cl.SlugCache[catId]
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
type LibraryResource struct {
|
||||||
|
ID int `db:"id"`
|
||||||
|
|
||||||
|
CategoryID int `db:"category_id"`
|
||||||
|
ProjectID *int `db:"project_id"`
|
||||||
|
|
||||||
|
Name string `db:"name"`
|
||||||
|
Description string `db:"description"`
|
||||||
|
Url string `db:"url"`
|
||||||
|
ContentType string `db:"content_type"`
|
||||||
|
Size int `db:"size"`
|
||||||
|
IsDeleted bool `db:"is_deleted"`
|
||||||
|
PreventsEmbed bool `db:"prevents_embed"`
|
||||||
|
}
|
|
@ -27,12 +27,14 @@ const (
|
||||||
type Project struct {
|
type Project struct {
|
||||||
ID int `db:"id"`
|
ID int `db:"id"`
|
||||||
|
|
||||||
|
ForumID *int `db:"forum_id"`
|
||||||
|
|
||||||
Slug string `db:"slug"`
|
Slug string `db:"slug"`
|
||||||
Name string `db:"name"`
|
Name string `db:"name"`
|
||||||
Blurb string `db:"blurb"`
|
Blurb string `db:"blurb"`
|
||||||
Description string `db:"description"`
|
Description string `db:"description"`
|
||||||
|
|
||||||
Lifecycle ProjectLifecycle `db:"lifecycle"`
|
Lifecycle ProjectLifecycle `db:"lifecycle"` // TODO(asaf): Ensure we only fetch projects in the correct lifecycle phase everywhere.
|
||||||
|
|
||||||
Color1 string `db:"color_1"`
|
Color1 string `db:"color_1"`
|
||||||
Color2 string `db:"color_2"`
|
Color2 string `db:"color_2"`
|
||||||
|
|
|
@ -57,6 +57,7 @@ func ProjectToTemplate(p *models.Project) Project {
|
||||||
Subdomain: p.Subdomain(),
|
Subdomain: p.Subdomain(),
|
||||||
Color1: p.Color1,
|
Color1: p.Color1,
|
||||||
Color2: p.Color2,
|
Color2: p.Color2,
|
||||||
|
Url: hmnurl.BuildProjectHomepage(p.Slug),
|
||||||
|
|
||||||
IsHMN: p.IsHMN(),
|
IsHMN: p.IsHMN(),
|
||||||
|
|
||||||
|
|
|
@ -10,9 +10,9 @@
|
||||||
<div class="content-block">
|
<div class="content-block">
|
||||||
<div class="optionbar">
|
<div class="optionbar">
|
||||||
<div class="options">
|
<div class="options">
|
||||||
<a class="button" href="{{ url "/atom" }}"><span class="icon big">4</span> RSS Feed</span></a>
|
<a class="button" href="{{ .AtomFeedUrl }}"><span class="icon big">4</span> RSS Feed</span></a>
|
||||||
{{ if .User }}
|
{{ if .User }}
|
||||||
<a class="button" href="{{ url "/markread" }}"><span class="big">✓</span> Mark all posts on site as read</a>
|
<a class="button" href="{{ .MarkAllReadUrl }}"><span class="big">✓</span> Mark all posts on site as read</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
<div class="options">
|
<div class="options">
|
||||||
|
|
|
@ -35,11 +35,11 @@
|
||||||
{{ define "forum_category_options" }}
|
{{ define "forum_category_options" }}
|
||||||
<div class="options">
|
<div class="options">
|
||||||
{{ if .User }}
|
{{ if .User }}
|
||||||
<a class="button new-thread" href="{{ printf "%s/t/new" .CategoryUrl }}"><span class="big">+</span> New Thread</a>
|
<a class="button new-thread" href="{{ .NewThreadUrl }}"><span class="big">+</span> New Thread</a>
|
||||||
{{/* TODO(asaf): Mark read should probably be a POST, since it's destructive and we would probably want CSRF for it */}}
|
{{/* TODO(asaf): Mark read should probably be a POST, since it's destructive and we would probably want CSRF for it */}}
|
||||||
<a class="button" href="{{ printf "%s/markread" .CategoryUrl }}"><span class="big">✓</span> Mark threads here as read</a>
|
<a class="button" href="{{ .MarkReadUrl }}"><span class="big">✓</span> Mark threads here as read</a>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<a class="button" href="{% url 'member_login' subdomain=request.subdomain %}">Log in to post a new thread</a>
|
<a class="button" href="{{ .LoginPageUrl }}">Log in to post a new thread</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
<div class="options">
|
<div class="options">
|
||||||
|
|
|
@ -120,7 +120,7 @@
|
||||||
{{ else if .User }}
|
{{ else if .User }}
|
||||||
<a class="button" href="{{ .ReplyUrl }}">⤷ Reply to Thread</a>
|
<a class="button" href="{{ .ReplyUrl }}">⤷ Reply to Thread</a>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<span><a href="{% url 'member_login' subdomain=None %}">Log in</a> to reply</span>
|
<span><a href="{{ .LoginPageUrl }}">Log in</a> to reply</span>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
<div class="options order-0 order-last-ns">
|
<div class="options order-0 order-last-ns">
|
||||||
|
|
|
@ -1,35 +1,35 @@
|
||||||
<footer class="pa3 pa4-l">
|
<footer class="pa3 pa4-l">
|
||||||
<h2>
|
<h2>
|
||||||
Community by <a href="{{ url "/" }}">handmade.network</a>
|
Community by <a href="{{ .Footer.HomepageUrl }}">handmade.network</a>
|
||||||
</h2>
|
</h2>
|
||||||
<ul class="list">
|
<ul class="list">
|
||||||
{{ $footerClasses := "ma0 pa0 dib-ns" }}
|
{{ $footerClasses := "ma0 pa0 dib-ns" }}
|
||||||
<li class="{{ $footerClasses }}">
|
<li class="{{ $footerClasses }}">
|
||||||
<a href="{{ url "/" }}">Main Page</a>
|
<a href="{{ .Footer.HomepageUrl }}">Main Page</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="{{ $footerClasses }}">
|
<li class="{{ $footerClasses }}">
|
||||||
<a href="{{ url "/about" }}">About</a>
|
<a href="{{ .Footer.AboutUrl }}">About</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="{{ $footerClasses }}">
|
<li class="{{ $footerClasses }}">
|
||||||
<a href="{{ url "/manifesto" }}">Handmade Manifesto</a>
|
<a href="{{ .Footer.ManifestoUrl }}">Handmade Manifesto</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="{{ $footerClasses }}">
|
<li class="{{ $footerClasses }}">
|
||||||
<a href="{{ url "/code-of-conduct" }}">Code of Conduct</a>
|
<a href="{{ .Footer.CodeOfConductUrl }}">Code of Conduct</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="{{ $footerClasses }}">
|
<li class="{{ $footerClasses }}">
|
||||||
<a href="{{ url "/communication-guidelines" }}">Communication Guidelines</a>
|
<a href="{{ .Footer.CommunicationGuidelinesUrl }}">Communication Guidelines</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="{{ $footerClasses }}">
|
<li class="{{ $footerClasses }}">
|
||||||
<a href="{{ url "/projects" }}">Project Index</a>
|
<a href="{{ .Footer.ProjectIndexUrl }}">Project Index</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="{{ $footerClasses }}">
|
<li class="{{ $footerClasses }}">
|
||||||
<a href="{{ url "/forums" }}">Community Forums</a>
|
<a href="{{ .Footer.ForumsUrl }}">Community Forums</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="{{ $footerClasses }}">
|
<li class="{{ $footerClasses }}">
|
||||||
<a href="{{ url "/contact" }}">Contact</a>
|
<a href="{{ .Footer.ContactUrl }}">Contact</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="{{ $footerClasses }}">
|
<li class="{{ $footerClasses }}">
|
||||||
<a href="{{ url "/sitemap" }}">Sitemap</a>
|
<a href="{{ .Footer.SitemapUrl }}">Sitemap</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
@ -3,15 +3,15 @@
|
||||||
<div class="user-options flex justify-center justify-end-ns">
|
<div class="user-options flex justify-center justify-end-ns">
|
||||||
{{ if .User }}
|
{{ if .User }}
|
||||||
{{ if .User.IsSuperuser }}
|
{{ if .User.IsSuperuser }}
|
||||||
<a class="admin-panel" href="{{ url "/admin_panel" }}"><span class="icon-settings"> Admin</span></a>
|
<a class="admin-panel" href="{{ .Header.AdminUrl }}"><span class="icon-settings"> Admin</span></a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<a class="username settings" href="{{ url "/member_settings" }}"><span class="icon-settings"></span> {{ .User.Username }}</a>
|
<a class="username settings" href="{{ .Header.MemberSettingsUrl }}"><span class="icon-settings"></span> {{ .User.Username }}</a>
|
||||||
<a class="logout" href="{{ url "/logout" }}"><span class="icon-logout"></span> Logout</a>
|
<a class="logout" href="{{ .Header.LogoutActionUrl }}"><span class="icon-logout"></span> Logout</a>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<a class="register" id="register-link" href="{{ url "/member_register" }}">Register</a>
|
<a class="register" id="register-link" href="{{ .Header.RegisterUrl }}">Register</a>
|
||||||
<a class="login" id="login-link" href="{{ projecturl "/login" .Project }}">Log in</a>
|
<a class="login" id="login-link" href="{{ .LoginPageUrl }}">Log in</a>
|
||||||
<div id="login-popup">
|
<div id="login-popup">
|
||||||
<form action="{{ projecturl "/login" .Project }}" method="post">
|
<form action="{{ .Header.LoginActionUrl }}" method="post">
|
||||||
{{/* TODO: CSRF */}}
|
{{/* TODO: CSRF */}}
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -33,41 +33,41 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-bar flex flex-column flex-row-l justify-between {% if project and project.slug != 'hmn' %}project{% endif %}">
|
<div class="menu-bar flex flex-column flex-row-l justify-between {% if project and project.slug != 'hmn' %}project{% endif %}">
|
||||||
<div class="flex flex-column flex-row-ns">
|
<div class="flex flex-column flex-row-ns">
|
||||||
<a href="{{ url "/" }}" class="logo hmdev-logo">
|
<a href="{{ .Header.HMNHomepageUrl }}" class="logo hmdev-logo">
|
||||||
<div class="underscore"></div>
|
<div class="underscore"></div>
|
||||||
</a>
|
</a>
|
||||||
<div class="items flex items-center justify-center justify-start-ns">
|
<div class="items flex items-center justify-center justify-start-ns">
|
||||||
{{ if not .Project.IsHMN }}
|
{{ if not .Project.IsHMN }}
|
||||||
<a class="project-logo" href="{{ projecturl "/" .Project }}">
|
<a class="project-logo" href="{{ .Header.ProjectHomepageUrl }}">
|
||||||
<h1>{{ .Project.Name }}</h1>
|
<h1>{{ .Project.Name }}</h1>
|
||||||
</a>
|
</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ if .Project.HasBlog }}
|
{{ if .Project.HasBlog }}
|
||||||
<a href="{{ projecturl "/blog" .Project }}" class="blog">Blog</a>
|
<a href="{{ .Header.BlogUrl }}" class="blog">Blog</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ if .Project.HasForum }}
|
{{ if .Project.HasForum }}
|
||||||
<a href="{{ projecturl "/forums" .Project }}" class="forums">Forums</a>
|
<a href="{{ .Header.ForumsUrl }}" class="forums">Forums</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ if .Project.HasWiki }}
|
{{ if .Project.HasWiki }}
|
||||||
<a href="{{ projecturl "/wiki" .Project }}" class="wiki">Wiki</a>
|
<a href="{{ .Header.WikiUrl }}" class="wiki">Wiki</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ if .Project.HasLibrary }}
|
{{ if .Project.HasLibrary }}
|
||||||
<a href="{{ projecturl "/library" .Project }}" class="library">Library</a>
|
<a href="{{ .Header.LibraryUrl }}" class="library">Library</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ if .Project.IsHMN }}
|
{{ if .Project.IsHMN }}
|
||||||
<a href="{{ projecturl "/manifesto" .Project }}" class="misson">Mission</a>
|
<a href="{{ .Header.ManifestoUrl }}" class="misson">Mission</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{/* {% if project.default_annotation_category %} */}}
|
{{/* {% if project.default_annotation_category %} */}}
|
||||||
{{ if false }}
|
{{ if false }}
|
||||||
<a href="{% url 'episode_list_topic' project.slug project.default_annotation_category %}" class="annotations">Episode Guide</a>
|
<a href="{{ .Header.EpisodeGuideUrl }}" class="annotations">Episode Guide</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{/* {% if showEditLink == True %} */}}
|
{{/* {% if showEditLink == True %} */}}
|
||||||
{{/* {{ if false }}
|
{{/* {{ if false }}
|
||||||
<a class="edit" href="{{ EditLink }}" title="Edit {{ project.name }}"><span class="icon">0</span> Settings</a>
|
<a class="edit" href="{{ .Header.EditUrl }}" title="Edit {{ project.name }}"><span class="icon">0</span> Settings</a>
|
||||||
{{ end }} */}}
|
{{ end }} */}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form class="dn ma0 flex-l flex-column justify-center items-end" method="post" action="{{ url "/search" }}" target="_blank">
|
<form class="dn ma0 flex-l flex-column justify-center items-end" method="post" action="{{ .Header.SearchActionUrl }}" target="_blank">
|
||||||
{{/* TODO: CSRF? */}}
|
{{/* TODO: CSRF? */}}
|
||||||
<input class="site-search bn lite pa2 fira" type="text" name="term" value="" placeholder="Search with Google" size="17" />
|
<input class="site-search bn lite pa2 fira" type="text" name="term" value="" placeholder="Search with Google" size="17" />
|
||||||
<input id="search_button_homepage" type="submit" value="Go"/>
|
<input id="search_button_homepage" type="submit" value="Go"/>
|
||||||
|
|
|
@ -35,25 +35,25 @@
|
||||||
<h2 class="di-l mr2-l">Around the Network</h2>
|
<h2 class="di-l mr2-l">Around the Network</h2>
|
||||||
<ul class="list dib-l">
|
<ul class="list dib-l">
|
||||||
<li class="dib-ns ma0 ph2">
|
<li class="dib-ns ma0 ph2">
|
||||||
<a href="{{ url "/feed" }}">View all posts on HMN</a>
|
<a href="{{ .FeedUrl }}">View all posts on HMN</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="dib-ns ma0 ph2">
|
<li class="dib-ns ma0 ph2">
|
||||||
<a href="{{ url "/podcast" }}">Podcast</a>
|
<a href="{{ .PodcastUrl }}">Podcast</a>
|
||||||
</li>
|
</li>
|
||||||
{{/* TODO: Make a better IRC intro page because the current one is trash anyway */}}
|
{{/* TODO: Make a better IRC intro page because the current one is trash anyway */}}
|
||||||
{{/*
|
{{/*
|
||||||
<li class="dib-ns ma0 ph2">
|
<li class="dib-ns ma0 ph2">
|
||||||
<a href="{{ url "/streams" }}">See who's live</a>
|
<a href="{{ .StreamsUrl }}">See who's live</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="dib-ns ma0 ph2">
|
<li class="dib-ns ma0 ph2">
|
||||||
<a href="/blogs/p/1138-%5Btutorial%5D_handmade_network_irc" target="_blank">Chat in IRC</a>
|
<a href="{{ .IRCUrl }}" target="_blank">Chat in IRC</a>
|
||||||
</li>
|
</li>
|
||||||
*/}}
|
*/}}
|
||||||
<li class="dib-ns ma0 ph2">
|
<li class="dib-ns ma0 ph2">
|
||||||
<a href="https://discord.gg/hxWxDee" target="_blank">Chat on Discord</a>
|
<a href="{{ .DiscordUrl }}" target="_blank">Chat on Discord</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="dib-ns ma0 ph2">
|
<li class="dib-ns ma0 ph2">
|
||||||
<a href="https://handmadedev.show/" target="_blank">See the Show</a>
|
<a href="{{ .ShowUrl }}" target="_blank">See the Show</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -78,7 +78,7 @@
|
||||||
{{ $c1 := hex2color $proj.Color1 }}
|
{{ $c1 := hex2color $proj.Color1 }}
|
||||||
<a
|
<a
|
||||||
class="project-title"
|
class="project-title"
|
||||||
href="{{ projecturl "/" $proj }}"
|
href="{{ $proj.Url }}"
|
||||||
>
|
>
|
||||||
<h2 class="ph3">{{ $proj.Name }}</h2>
|
<h2 class="ph3">{{ $proj.Name }}</h2>
|
||||||
</a>
|
</a>
|
||||||
|
@ -92,7 +92,7 @@
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
<div class="ph3 thread unread more">
|
<div class="ph3 thread unread more">
|
||||||
<a class="title" href="{{ projecturl "/forums" $proj }}">
|
<a class="title" href="{{ $entry.ForumsUrl }}">
|
||||||
More posts →
|
More posts →
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -113,7 +113,7 @@
|
||||||
<h2 class="di-l mr2-l">Community Showcase</h2>
|
<h2 class="di-l mr2-l">Community Showcase</h2>
|
||||||
<ul class="list dib-l">
|
<ul class="list dib-l">
|
||||||
<li class="dib-ns ma0 ph2">
|
<li class="dib-ns ma0 ph2">
|
||||||
<a href="{% url 'showcase' %}">View all</a>
|
<a href="{{ .ShowcaseUrl }}">View all</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -129,7 +129,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="c--dimmer i pv2 ph3 ph0-ns">
|
<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="https://discord.gg/hxWxDee" target="_blank">Join us on Discord.</a>
|
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>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
</style>
|
</style>
|
||||||
{{ block "extrahead" . }}{{ end }}
|
{{ block "extrahead" . }}{{ end }}
|
||||||
<link rel="stylesheet" href="{{ statictheme .Theme "theme.css" }}" />
|
<link rel="stylesheet" href="{{ statictheme .Theme "theme.css" }}" />
|
||||||
<link rel="stylesheet" href="{{ urlq "assets/project.css" (query "color" .Project.Color1) }}" />
|
<link rel="stylesheet" href="{{ .ProjectCSSUrl }}" />
|
||||||
<link rel="apple-touch-icon" sizes="57x57" href="{{ static "apple-icon-57x57.png" }}">
|
<link rel="apple-touch-icon" sizes="57x57" href="{{ static "apple-icon-57x57.png" }}">
|
||||||
<link rel="apple-touch-icon" sizes="60x60" href="{{ static "apple-icon-60x60.png" }}">
|
<link rel="apple-touch-icon" sizes="60x60" href="{{ static "apple-icon-60x60.png" }}">
|
||||||
<link rel="apple-touch-icon" sizes="72x72" href="{{ static "apple-icon-72x72.png" }}">
|
<link rel="apple-touch-icon" sizes="72x72" href="{{ static "apple-icon-72x72.png" }}">
|
||||||
|
|
|
@ -14,8 +14,44 @@ type BaseData struct {
|
||||||
BodyClasses []string
|
BodyClasses []string
|
||||||
Breadcrumbs []Breadcrumb
|
Breadcrumbs []Breadcrumb
|
||||||
|
|
||||||
|
LoginPageUrl string
|
||||||
|
ProjectCSSUrl string
|
||||||
|
|
||||||
Project Project
|
Project Project
|
||||||
User *User
|
User *User
|
||||||
|
|
||||||
|
Header Header
|
||||||
|
Footer Footer
|
||||||
|
}
|
||||||
|
|
||||||
|
type Header struct {
|
||||||
|
AdminUrl string
|
||||||
|
MemberSettingsUrl string
|
||||||
|
LoginActionUrl string
|
||||||
|
LogoutActionUrl string
|
||||||
|
RegisterUrl string
|
||||||
|
HMNHomepageUrl string
|
||||||
|
ProjectHomepageUrl string
|
||||||
|
BlogUrl string
|
||||||
|
ForumsUrl string
|
||||||
|
WikiUrl string
|
||||||
|
LibraryUrl string
|
||||||
|
ManifestoUrl string
|
||||||
|
EpisodeGuideUrl string
|
||||||
|
EditUrl string
|
||||||
|
SearchActionUrl string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Footer struct {
|
||||||
|
HomepageUrl string
|
||||||
|
AboutUrl string
|
||||||
|
ManifestoUrl string
|
||||||
|
CodeOfConductUrl string
|
||||||
|
CommunicationGuidelinesUrl string
|
||||||
|
ProjectIndexUrl string
|
||||||
|
ForumsUrl string
|
||||||
|
ContactUrl string
|
||||||
|
SitemapUrl string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Thread struct {
|
type Thread struct {
|
||||||
|
@ -54,6 +90,7 @@ type Project struct {
|
||||||
Subdomain string
|
Subdomain string
|
||||||
Color1 string
|
Color1 string
|
||||||
Color2 string
|
Color2 string
|
||||||
|
Url string
|
||||||
|
|
||||||
IsHMN bool
|
IsHMN bool
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
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",
|
||||||
|
}
|
|
@ -17,8 +17,10 @@ import (
|
||||||
type FeedData struct {
|
type FeedData struct {
|
||||||
templates.BaseData
|
templates.BaseData
|
||||||
|
|
||||||
Posts []templates.PostListItem
|
Posts []templates.PostListItem
|
||||||
Pagination templates.Pagination
|
Pagination templates.Pagination
|
||||||
|
AtomFeedUrl string
|
||||||
|
MarkAllReadUrl string
|
||||||
}
|
}
|
||||||
|
|
||||||
func Feed(c *RequestContext) ResponseData {
|
func Feed(c *RequestContext) ResponseData {
|
||||||
|
@ -76,13 +78,14 @@ func Feed(c *RequestContext) ResponseData {
|
||||||
|
|
||||||
c.Perf.StartBlock("SQL", "Fetch posts")
|
c.Perf.StartBlock("SQL", "Fetch posts")
|
||||||
type feedPostQuery struct {
|
type feedPostQuery struct {
|
||||||
Post models.Post `db:"post"`
|
Post models.Post `db:"post"`
|
||||||
Thread models.Thread `db:"thread"`
|
Thread models.Thread `db:"thread"`
|
||||||
Cat models.Category `db:"cat"`
|
Cat models.Category `db:"cat"`
|
||||||
Proj models.Project `db:"proj"`
|
Proj models.Project `db:"proj"`
|
||||||
User models.User `db:"auth_user"`
|
LibraryResource *models.LibraryResource `db:"lib_resource"`
|
||||||
ThreadLastReadTime *time.Time `db:"tlri.lastread"`
|
User models.User `db:"auth_user"`
|
||||||
CatLastReadTime *time.Time `db:"clri.lastread"`
|
ThreadLastReadTime *time.Time `db:"tlri.lastread"`
|
||||||
|
CatLastReadTime *time.Time `db:"clri.lastread"`
|
||||||
}
|
}
|
||||||
posts, err := db.Query(c.Context(), c.Conn, feedPostQuery{},
|
posts, err := db.Query(c.Context(), c.Conn, feedPostQuery{},
|
||||||
`
|
`
|
||||||
|
@ -92,15 +95,16 @@ func Feed(c *RequestContext) ResponseData {
|
||||||
JOIN handmade_thread AS thread ON thread.id = post.thread_id
|
JOIN handmade_thread AS thread ON thread.id = post.thread_id
|
||||||
JOIN handmade_category AS cat ON cat.id = post.category_id
|
JOIN handmade_category AS cat ON cat.id = post.category_id
|
||||||
JOIN handmade_project AS proj ON proj.id = post.project_id
|
JOIN handmade_project AS proj ON proj.id = post.project_id
|
||||||
LEFT OUTER JOIN handmade_threadlastreadinfo AS tlri ON (
|
LEFT JOIN handmade_threadlastreadinfo AS tlri ON (
|
||||||
tlri.thread_id = post.thread_id
|
tlri.thread_id = post.thread_id
|
||||||
AND tlri.user_id = $1
|
AND tlri.user_id = $1
|
||||||
)
|
)
|
||||||
LEFT OUTER JOIN handmade_categorylastreadinfo AS clri ON (
|
LEFT JOIN handmade_categorylastreadinfo AS clri ON (
|
||||||
clri.category_id = post.category_id
|
clri.category_id = post.category_id
|
||||||
AND clri.user_id = $1
|
AND clri.user_id = $1
|
||||||
)
|
)
|
||||||
LEFT OUTER JOIN auth_user ON post.author_id = auth_user.id
|
LEFT JOIN auth_user ON post.author_id = auth_user.id
|
||||||
|
LEFT JOIN handmade_libraryresource as lib_resource ON lib_resource.category_id = post.category_id
|
||||||
WHERE
|
WHERE
|
||||||
post.category_kind = ANY ($2)
|
post.category_kind = ANY ($2)
|
||||||
AND post.deleted = FALSE
|
AND post.deleted = FALSE
|
||||||
|
@ -123,22 +127,6 @@ func Feed(c *RequestContext) ResponseData {
|
||||||
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
||||||
c.Perf.EndBlock()
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
categoryUrlCache := make(map[int]string)
|
|
||||||
getCategoryUrl := func(projectSlug string, cat *models.Category) string {
|
|
||||||
_, ok := categoryUrlCache[cat.ID]
|
|
||||||
if !ok {
|
|
||||||
lineageNames := lineageBuilder.GetLineageSlugs(cat.ID)
|
|
||||||
switch cat.Kind {
|
|
||||||
case models.CatKindForum:
|
|
||||||
categoryUrlCache[cat.ID] = hmnurl.BuildForumCategory(projectSlug, lineageNames[1:], 1)
|
|
||||||
// TODO(asaf): Add more kinds!!!
|
|
||||||
default:
|
|
||||||
categoryUrlCache[cat.ID] = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return categoryUrlCache[cat.ID]
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Perf.StartBlock("FEED", "Build post items")
|
c.Perf.StartBlock("FEED", "Build post items")
|
||||||
var postItems []templates.PostListItem
|
var postItems []templates.PostListItem
|
||||||
for _, iPostResult := range posts.ToSlice() {
|
for _, iPostResult := range posts.ToSlice() {
|
||||||
|
@ -151,42 +139,16 @@ func Feed(c *RequestContext) ResponseData {
|
||||||
hasRead = true
|
hasRead = true
|
||||||
}
|
}
|
||||||
|
|
||||||
breadcrumbs := make([]templates.Breadcrumb, 0, len(lineageBuilder.GetLineage(postResult.Cat.ID)))
|
postItems = append(postItems, MakePostListItem(
|
||||||
breadcrumbs = append(breadcrumbs, templates.Breadcrumb{
|
lineageBuilder,
|
||||||
Name: postResult.Proj.Name,
|
&postResult.Proj,
|
||||||
Url: hmnurl.ProjectUrl("/", nil, postResult.Proj.Slug),
|
&postResult.Thread,
|
||||||
})
|
&postResult.Post,
|
||||||
if postResult.Post.CategoryKind == models.CatKindLibraryResource {
|
&postResult.User,
|
||||||
// TODO(asaf): Fetch library root topic for the project and construct breadcrumb for it
|
postResult.LibraryResource,
|
||||||
} else {
|
!hasRead,
|
||||||
lineage := lineageBuilder.GetLineage(postResult.Cat.ID)
|
true,
|
||||||
for i, cat := range lineage {
|
))
|
||||||
name := *cat.Name
|
|
||||||
if i == 0 {
|
|
||||||
switch cat.Kind {
|
|
||||||
case models.CatKindForum:
|
|
||||||
name = "Forums"
|
|
||||||
case models.CatKindBlog:
|
|
||||||
name = "Blog"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
breadcrumbs = append(breadcrumbs, templates.Breadcrumb{
|
|
||||||
Name: name,
|
|
||||||
Url: getCategoryUrl(postResult.Proj.Subdomain(), cat),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
postItems = append(postItems, templates.PostListItem{
|
|
||||||
Title: postResult.Thread.Title,
|
|
||||||
Url: hmnurl.BuildForumPost(postResult.Proj.Subdomain(), lineageBuilder.GetLineageSlugs(postResult.Cat.ID)[1:], postResult.Post.ID, postResult.Post.ThreadID),
|
|
||||||
User: templates.UserToTemplate(&postResult.User),
|
|
||||||
Date: postResult.Post.PostDate,
|
|
||||||
Breadcrumbs: breadcrumbs,
|
|
||||||
Unread: !hasRead,
|
|
||||||
Classes: "post-bg-alternate", // TODO: Should this be the default, and the home page can suppress it?
|
|
||||||
Content: postResult.Post.Preview,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
c.Perf.EndBlock()
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
|
@ -197,8 +159,10 @@ func Feed(c *RequestContext) ResponseData {
|
||||||
res.WriteTemplate("feed.html", FeedData{
|
res.WriteTemplate("feed.html", FeedData{
|
||||||
BaseData: baseData,
|
BaseData: baseData,
|
||||||
|
|
||||||
Posts: postItems,
|
AtomFeedUrl: hmnurl.BuildAtomFeed(),
|
||||||
Pagination: pagination,
|
MarkAllReadUrl: hmnurl.BuildMarkRead(0),
|
||||||
|
Posts: postItems,
|
||||||
|
Pagination: pagination,
|
||||||
}, c.Perf)
|
}, c.Perf)
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
package website
|
package website
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -16,13 +14,13 @@ import (
|
||||||
"git.handmade.network/hmn/hmn/src/oops"
|
"git.handmade.network/hmn/hmn/src/oops"
|
||||||
"git.handmade.network/hmn/hmn/src/templates"
|
"git.handmade.network/hmn/hmn/src/templates"
|
||||||
"git.handmade.network/hmn/hmn/src/utils"
|
"git.handmade.network/hmn/hmn/src/utils"
|
||||||
"github.com/jackc/pgx/v4/pgxpool"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type forumCategoryData struct {
|
type forumCategoryData struct {
|
||||||
templates.BaseData
|
templates.BaseData
|
||||||
|
|
||||||
CategoryUrl string
|
NewThreadUrl string
|
||||||
|
MarkReadUrl string
|
||||||
Threads []templates.ThreadListItem
|
Threads []templates.ThreadListItem
|
||||||
Pagination templates.Pagination
|
Pagination templates.Pagination
|
||||||
Subcategories []forumSubcategoryData
|
Subcategories []forumSubcategoryData
|
||||||
|
@ -38,19 +36,17 @@ type forumSubcategoryData struct {
|
||||||
func ForumCategory(c *RequestContext) ResponseData {
|
func ForumCategory(c *RequestContext) ResponseData {
|
||||||
const threadsPerPage = 25
|
const threadsPerPage = 25
|
||||||
|
|
||||||
// TODO(asaf): Consider making this more robust.
|
c.Perf.StartBlock("SQL", "Fetch category tree")
|
||||||
// Right now this code allows for weird urls like:
|
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
|
||||||
// "/forums/asdf/wip" which doesn't verify the lineage and displays the wip forums
|
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
||||||
// "/forums/wip///" which fetches the main forums page because it happens to have a blank slug
|
c.Perf.EndBlock()
|
||||||
// "/forums/wip/" which fetches the main forums page because Split returns an extra blank string
|
|
||||||
// "/forums/wip/1" this one fetches the wip forums because the regex matches the `/1` as part of the page group
|
currentCatId, valid := validateSubforums(lineageBuilder, c.CurrentProject, c.PathParams["cats"])
|
||||||
// "/forums/wip/1/" 404 - doesn't match the regex
|
if !valid {
|
||||||
// "/forums/" 404 - doesn't match the regex
|
return FourOhFour(c)
|
||||||
catPath := c.PathParams["cats"]
|
}
|
||||||
catSlugs := strings.Split(catPath, "/")
|
|
||||||
currentCatId := fetchCatIdFromSlugs(c.Context(), c.Conn, catSlugs, c.CurrentProject.ID)
|
currentSubforumSlugs := lineageBuilder.GetSubforumLineageSlugs(currentCatId)
|
||||||
// TODO(asaf): 404 if we can't find our cat.
|
|
||||||
categoryUrls := GetProjectCategoryUrls(c.Context(), c.Conn, c.CurrentProject.ID)
|
|
||||||
|
|
||||||
c.Perf.StartBlock("SQL", "Fetch count of page threads")
|
c.Perf.StartBlock("SQL", "Fetch count of page threads")
|
||||||
numThreads, err := db.QueryInt(c.Context(), c.Conn,
|
numThreads, err := db.QueryInt(c.Context(), c.Conn,
|
||||||
|
@ -76,11 +72,11 @@ func ForumCategory(c *RequestContext) ResponseData {
|
||||||
if pageParsed, err := strconv.Atoi(pageString); err == nil {
|
if pageParsed, err := strconv.Atoi(pageString); err == nil {
|
||||||
page = pageParsed
|
page = pageParsed
|
||||||
} else {
|
} else {
|
||||||
return c.Redirect("/feed", http.StatusSeeOther) // TODO
|
return c.Redirect(hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs, 1), http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if page < 1 || numPages < page {
|
if page < 1 || numPages < page {
|
||||||
return c.Redirect("/feed", http.StatusSeeOther) // TODO
|
return c.Redirect(hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs, utils.IntClamp(1, page, numPages)), http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
howManyThreadsToSkip := (page - 1) * threadsPerPage
|
howManyThreadsToSkip := (page - 1) * threadsPerPage
|
||||||
|
@ -143,10 +139,8 @@ func ForumCategory(c *RequestContext) ResponseData {
|
||||||
}
|
}
|
||||||
|
|
||||||
return templates.ThreadListItem{
|
return templates.ThreadListItem{
|
||||||
Title: row.Thread.Title,
|
Title: row.Thread.Title,
|
||||||
// TODO(asaf): Use thread.category_id instead of currentCatId. At the moment this is generating wrong urls for threads in subcats.
|
Url: hmnurl.BuildForumThread(c.CurrentProject.Slug, lineageBuilder.GetSubforumLineageSlugs(row.Thread.CategoryID), row.Thread.ID, row.Thread.Title, 1),
|
||||||
Url: ThreadUrl(row.Thread, models.CatKindForum, categoryUrls[currentCatId]),
|
|
||||||
|
|
||||||
FirstUser: templates.UserToTemplate(row.FirstUser),
|
FirstUser: templates.UserToTemplate(row.FirstUser),
|
||||||
FirstDate: row.FirstPost.PostDate,
|
FirstDate: row.FirstPost.PostDate,
|
||||||
LastUser: templates.UserToTemplate(row.LastUser),
|
LastUser: templates.UserToTemplate(row.LastUser),
|
||||||
|
@ -168,29 +162,9 @@ func ForumCategory(c *RequestContext) ResponseData {
|
||||||
|
|
||||||
var subcats []forumSubcategoryData
|
var subcats []forumSubcategoryData
|
||||||
if page == 1 {
|
if page == 1 {
|
||||||
c.Perf.StartBlock("SQL", "Fetch subcategories")
|
subcatNodes := categoryTree[currentCatId].Children
|
||||||
type subcatQueryResult struct {
|
|
||||||
Cat models.Category `db:"cat"`
|
|
||||||
}
|
|
||||||
itSubcats, err := db.Query(c.Context(), c.Conn, subcatQueryResult{},
|
|
||||||
`
|
|
||||||
SELECT $columns
|
|
||||||
FROM
|
|
||||||
handmade_category AS cat
|
|
||||||
WHERE
|
|
||||||
cat.parent_id = $1
|
|
||||||
`,
|
|
||||||
currentCatId,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
panic(oops.New(err, "failed to fetch subcategories"))
|
|
||||||
}
|
|
||||||
defer itSubcats.Close()
|
|
||||||
c.Perf.EndBlock()
|
|
||||||
|
|
||||||
for _, irow := range itSubcats.ToSlice() {
|
|
||||||
catRow := irow.(*subcatQueryResult)
|
|
||||||
|
|
||||||
|
for _, catNode := range subcatNodes {
|
||||||
c.Perf.StartBlock("SQL", "Fetch count of subcategory threads")
|
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.
|
// TODO(asaf): [PERF] [MINOR] Consider replacing querying count per subcat with a single query for all cats with GROUP BY.
|
||||||
numThreads, err := db.QueryInt(c.Context(), c.Conn,
|
numThreads, err := db.QueryInt(c.Context(), c.Conn,
|
||||||
|
@ -201,7 +175,7 @@ func ForumCategory(c *RequestContext) ResponseData {
|
||||||
thread.category_id = $1
|
thread.category_id = $1
|
||||||
AND NOT thread.deleted
|
AND NOT thread.deleted
|
||||||
`,
|
`,
|
||||||
catRow.Cat.ID,
|
catNode.ID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(oops.New(err, "failed to get count of threads"))
|
panic(oops.New(err, "failed to get count of threads"))
|
||||||
|
@ -233,7 +207,7 @@ func ForumCategory(c *RequestContext) ResponseData {
|
||||||
ORDER BY lastpost.postdate DESC
|
ORDER BY lastpost.postdate DESC
|
||||||
LIMIT 3
|
LIMIT 3
|
||||||
`,
|
`,
|
||||||
catRow.Cat.ID,
|
catNode.ID,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -249,8 +223,8 @@ func ForumCategory(c *RequestContext) ResponseData {
|
||||||
}
|
}
|
||||||
|
|
||||||
subcats = append(subcats, forumSubcategoryData{
|
subcats = append(subcats, forumSubcategoryData{
|
||||||
Name: *catRow.Cat.Name,
|
Name: *catNode.Name,
|
||||||
Url: categoryUrls[catRow.Cat.ID],
|
Url: hmnurl.BuildForumCategory(c.CurrentProject.Slug, lineageBuilder.GetSubforumLineageSlugs(catNode.ID), 1),
|
||||||
Threads: threads,
|
Threads: threads,
|
||||||
TotalThreads: numThreads,
|
TotalThreads: numThreads,
|
||||||
})
|
})
|
||||||
|
@ -266,28 +240,37 @@ func ForumCategory(c *RequestContext) ResponseData {
|
||||||
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 subcategories.
|
||||||
{
|
{
|
||||||
Name: c.CurrentProject.Name,
|
Name: c.CurrentProject.Name,
|
||||||
Url: hmnurl.ProjectUrl("/", nil, c.CurrentProject.Slug),
|
Url: hmnurl.BuildProjectHomepage(c.CurrentProject.Slug),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "Forums",
|
Name: "Forums",
|
||||||
Url: categoryUrls[currentCatId],
|
Url: hmnurl.BuildForumCategory(c.CurrentProject.Slug, nil, 1),
|
||||||
Current: true,
|
Current: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentSubforums := lineageBuilder.GetSubforumLineage(currentCatId)
|
||||||
|
for i, subforum := range currentSubforums {
|
||||||
|
baseData.Breadcrumbs = append(baseData.Breadcrumbs, templates.Breadcrumb{
|
||||||
|
Name: *subforum.Name, // NOTE(asaf): All subforum categories must have names.
|
||||||
|
Url: hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs[0:i+1], 1),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
var res ResponseData
|
var res ResponseData
|
||||||
err = res.WriteTemplate("forum_category.html", forumCategoryData{
|
err = res.WriteTemplate("forum_category.html", forumCategoryData{
|
||||||
BaseData: baseData,
|
BaseData: baseData,
|
||||||
CategoryUrl: categoryUrls[currentCatId],
|
NewThreadUrl: hmnurl.BuildForumNewThread(c.CurrentProject.Slug, currentSubforumSlugs),
|
||||||
Threads: threads,
|
MarkReadUrl: hmnurl.BuildMarkRead(currentCatId),
|
||||||
|
Threads: threads,
|
||||||
Pagination: templates.Pagination{
|
Pagination: templates.Pagination{
|
||||||
Current: page,
|
Current: page,
|
||||||
Total: numPages,
|
Total: numPages,
|
||||||
|
|
||||||
FirstUrl: categoryUrls[currentCatId],
|
FirstUrl: hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs, 1),
|
||||||
LastUrl: fmt.Sprintf("%s/%d", categoryUrls[currentCatId], numPages),
|
LastUrl: hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs, numPages),
|
||||||
NextUrl: fmt.Sprintf("%s/%d", categoryUrls[currentCatId], page+1),
|
NextUrl: hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs, utils.IntClamp(1, page+1, numPages)),
|
||||||
PreviousUrl: fmt.Sprintf("%s/%d", categoryUrls[currentCatId], page-1),
|
PreviousUrl: hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs, utils.IntClamp(1, page-1, numPages)),
|
||||||
},
|
},
|
||||||
Subcategories: subcats,
|
Subcategories: subcats,
|
||||||
}, c.Perf)
|
}, c.Perf)
|
||||||
|
@ -312,11 +295,23 @@ type forumThreadData struct {
|
||||||
func ForumThread(c *RequestContext) ResponseData {
|
func ForumThread(c *RequestContext) ResponseData {
|
||||||
const postsPerPage = 15
|
const postsPerPage = 15
|
||||||
|
|
||||||
|
c.Perf.StartBlock("SQL", "Fetch category tree")
|
||||||
|
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
|
||||||
|
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
||||||
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
|
currentCatId, valid := validateSubforums(lineageBuilder, c.CurrentProject, c.PathParams["cats"])
|
||||||
|
if !valid {
|
||||||
|
return FourOhFour(c)
|
||||||
|
}
|
||||||
|
|
||||||
threadId, err := strconv.Atoi(c.PathParams["threadid"])
|
threadId, err := strconv.Atoi(c.PathParams["threadid"])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return FourOhFour(c)
|
return FourOhFour(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentSubforumSlugs := lineageBuilder.GetSubforumLineageSlugs(currentCatId)
|
||||||
|
|
||||||
c.Perf.StartBlock("SQL", "Fetch current thread")
|
c.Perf.StartBlock("SQL", "Fetch current thread")
|
||||||
type threadQueryResult struct {
|
type threadQueryResult struct {
|
||||||
Thread models.Thread `db:"thread"`
|
Thread models.Thread `db:"thread"`
|
||||||
|
@ -330,10 +325,10 @@ func ForumThread(c *RequestContext) ResponseData {
|
||||||
WHERE
|
WHERE
|
||||||
thread.id = $1
|
thread.id = $1
|
||||||
AND NOT thread.deleted
|
AND NOT thread.deleted
|
||||||
AND cat.project_id = $2
|
AND cat.id = $2
|
||||||
`,
|
`,
|
||||||
threadId,
|
threadId,
|
||||||
c.CurrentProject.ID,
|
currentCatId, // NOTE(asaf): This verifies that the requested thread is under the requested subforum.
|
||||||
)
|
)
|
||||||
c.Perf.EndBlock()
|
c.Perf.EndBlock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -345,14 +340,6 @@ func ForumThread(c *RequestContext) ResponseData {
|
||||||
}
|
}
|
||||||
thread := irow.(*threadQueryResult).Thread
|
thread := irow.(*threadQueryResult).Thread
|
||||||
|
|
||||||
categoryUrls := GetProjectCategoryUrls(c.Context(), c.Conn, c.CurrentProject.ID)
|
|
||||||
|
|
||||||
c.Perf.StartBlock("SQL", "Fetch category tree")
|
|
||||||
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
|
|
||||||
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
|
||||||
subforums := lineageBuilder.GetLineageSlugs(thread.CategoryID)[1:]
|
|
||||||
c.Perf.EndBlock()
|
|
||||||
|
|
||||||
numPosts, err := db.QueryInt(c.Context(), c.Conn,
|
numPosts, err := db.QueryInt(c.Context(), c.Conn,
|
||||||
`
|
`
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
|
@ -368,17 +355,17 @@ func ForumThread(c *RequestContext) ResponseData {
|
||||||
}
|
}
|
||||||
page, numPages, ok := getPageInfo(c.PathParams["page"], numPosts, postsPerPage)
|
page, numPages, ok := getPageInfo(c.PathParams["page"], numPosts, postsPerPage)
|
||||||
if !ok {
|
if !ok {
|
||||||
urlNoPage := ThreadUrl(thread, models.CatKindForum, categoryUrls[thread.CategoryID])
|
urlNoPage := hmnurl.BuildForumThread(c.CurrentProject.Slug, currentSubforumSlugs, thread.ID, thread.Title, 1)
|
||||||
return c.Redirect(urlNoPage, http.StatusSeeOther)
|
return c.Redirect(urlNoPage, http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
pagination := templates.Pagination{
|
pagination := templates.Pagination{
|
||||||
Current: page,
|
Current: page,
|
||||||
Total: numPages,
|
Total: numPages,
|
||||||
|
|
||||||
FirstUrl: hmnurl.BuildForumThread(c.CurrentProject.Slug, subforums, thread.ID, 1),
|
FirstUrl: hmnurl.BuildForumThread(c.CurrentProject.Slug, currentSubforumSlugs, thread.ID, thread.Title, 1),
|
||||||
LastUrl: hmnurl.BuildForumThread(c.CurrentProject.Slug, subforums, thread.ID, numPages),
|
LastUrl: hmnurl.BuildForumThread(c.CurrentProject.Slug, currentSubforumSlugs, thread.ID, thread.Title, numPages),
|
||||||
NextUrl: hmnurl.BuildForumThread(c.CurrentProject.Slug, subforums, thread.ID, utils.IntClamp(1, page+1, numPages)),
|
NextUrl: hmnurl.BuildForumThread(c.CurrentProject.Slug, currentSubforumSlugs, thread.ID, thread.Title, utils.IntClamp(1, page+1, numPages)),
|
||||||
PreviousUrl: hmnurl.BuildForumThread(c.CurrentProject.Slug, subforums, thread.ID, utils.IntClamp(1, page-1, numPages)),
|
PreviousUrl: hmnurl.BuildForumThread(c.CurrentProject.Slug, currentSubforumSlugs, thread.ID, thread.Title, utils.IntClamp(1, page-1, numPages)),
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Perf.StartBlock("SQL", "Fetch posts")
|
c.Perf.StartBlock("SQL", "Fetch posts")
|
||||||
|
@ -418,7 +405,7 @@ func ForumThread(c *RequestContext) ResponseData {
|
||||||
|
|
||||||
post := templates.PostToTemplate(&row.Post, row.Author)
|
post := templates.PostToTemplate(&row.Post, row.Author)
|
||||||
post.AddContentVersion(row.Ver, row.Editor)
|
post.AddContentVersion(row.Ver, row.Editor)
|
||||||
post.AddUrls(c.CurrentProject.Slug, subforums, thread.ID, post.ID)
|
post.AddUrls(c.CurrentProject.Slug, currentSubforumSlugs, thread.ID, post.ID)
|
||||||
|
|
||||||
posts = append(posts, post)
|
posts = append(posts, post)
|
||||||
}
|
}
|
||||||
|
@ -432,8 +419,8 @@ func ForumThread(c *RequestContext) ResponseData {
|
||||||
BaseData: baseData,
|
BaseData: baseData,
|
||||||
Thread: templates.ThreadToTemplate(&thread),
|
Thread: templates.ThreadToTemplate(&thread),
|
||||||
Posts: posts,
|
Posts: posts,
|
||||||
CategoryUrl: categoryUrls[thread.CategoryID],
|
CategoryUrl: hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs, 1),
|
||||||
ReplyUrl: hmnurl.BuildForumPostReply(c.CurrentProject.Slug, subforums, thread.ID, *thread.FirstID),
|
ReplyUrl: hmnurl.BuildForumPostReply(c.CurrentProject.Slug, currentSubforumSlugs, thread.ID, *thread.FirstID),
|
||||||
Pagination: pagination,
|
Pagination: pagination,
|
||||||
}, c.Perf)
|
}, c.Perf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -443,46 +430,36 @@ func ForumThread(c *RequestContext) ResponseData {
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchCatIdFromSlugs(ctx context.Context, conn *pgxpool.Pool, catSlugs []string, projectId int) int {
|
func validateSubforums(lineageBuilder *models.CategoryLineageBuilder, project *models.Project, catPath string) (int, bool) {
|
||||||
if len(catSlugs) == 1 {
|
if project.ForumID == nil {
|
||||||
var err error
|
return -1, false
|
||||||
currentCatId, err := db.QueryInt(ctx, conn,
|
|
||||||
`
|
|
||||||
SELECT cat.id
|
|
||||||
FROM
|
|
||||||
handmade_category AS cat
|
|
||||||
JOIN handmade_project AS proj ON proj.forum_id = cat.id
|
|
||||||
WHERE
|
|
||||||
proj.id = $1
|
|
||||||
AND cat.kind = $2
|
|
||||||
`,
|
|
||||||
projectId,
|
|
||||||
models.CatKindForum,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
panic(oops.New(err, "failed to get root category id"))
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentCatId
|
|
||||||
} else {
|
|
||||||
var err error
|
|
||||||
currentCatId, err := db.QueryInt(ctx, conn,
|
|
||||||
`
|
|
||||||
SELECT id
|
|
||||||
FROM handmade_category
|
|
||||||
WHERE
|
|
||||||
slug = $1
|
|
||||||
AND kind = $2
|
|
||||||
AND project_id = $3
|
|
||||||
`,
|
|
||||||
catSlugs[len(catSlugs)-1],
|
|
||||||
models.CatKindForum,
|
|
||||||
projectId,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
panic(oops.New(err, "failed to get current category id"))
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentCatId
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
subforumCatId := *project.ForumID
|
||||||
|
if len(catPath) == 0 {
|
||||||
|
return subforumCatId, true
|
||||||
|
}
|
||||||
|
|
||||||
|
catPath = strings.ToLower(catPath)
|
||||||
|
valid := false
|
||||||
|
catSlugs := strings.Split(catPath, "/")
|
||||||
|
lastSlug := catSlugs[len(catSlugs)-1]
|
||||||
|
if len(lastSlug) > 0 {
|
||||||
|
lastSlugCatId := lineageBuilder.FindIdBySlug(project.ID, lastSlug)
|
||||||
|
if lastSlugCatId != -1 {
|
||||||
|
subforumSlugs := lineageBuilder.GetSubforumLineageSlugs(lastSlugCatId)
|
||||||
|
allMatch := true
|
||||||
|
for i, subforum := range subforumSlugs {
|
||||||
|
if subforum != catSlugs[i] {
|
||||||
|
allMatch = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
valid = allMatch
|
||||||
|
}
|
||||||
|
if valid {
|
||||||
|
subforumCatId = lastSlugCatId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return subforumCatId, valid
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.handmade.network/hmn/hmn/src/db"
|
"git.handmade.network/hmn/hmn/src/db"
|
||||||
|
"git.handmade.network/hmn/hmn/src/hmnurl"
|
||||||
"git.handmade.network/hmn/hmn/src/models"
|
"git.handmade.network/hmn/hmn/src/models"
|
||||||
"git.handmade.network/hmn/hmn/src/oops"
|
"git.handmade.network/hmn/hmn/src/oops"
|
||||||
"git.handmade.network/hmn/hmn/src/templates"
|
"git.handmade.network/hmn/hmn/src/templates"
|
||||||
|
@ -18,12 +19,21 @@ type LandingTemplateData struct {
|
||||||
NewsPost LandingPageFeaturedPost
|
NewsPost LandingPageFeaturedPost
|
||||||
PostColumns [][]LandingPageProject
|
PostColumns [][]LandingPageProject
|
||||||
ShowcaseTimelineJson string
|
ShowcaseTimelineJson string
|
||||||
|
|
||||||
|
FeedUrl string
|
||||||
|
PodcastUrl string
|
||||||
|
StreamsUrl string
|
||||||
|
IRCUrl string
|
||||||
|
DiscordUrl string
|
||||||
|
ShowUrl string
|
||||||
|
ShowcaseUrl string
|
||||||
}
|
}
|
||||||
|
|
||||||
type LandingPageProject struct {
|
type LandingPageProject struct {
|
||||||
Project templates.Project
|
Project templates.Project
|
||||||
FeaturedPost *LandingPageFeaturedPost
|
FeaturedPost *LandingPageFeaturedPost
|
||||||
Posts []templates.PostListItem
|
Posts []templates.PostListItem
|
||||||
|
ForumsUrl string
|
||||||
}
|
}
|
||||||
|
|
||||||
type LandingPageFeaturedPost struct {
|
type LandingPageFeaturedPost struct {
|
||||||
|
@ -45,11 +55,12 @@ func Index(c *RequestContext) ResponseData {
|
||||||
SELECT $columns
|
SELECT $columns
|
||||||
FROM handmade_project
|
FROM handmade_project
|
||||||
WHERE
|
WHERE
|
||||||
flags = 0
|
(flags = 0 AND NOT lifecycle = ANY($1))
|
||||||
OR id = $1
|
OR id = $2
|
||||||
ORDER BY all_last_updated DESC
|
ORDER BY all_last_updated DESC
|
||||||
LIMIT $2
|
LIMIT $3
|
||||||
`,
|
`,
|
||||||
|
[]models.ProjectLifecycle{models.ProjectLifecycleUnapproved, models.ProjectLifecycleApprovalRequired},
|
||||||
models.HMNProjectID,
|
models.HMNProjectID,
|
||||||
numProjectsToGet*2, // hedge your bets against projects that don't have any content
|
numProjectsToGet*2, // hedge your bets against projects that don't have any content
|
||||||
)
|
)
|
||||||
|
@ -62,9 +73,11 @@ func Index(c *RequestContext) ResponseData {
|
||||||
|
|
||||||
allProjects := iterProjects.ToSlice()
|
allProjects := iterProjects.ToSlice()
|
||||||
c.Perf.EndBlock()
|
c.Perf.EndBlock()
|
||||||
c.Logger.Debug().Interface("allProjects", allProjects).Msg("all the projects")
|
|
||||||
|
|
||||||
categoryUrls := GetAllCategoryUrls(c.Context(), c.Conn)
|
c.Perf.StartBlock("SQL", "Fetch category tree")
|
||||||
|
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
|
||||||
|
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
||||||
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
var currentUserId *int
|
var currentUserId *int
|
||||||
if c.CurrentUser != nil {
|
if c.CurrentUser != nil {
|
||||||
|
@ -77,11 +90,12 @@ func Index(c *RequestContext) ResponseData {
|
||||||
|
|
||||||
c.Perf.StartBlock("SQL", fmt.Sprintf("Fetch posts for %s", proj.Name))
|
c.Perf.StartBlock("SQL", fmt.Sprintf("Fetch posts for %s", proj.Name))
|
||||||
type projectPostQuery struct {
|
type projectPostQuery struct {
|
||||||
Post models.Post `db:"post"`
|
Post models.Post `db:"post"`
|
||||||
Thread models.Thread `db:"thread"`
|
Thread models.Thread `db:"thread"`
|
||||||
User models.User `db:"auth_user"`
|
User models.User `db:"auth_user"`
|
||||||
ThreadLastReadTime *time.Time `db:"tlri.lastread"`
|
LibraryResource *models.LibraryResource `db:"lib_resource"`
|
||||||
CatLastReadTime *time.Time `db:"clri.lastread"`
|
ThreadLastReadTime *time.Time `db:"tlri.lastread"`
|
||||||
|
CatLastReadTime *time.Time `db:"clri.lastread"`
|
||||||
}
|
}
|
||||||
projectPostIter, err := db.Query(c.Context(), c.Conn, projectPostQuery{},
|
projectPostIter, err := db.Query(c.Context(), c.Conn, projectPostQuery{},
|
||||||
`
|
`
|
||||||
|
@ -98,6 +112,7 @@ func Index(c *RequestContext) ResponseData {
|
||||||
AND clri.user_id = $1
|
AND clri.user_id = $1
|
||||||
)
|
)
|
||||||
LEFT JOIN auth_user ON post.author_id = auth_user.id
|
LEFT JOIN auth_user ON post.author_id = auth_user.id
|
||||||
|
LEFT JOIN handmade_libraryresource as lib_resource ON lib_resource.category_id = post.category_id
|
||||||
WHERE
|
WHERE
|
||||||
post.project_id = $2
|
post.project_id = $2
|
||||||
AND post.category_kind IN ($3, $4, $5, $6)
|
AND post.category_kind IN ($3, $4, $5, $6)
|
||||||
|
@ -117,8 +132,16 @@ func Index(c *RequestContext) ResponseData {
|
||||||
}
|
}
|
||||||
projectPosts := projectPostIter.ToSlice()
|
projectPosts := projectPostIter.ToSlice()
|
||||||
|
|
||||||
|
forumsUrl := ""
|
||||||
|
if proj.ForumID != nil {
|
||||||
|
forumsUrl = hmnurl.BuildForumCategory(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")
|
||||||
|
}
|
||||||
|
|
||||||
landingPageProject := LandingPageProject{
|
landingPageProject := LandingPageProject{
|
||||||
Project: templates.ProjectToTemplate(proj),
|
Project: templates.ProjectToTemplate(proj),
|
||||||
|
ForumsUrl: forumsUrl,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, projectPostRow := range projectPosts {
|
for _, projectPostRow := range projectPosts {
|
||||||
|
@ -150,28 +173,35 @@ func Index(c *RequestContext) ResponseData {
|
||||||
WHERE
|
WHERE
|
||||||
post.id = $1
|
post.id = $1
|
||||||
`, projectPost.Post.ID)
|
`, projectPost.Post.ID)
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
c.Perf.EndBlock()
|
c.Perf.EndBlock()
|
||||||
|
if err != nil {
|
||||||
|
c.Logger.Error().Err(err).Msg("failed to fetch featured post content")
|
||||||
|
continue
|
||||||
|
}
|
||||||
content := contentResult.(*featuredContentResult).Content
|
content := contentResult.(*featuredContentResult).Content
|
||||||
|
|
||||||
landingPageProject.FeaturedPost = &LandingPageFeaturedPost{
|
landingPageProject.FeaturedPost = &LandingPageFeaturedPost{
|
||||||
Title: projectPost.Thread.Title,
|
Title: projectPost.Thread.Title,
|
||||||
Url: PostUrl(projectPost.Post, projectPost.Post.CategoryKind, categoryUrls[projectPost.Post.CategoryID]),
|
Url: hmnurl.BuildBlogPost(proj.Slug, projectPost.Thread.ID, projectPost.Post.ID),
|
||||||
User: templates.UserToTemplate(&projectPost.User),
|
User: templates.UserToTemplate(&projectPost.User),
|
||||||
Date: projectPost.Post.PostDate,
|
Date: projectPost.Post.PostDate,
|
||||||
Unread: !hasRead,
|
Unread: !hasRead,
|
||||||
Content: template.HTML(content),
|
Content: template.HTML(content),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
landingPageProject.Posts = append(landingPageProject.Posts, templates.PostListItem{
|
landingPageProject.Posts = append(
|
||||||
Title: projectPost.Thread.Title,
|
landingPageProject.Posts,
|
||||||
Url: PostUrl(projectPost.Post, projectPost.Post.CategoryKind, categoryUrls[projectPost.Post.CategoryID]),
|
MakePostListItem(
|
||||||
User: templates.UserToTemplate(&projectPost.User),
|
lineageBuilder,
|
||||||
Date: projectPost.Post.PostDate,
|
proj,
|
||||||
Unread: !hasRead,
|
&projectPost.Thread,
|
||||||
})
|
&projectPost.Post,
|
||||||
|
&projectPost.User,
|
||||||
|
projectPost.LibraryResource,
|
||||||
|
!hasRead,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,30 +215,6 @@ func Index(c *RequestContext) ResponseData {
|
||||||
}
|
}
|
||||||
c.Perf.EndBlock()
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
c.Perf.StartBlock("SQL", "Get news")
|
|
||||||
type newsThreadQuery struct {
|
|
||||||
Thread models.Thread `db:"thread"`
|
|
||||||
}
|
|
||||||
newsThreadRow, err := db.QueryOne(c.Context(), c.Conn, newsThreadQuery{},
|
|
||||||
`
|
|
||||||
SELECT $columns
|
|
||||||
FROM
|
|
||||||
handmade_thread as thread
|
|
||||||
JOIN handmade_category AS cat ON thread.category_id = cat.id
|
|
||||||
WHERE
|
|
||||||
cat.project_id = $1
|
|
||||||
AND cat.kind = $2
|
|
||||||
`,
|
|
||||||
models.HMNProjectID,
|
|
||||||
models.CatKindBlog,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch latest news post"))
|
|
||||||
}
|
|
||||||
c.Perf.EndBlock()
|
|
||||||
newsThread := newsThreadRow.(*newsThreadQuery)
|
|
||||||
_ = newsThread // TODO: NO
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Columns are filled by placing projects into the least full column.
|
Columns are filled by placing projects into the least full column.
|
||||||
The fill array tracks the estimated sizes.
|
The fill array tracks the estimated sizes.
|
||||||
|
@ -239,6 +245,7 @@ func Index(c *RequestContext) ResponseData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.Perf.StartBlock("SQL", "Get news")
|
||||||
type newsPostQuery struct {
|
type newsPostQuery struct {
|
||||||
Post models.Post `db:"post"`
|
Post models.Post `db:"post"`
|
||||||
PostVersion models.PostVersion `db:"ver"`
|
PostVersion models.PostVersion `db:"ver"`
|
||||||
|
@ -251,12 +258,11 @@ func Index(c *RequestContext) ResponseData {
|
||||||
FROM
|
FROM
|
||||||
handmade_post AS post
|
handmade_post AS post
|
||||||
JOIN handmade_thread AS thread ON post.thread_id = thread.id
|
JOIN handmade_thread AS thread ON post.thread_id = thread.id
|
||||||
JOIN handmade_category AS cat ON thread.category_id = cat.id
|
|
||||||
JOIN auth_user ON post.author_id = auth_user.id
|
JOIN auth_user ON post.author_id = auth_user.id
|
||||||
JOIN handmade_postversion AS ver ON post.current_id = ver.id
|
JOIN handmade_postversion AS ver ON post.current_id = ver.id
|
||||||
WHERE
|
WHERE
|
||||||
cat.project_id = $1
|
post.project_id = $1
|
||||||
AND cat.kind = $2
|
AND post.category_kind = $2
|
||||||
AND post.id = thread.first_id
|
AND post.id = thread.first_id
|
||||||
AND NOT thread.deleted
|
AND NOT thread.deleted
|
||||||
ORDER BY post.postdate DESC
|
ORDER BY post.postdate DESC
|
||||||
|
@ -269,16 +275,24 @@ func Index(c *RequestContext) ResponseData {
|
||||||
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch news post"))
|
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch news post"))
|
||||||
}
|
}
|
||||||
newsPostResult := newsPostRow.(*newsPostQuery)
|
newsPostResult := newsPostRow.(*newsPostQuery)
|
||||||
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
baseData := getBaseData(c)
|
baseData := getBaseData(c)
|
||||||
baseData.BodyClasses = append(baseData.BodyClasses, "hmdev", "landing") // TODO: Is "hmdev" necessary any more?
|
baseData.BodyClasses = append(baseData.BodyClasses, "hmdev", "landing") // TODO: Is "hmdev" necessary any more?
|
||||||
|
|
||||||
var res ResponseData
|
var res ResponseData
|
||||||
err = res.WriteTemplate("landing.html", LandingTemplateData{
|
err = res.WriteTemplate("landing.html", LandingTemplateData{
|
||||||
BaseData: baseData,
|
BaseData: baseData,
|
||||||
|
FeedUrl: hmnurl.BuildFeed(),
|
||||||
|
PodcastUrl: hmnurl.BuildPodcast(models.HMNProjectSlug),
|
||||||
|
StreamsUrl: hmnurl.BuildStreams(),
|
||||||
|
IRCUrl: hmnurl.BuildBlogThread(models.HMNProjectSlug, 1138, "[Tutorial] Handmade Network IRC", 1),
|
||||||
|
DiscordUrl: "https://discord.gg/hxWxDee",
|
||||||
|
ShowUrl: "https://handmadedev.show/",
|
||||||
|
ShowcaseUrl: hmnurl.BuildShowcase(),
|
||||||
NewsPost: LandingPageFeaturedPost{
|
NewsPost: LandingPageFeaturedPost{
|
||||||
Title: newsPostResult.Thread.Title,
|
Title: newsPostResult.Thread.Title,
|
||||||
Url: PostUrl(newsPostResult.Post, models.CatKindBlog, ""),
|
Url: hmnurl.BuildBlogPost(models.HMNProjectSlug, newsPostResult.Thread.ID, newsPostResult.Post.ID),
|
||||||
User: templates.UserToTemplate(&newsPostResult.User),
|
User: templates.UserToTemplate(&newsPostResult.User),
|
||||||
Date: newsPostResult.Post.PostDate,
|
Date: newsPostResult.Post.PostDate,
|
||||||
Unread: true, // TODO
|
Unread: true, // TODO
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
package website
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.handmade.network/hmn/hmn/src/hmnurl"
|
||||||
|
"git.handmade.network/hmn/hmn/src/models"
|
||||||
|
"git.handmade.network/hmn/hmn/src/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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:
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hmnurl.BuildProjectHomepage(projectSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) templates.PostListItem {
|
||||||
|
var result templates.PostListItem
|
||||||
|
|
||||||
|
result.Title = thread.Title
|
||||||
|
result.User = templates.UserToTemplate(user)
|
||||||
|
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)
|
||||||
|
|
||||||
|
if includeBreadcrumbs {
|
||||||
|
result.Breadcrumbs = append(result.Breadcrumbs, templates.Breadcrumb{
|
||||||
|
Name: project.Name,
|
||||||
|
Url: hmnurl.BuildProjectHomepage(project.Slug),
|
||||||
|
})
|
||||||
|
result.Breadcrumbs = append(result.Breadcrumbs, templates.Breadcrumb{
|
||||||
|
Name: CategoryKindDisplayNames[post.CategoryKind],
|
||||||
|
Url: BuildProjectMainCategoryUrl(project.Slug, post.CategoryKind),
|
||||||
|
})
|
||||||
|
switch post.CategoryKind {
|
||||||
|
case models.CatKindForum:
|
||||||
|
subforums := lineageBuilder.GetSubforumLineage(post.CategoryID)
|
||||||
|
slugs := lineageBuilder.GetSubforumLineageSlugs(post.CategoryID)
|
||||||
|
for i, subforum := range subforums {
|
||||||
|
result.Breadcrumbs = append(result.Breadcrumbs, templates.Breadcrumb{
|
||||||
|
Name: *subforum.Name, // NOTE(asaf): All subforum categories must have names.
|
||||||
|
Url: hmnurl.BuildForumCategory(project.Slug, slugs[0:i+1], 1),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case models.CatKindLibraryResource:
|
||||||
|
result.Breadcrumbs = append(result.Breadcrumbs, templates.Breadcrumb{
|
||||||
|
Name: libraryResource.Name,
|
||||||
|
Url: hmnurl.BuildLibraryResource(project.Slug, libraryResource.ID),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
|
@ -81,8 +81,8 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(asaf): login/logout shouldn't happen on subdomains. We should verify that in the middleware.
|
// TODO(asaf): login/logout shouldn't happen on subdomains. We should verify that in the middleware.
|
||||||
routes.POST(hmnurl.RegexLogin, Login)
|
routes.POST(hmnurl.RegexLoginAction, Login)
|
||||||
routes.GET(hmnurl.RegexLogout, Logout)
|
routes.GET(hmnurl.RegexLogoutAction, Logout)
|
||||||
routes.StdHandler(hmnurl.RegexPublic,
|
routes.StdHandler(hmnurl.RegexPublic,
|
||||||
http.StripPrefix("/public/", http.FileServer(http.Dir("public"))),
|
http.StripPrefix("/public/", http.FileServer(http.Dir("public"))),
|
||||||
)
|
)
|
||||||
|
@ -130,9 +130,39 @@ func getBaseData(c *RequestContext) templates.BaseData {
|
||||||
}
|
}
|
||||||
|
|
||||||
return templates.BaseData{
|
return templates.BaseData{
|
||||||
Project: templates.ProjectToTemplate(c.CurrentProject),
|
Project: templates.ProjectToTemplate(c.CurrentProject),
|
||||||
User: templateUser,
|
LoginPageUrl: hmnurl.BuildLoginPage(c.FullUrl()),
|
||||||
Theme: "light",
|
User: templateUser,
|
||||||
|
Theme: "light",
|
||||||
|
ProjectCSSUrl: hmnurl.BuildProjectCSS(c.CurrentProject.Color1),
|
||||||
|
Header: templates.Header{
|
||||||
|
AdminUrl: hmnurl.BuildHomepage(), // TODO(asaf)
|
||||||
|
MemberSettingsUrl: hmnurl.BuildHomepage(), // TODO(asaf)
|
||||||
|
LoginActionUrl: hmnurl.BuildLoginAction(c.FullUrl()),
|
||||||
|
LogoutActionUrl: hmnurl.BuildLogoutAction(),
|
||||||
|
RegisterUrl: hmnurl.BuildHomepage(), // TODO(asaf)
|
||||||
|
HMNHomepageUrl: hmnurl.BuildHomepage(), // TODO(asaf)
|
||||||
|
ProjectHomepageUrl: hmnurl.BuildProjectHomepage(c.CurrentProject.Slug),
|
||||||
|
BlogUrl: hmnurl.BuildBlog(c.CurrentProject.Slug, 1),
|
||||||
|
ForumsUrl: hmnurl.BuildForumCategory(c.CurrentProject.Slug, nil, 1),
|
||||||
|
WikiUrl: hmnurl.BuildWiki(c.CurrentProject.Slug),
|
||||||
|
LibraryUrl: hmnurl.BuildLibrary(c.CurrentProject.Slug),
|
||||||
|
ManifestoUrl: hmnurl.BuildManifesto(),
|
||||||
|
EpisodeGuideUrl: hmnurl.BuildHomepage(), // TODO(asaf)
|
||||||
|
EditUrl: hmnurl.BuildHomepage(), // TODO(asaf)
|
||||||
|
SearchActionUrl: hmnurl.BuildHomepage(), // TODO(asaf)
|
||||||
|
},
|
||||||
|
Footer: templates.Footer{
|
||||||
|
HomepageUrl: hmnurl.BuildHomepage(),
|
||||||
|
AboutUrl: hmnurl.BuildAbout(),
|
||||||
|
ManifestoUrl: hmnurl.BuildManifesto(),
|
||||||
|
CodeOfConductUrl: hmnurl.BuildCodeOfConduct(),
|
||||||
|
CommunicationGuidelinesUrl: hmnurl.BuildCommunicationGuidelines(),
|
||||||
|
ProjectIndexUrl: hmnurl.BuildProjectIndex(),
|
||||||
|
ForumsUrl: hmnurl.BuildForumCategory(models.HMNProjectSlug, nil, 1),
|
||||||
|
ContactUrl: hmnurl.BuildContactPage(),
|
||||||
|
SitemapUrl: hmnurl.BuildSiteMap(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -284,11 +314,11 @@ func TrackRequestPerf(c *RequestContext, perfCollector *perf.PerfCollector) (aft
|
||||||
c.Perf.EndRequest()
|
c.Perf.EndRequest()
|
||||||
log := logging.Info()
|
log := logging.Info()
|
||||||
blockStack := make([]time.Time, 0)
|
blockStack := make([]time.Time, 0)
|
||||||
for _, block := range c.Perf.Blocks {
|
for i, block := range c.Perf.Blocks {
|
||||||
for len(blockStack) > 0 && block.End.After(blockStack[len(blockStack)-1]) {
|
for len(blockStack) > 0 && block.End.After(blockStack[len(blockStack)-1]) {
|
||||||
blockStack = blockStack[:len(blockStack)-1]
|
blockStack = blockStack[:len(blockStack)-1]
|
||||||
}
|
}
|
||||||
log.Str(fmt.Sprintf("At %9.2fms", c.Perf.MsFromStart(&block)), fmt.Sprintf("%*.s[%s] %s (%.4fms)", len(blockStack)*2, "", block.Category, block.Description, block.DurationMs()))
|
log.Str(fmt.Sprintf("[%4.d] At %9.2fms", i, c.Perf.MsFromStart(&block)), fmt.Sprintf("%*.s[%s] %s (%.4fms)", len(blockStack)*2, "", block.Category, block.Description, block.DurationMs()))
|
||||||
blockStack = append(blockStack, block.End)
|
blockStack = append(blockStack, block.End)
|
||||||
}
|
}
|
||||||
log.Msg(fmt.Sprintf("Served %s in %.4fms", c.Perf.Path, float64(c.Perf.End.Sub(c.Perf.Start).Nanoseconds())/1000/1000))
|
log.Msg(fmt.Sprintf("Served %s in %.4fms", c.Perf.Path, float64(c.Perf.End.Sub(c.Perf.Start).Nanoseconds())/1000/1000))
|
||||||
|
@ -298,6 +328,6 @@ func TrackRequestPerf(c *RequestContext, perfCollector *perf.PerfCollector) (aft
|
||||||
|
|
||||||
func LogContextErrors(c *RequestContext, res *ResponseData) {
|
func LogContextErrors(c *RequestContext, res *ResponseData) {
|
||||||
for _, err := range res.Errors {
|
for _, err := range res.Errors {
|
||||||
c.Logger.Error().Err(err).Msg("error occurred during request")
|
c.Logger.Error().Timestamp().Stack().Str("Requested", c.FullUrl()).Err(err).Msg("error occurred during request")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,136 +1,20 @@
|
||||||
package website
|
package website
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.handmade.network/hmn/hmn/src/db"
|
|
||||||
"git.handmade.network/hmn/hmn/src/hmnurl"
|
"git.handmade.network/hmn/hmn/src/hmnurl"
|
||||||
"git.handmade.network/hmn/hmn/src/models"
|
"git.handmade.network/hmn/hmn/src/models"
|
||||||
"github.com/jackc/pgx/v4/pgxpool"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type categoryUrlQueryResult struct {
|
func BuildProjectMainCategoryUrl(projectSlug string, kind models.CategoryKind) string {
|
||||||
Cat models.Category `db:"cat"`
|
switch kind {
|
||||||
Project models.Project `db:"project"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetAllCategoryUrls(ctx context.Context, conn *pgxpool.Pool) map[int]string {
|
|
||||||
it, err := db.Query(ctx, conn, categoryUrlQueryResult{},
|
|
||||||
`
|
|
||||||
SELECT $columns
|
|
||||||
FROM
|
|
||||||
handmade_category AS cat
|
|
||||||
JOIN handmade_project AS project ON project.id = cat.project_id
|
|
||||||
WHERE
|
|
||||||
cat.kind != 6
|
|
||||||
`, // TODO(asaf): Clean up the db and remove the cat.kind != 6 check
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer it.Close()
|
|
||||||
|
|
||||||
return makeCategoryUrls(it.ToSlice())
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetProjectCategoryUrls(ctx context.Context, conn *pgxpool.Pool, projectId ...int) map[int]string {
|
|
||||||
it, err := db.Query(ctx, conn, categoryUrlQueryResult{},
|
|
||||||
`
|
|
||||||
SELECT $columns
|
|
||||||
FROM
|
|
||||||
handmade_category AS cat
|
|
||||||
JOIN handmade_project AS project ON project.id = cat.project_id
|
|
||||||
WHERE
|
|
||||||
project.id = ANY ($1)
|
|
||||||
AND cat.kind != $2
|
|
||||||
`, // TODO(asaf): Clean up the db and remove the cat.kind != library resource check
|
|
||||||
projectId,
|
|
||||||
models.CatKindLibraryResource,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer it.Close()
|
|
||||||
|
|
||||||
return makeCategoryUrls(it.ToSlice())
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeCategoryUrls(rows []interface{}) map[int]string {
|
|
||||||
categories := make(map[int]*models.Category)
|
|
||||||
for _, irow := range rows {
|
|
||||||
cat := irow.(*categoryUrlQueryResult).Cat
|
|
||||||
categories[cat.ID] = &cat
|
|
||||||
}
|
|
||||||
|
|
||||||
result := make(map[int]string)
|
|
||||||
for _, irow := range rows {
|
|
||||||
row := irow.(*categoryUrlQueryResult)
|
|
||||||
|
|
||||||
// get hierarchy (backwards, so current -> parent -> root)
|
|
||||||
var hierarchyReverse []*models.Category
|
|
||||||
currentCatID := row.Cat.ID
|
|
||||||
for {
|
|
||||||
cat := categories[currentCatID]
|
|
||||||
|
|
||||||
hierarchyReverse = append(hierarchyReverse, cat)
|
|
||||||
if cat.ParentID == nil {
|
|
||||||
break
|
|
||||||
} else {
|
|
||||||
currentCatID = *cat.ParentID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// reverse to get root -> parent -> current
|
|
||||||
hierarchy := make([]*models.Category, len(hierarchyReverse))
|
|
||||||
for i := len(hierarchyReverse) - 1; i >= 0; i-- {
|
|
||||||
hierarchy[len(hierarchyReverse)-1-i] = hierarchyReverse[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
result[row.Cat.ID] = CategoryUrl(row.Project.Slug, hierarchy...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func CategoryUrl(projectSlug string, cats ...*models.Category) string {
|
|
||||||
catSlugs := make([]string, 0, len(cats))
|
|
||||||
for _, cat := range cats {
|
|
||||||
catSlugs = append(catSlugs, *cat.Slug)
|
|
||||||
}
|
|
||||||
switch cats[0].Kind {
|
|
||||||
case models.CatKindForum:
|
|
||||||
return hmnurl.BuildForumCategory(projectSlug, catSlugs[1:], 1)
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func PostUrl(post models.Post, catKind models.CategoryKind, categoryUrl string) string {
|
|
||||||
categoryUrl = strings.TrimRight(categoryUrl, "/")
|
|
||||||
|
|
||||||
switch catKind {
|
|
||||||
// TODO: All the relevant post types. Maybe it doesn't make sense to lump them all together here.
|
|
||||||
case models.CatKindBlog:
|
case models.CatKindBlog:
|
||||||
return fmt.Sprintf("%s/p/%d/e/%d", categoryUrl, post.ThreadID, post.ID)
|
return hmnurl.BuildBlog(projectSlug, 1)
|
||||||
case models.CatKindForum:
|
case models.CatKindForum:
|
||||||
return fmt.Sprintf("%s/t/%d/p/%d", categoryUrl, post.ThreadID, post.ID)
|
return hmnurl.BuildForumCategory(projectSlug, nil, 1)
|
||||||
|
case models.CatKindWiki:
|
||||||
|
return hmnurl.BuildWiki(projectSlug)
|
||||||
|
case models.CatKindLibraryResource:
|
||||||
|
return hmnurl.BuildLibrary(projectSlug)
|
||||||
}
|
}
|
||||||
|
return hmnurl.BuildProjectHomepage(projectSlug)
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func ThreadUrl(thread models.Thread, catKind models.CategoryKind, categoryUrl string) string {
|
|
||||||
categoryUrl = strings.TrimRight(categoryUrl, "/")
|
|
||||||
|
|
||||||
switch catKind {
|
|
||||||
// TODO: All the relevant post types. Maybe it doesn't make sense to lump them all together here.
|
|
||||||
case models.CatKindBlog:
|
|
||||||
return fmt.Sprintf("%s/p/%d", categoryUrl, thread.ID)
|
|
||||||
case models.CatKindForum:
|
|
||||||
return fmt.Sprintf("%s/t/%d", categoryUrl, thread.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue