URL rework!
This commit is contained in:
parent
4e946cd476
commit
02938bbf2c
|
@ -1,7 +1,9 @@
|
|||
package hmnurl
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"git.handmade.network/hmn/hmn/src/config"
|
||||
"git.handmade.network/hmn/hmn/src/models"
|
||||
|
@ -17,9 +19,11 @@ type Q struct {
|
|||
}
|
||||
|
||||
var baseUrlParsed url.URL
|
||||
var cacheBust string
|
||||
|
||||
func init() {
|
||||
SetGlobalBaseUrl(config.Config.BaseUrl)
|
||||
SetCacheBust(fmt.Sprint(time.Now().Unix()))
|
||||
}
|
||||
|
||||
func SetGlobalBaseUrl(fullBaseUrl string) {
|
||||
|
@ -34,11 +38,19 @@ func SetGlobalBaseUrl(fullBaseUrl string) {
|
|||
baseUrlParsed = *parsed
|
||||
}
|
||||
|
||||
func SetCacheBust(newCacheBust string) {
|
||||
cacheBust = newCacheBust
|
||||
}
|
||||
|
||||
func Url(path string, query []Q) string {
|
||||
return ProjectUrl(path, query, "")
|
||||
}
|
||||
|
||||
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
|
||||
if slug == models.HMNProjectSlug {
|
||||
subdomain = ""
|
||||
|
@ -54,6 +66,7 @@ func ProjectUrl(path string, query []Q, slug string) string {
|
|||
Host: host,
|
||||
Path: trim(path),
|
||||
RawQuery: encodeQuery(query),
|
||||
Fragment: fragment,
|
||||
}
|
||||
|
||||
return url.String()
|
||||
|
|
|
@ -27,14 +27,32 @@ func TestUrl(t *testing.T) {
|
|||
|
||||
func TestHomepage(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildHomepage(), RegexHomepage, nil)
|
||||
AssertRegexMatch(t, BuildProjectHomepage("hero"), RegexHomepage, nil)
|
||||
AssertSubdomain(t, BuildProjectHomepage("hero"), "hero")
|
||||
}
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildLogin(), RegexLogin, nil)
|
||||
func TestProjectIndex(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildProjectIndex(), RegexProjectIndex, nil)
|
||||
}
|
||||
|
||||
func TestLogout(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildLogout(), RegexLogout, nil)
|
||||
func TestSiteMap(t *testing.T) {
|
||||
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) {
|
||||
|
@ -47,6 +65,10 @@ func TestStaticPages(t *testing.T) {
|
|||
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) {
|
||||
AssertRegexMatch(t, BuildFeed(), RegexFeed, nil)
|
||||
assert.Equal(t, BuildFeed(), BuildFeedWithPage(1))
|
||||
|
@ -57,30 +79,236 @@ func TestFeed(t *testing.T) {
|
|||
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) {
|
||||
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{"sub", "wip"}, 2), RegexForumCategory, map[string]string{"cats": "forums/sub/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": "sub/wip", "page": "2"})
|
||||
AssertSubdomain(t, BuildForumCategory("hmn", nil, 1), "")
|
||||
AssertSubdomain(t, BuildForumCategory("", nil, 1), "")
|
||||
AssertSubdomain(t, BuildForumCategory("hero", nil, 1), "hero")
|
||||
assert.Panics(t, func() { BuildForumCategory("", nil, 0) })
|
||||
assert.Panics(t, func() { BuildForumCategory("", []string{"", "wip"}, 1) })
|
||||
assert.Panics(t, func() { BuildForumCategory("", []string{" ", "wip"}, 1) })
|
||||
assert.Panics(t, func() { BuildForumCategory("", []string{"wip/jobs"}, 1) })
|
||||
}
|
||||
|
||||
func 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) {
|
||||
AssertRegexMatch(t, BuildProjectCSS("000000"), RegexProjectCSS, nil)
|
||||
}
|
||||
|
@ -96,6 +324,10 @@ func TestPublic(t *testing.T) {
|
|||
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) {
|
||||
parsed, err := url.Parse(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
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -8,24 +9,70 @@ import (
|
|||
"git.handmade.network/hmn/hmn/src/oops"
|
||||
)
|
||||
|
||||
// TODO(asaf): Make this whole file only crash in Dev
|
||||
|
||||
var RegexHomepage = regexp.MustCompile("^/$")
|
||||
|
||||
func BuildHomepage() string {
|
||||
return Url("/", nil)
|
||||
}
|
||||
|
||||
var RegexLogin = regexp.MustCompile("^/login$")
|
||||
|
||||
func BuildLogin() string {
|
||||
return Url("/login", nil)
|
||||
func BuildProjectHomepage(projectSlug string) string {
|
||||
return ProjectUrl("/", nil, projectSlug)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/*
|
||||
* Static Pages
|
||||
*/
|
||||
|
||||
var RegexManifesto = regexp.MustCompile("^/manifesto$")
|
||||
|
||||
func BuildManifesto() string {
|
||||
|
@ -68,6 +115,23 @@ func BuildProjectSubmissionGuidelines() string {
|
|||
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>.+)?)?$`)
|
||||
|
||||
func BuildFeed() string {
|
||||
|
@ -84,56 +148,31 @@ func BuildFeedWithPage(page int) string {
|
|||
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 {
|
||||
if page < 1 {
|
||||
panic(oops.New(nil, "Invalid forum thread page (%d), must be >= 1", page))
|
||||
}
|
||||
var RegexPodcast = regexp.MustCompile(`^/podcast$`)
|
||||
|
||||
var builder strings.Builder
|
||||
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)
|
||||
}
|
||||
builder.WriteString("/t/")
|
||||
builder.WriteString(strconv.Itoa(threadId))
|
||||
if page > 1 {
|
||||
builder.WriteRune('/')
|
||||
builder.WriteString(strconv.Itoa(page))
|
||||
}
|
||||
|
||||
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||
func BuildPodcast(projectSlug string) string {
|
||||
return ProjectUrl("/podcast", 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 {
|
||||
if page < 1 {
|
||||
panic(oops.New(nil, "Invalid forum thread page (%d), must be >= 1", page))
|
||||
}
|
||||
|
||||
var builder strings.Builder
|
||||
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)
|
||||
}
|
||||
builder := buildForumCategoryPath(subforums)
|
||||
|
||||
if page > 1 {
|
||||
builder.WriteRune('/')
|
||||
builder.WriteString(strconv.Itoa(page))
|
||||
|
@ -142,55 +181,387 @@ func BuildForumCategory(projectSlug string, subforums []string, page int) string
|
|||
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 {
|
||||
var builder strings.Builder
|
||||
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)
|
||||
}
|
||||
builder.WriteString("/t/")
|
||||
builder.WriteString(strconv.Itoa(threadId))
|
||||
builder.WriteString("/p/")
|
||||
builder.WriteString(strconv.Itoa(postId))
|
||||
func BuildForumNewThread(projectSlug string, subforums []string) string {
|
||||
builder := buildForumCategoryPath(subforums)
|
||||
builder.WriteString("/new")
|
||||
|
||||
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 {
|
||||
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 {
|
||||
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?
|
||||
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 {
|
||||
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$")
|
||||
|
||||
func BuildProjectCSS(color string) string {
|
||||
|
@ -218,4 +589,219 @@ func BuildPublic(filepath string) string {
|
|||
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("")
|
||||
|
||||
/*
|
||||
* 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 {
|
||||
Category
|
||||
Parent *CategoryTreeNode
|
||||
Parent *CategoryTreeNode
|
||||
Children []*CategoryTreeNode
|
||||
}
|
||||
|
||||
func (node *CategoryTreeNode) GetLineage() []*Category {
|
||||
|
@ -84,6 +85,15 @@ func GetFullCategoryTree(ctx context.Context, conn *pgxpool.Pool) CategoryTree {
|
|||
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
|
||||
}
|
||||
|
||||
|
@ -109,6 +119,10 @@ func (cl *CategoryLineageBuilder) GetLineage(catId int) []*Category {
|
|||
return cl.CategoryCache[catId]
|
||||
}
|
||||
|
||||
func (cl *CategoryLineageBuilder) GetSubforumLineage(catId int) []*Category {
|
||||
return cl.GetLineage(catId)[1:]
|
||||
}
|
||||
|
||||
func (cl *CategoryLineageBuilder) GetLineageSlugs(catId int) []string {
|
||||
_, ok := cl.SlugCache[catId]
|
||||
if !ok {
|
||||
|
@ -125,3 +139,16 @@ func (cl *CategoryLineageBuilder) GetLineageSlugs(catId int) []string {
|
|||
}
|
||||
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 {
|
||||
ID int `db:"id"`
|
||||
|
||||
ForumID *int `db:"forum_id"`
|
||||
|
||||
Slug string `db:"slug"`
|
||||
Name string `db:"name"`
|
||||
Blurb string `db:"blurb"`
|
||||
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"`
|
||||
Color2 string `db:"color_2"`
|
||||
|
|
|
@ -57,6 +57,7 @@ func ProjectToTemplate(p *models.Project) Project {
|
|||
Subdomain: p.Subdomain(),
|
||||
Color1: p.Color1,
|
||||
Color2: p.Color2,
|
||||
Url: hmnurl.BuildProjectHomepage(p.Slug),
|
||||
|
||||
IsHMN: p.IsHMN(),
|
||||
|
||||
|
|
|
@ -10,9 +10,9 @@
|
|||
<div class="content-block">
|
||||
<div class="optionbar">
|
||||
<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 }}
|
||||
<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 }}
|
||||
</div>
|
||||
<div class="options">
|
||||
|
|
|
@ -35,11 +35,11 @@
|
|||
{{ define "forum_category_options" }}
|
||||
<div class="options">
|
||||
{{ 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 */}}
|
||||
<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 }}
|
||||
<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 }}
|
||||
</div>
|
||||
<div class="options">
|
||||
|
|
|
@ -120,7 +120,7 @@
|
|||
{{ else if .User }}
|
||||
<a class="button" href="{{ .ReplyUrl }}">⤷ Reply to Thread</a>
|
||||
{{ 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 }}
|
||||
</div>
|
||||
<div class="options order-0 order-last-ns">
|
||||
|
|
|
@ -1,35 +1,35 @@
|
|||
<footer class="pa3 pa4-l">
|
||||
<h2>
|
||||
Community by <a href="{{ url "/" }}">handmade.network</a>
|
||||
Community by <a href="{{ .Footer.HomepageUrl }}">handmade.network</a>
|
||||
</h2>
|
||||
<ul class="list">
|
||||
{{ $footerClasses := "ma0 pa0 dib-ns" }}
|
||||
<li class="{{ $footerClasses }}">
|
||||
<a href="{{ url "/" }}">Main Page</a>
|
||||
<a href="{{ .Footer.HomepageUrl }}">Main Page</a>
|
||||
</li>
|
||||
<li class="{{ $footerClasses }}">
|
||||
<a href="{{ url "/about" }}">About</a>
|
||||
<a href="{{ .Footer.AboutUrl }}">About</a>
|
||||
</li>
|
||||
<li class="{{ $footerClasses }}">
|
||||
<a href="{{ url "/manifesto" }}">Handmade Manifesto</a>
|
||||
<a href="{{ .Footer.ManifestoUrl }}">Handmade Manifesto</a>
|
||||
</li>
|
||||
<li class="{{ $footerClasses }}">
|
||||
<a href="{{ url "/code-of-conduct" }}">Code of Conduct</a>
|
||||
<a href="{{ .Footer.CodeOfConductUrl }}">Code of Conduct</a>
|
||||
</li>
|
||||
<li class="{{ $footerClasses }}">
|
||||
<a href="{{ url "/communication-guidelines" }}">Communication Guidelines</a>
|
||||
<a href="{{ .Footer.CommunicationGuidelinesUrl }}">Communication Guidelines</a>
|
||||
</li>
|
||||
<li class="{{ $footerClasses }}">
|
||||
<a href="{{ url "/projects" }}">Project Index</a>
|
||||
<a href="{{ .Footer.ProjectIndexUrl }}">Project Index</a>
|
||||
</li>
|
||||
<li class="{{ $footerClasses }}">
|
||||
<a href="{{ url "/forums" }}">Community Forums</a>
|
||||
<a href="{{ .Footer.ForumsUrl }}">Community Forums</a>
|
||||
</li>
|
||||
<li class="{{ $footerClasses }}">
|
||||
<a href="{{ url "/contact" }}">Contact</a>
|
||||
<a href="{{ .Footer.ContactUrl }}">Contact</a>
|
||||
</li>
|
||||
<li class="{{ $footerClasses }}">
|
||||
<a href="{{ url "/sitemap" }}">Sitemap</a>
|
||||
<a href="{{ .Footer.SitemapUrl }}">Sitemap</a>
|
||||
</li>
|
||||
</ul>
|
||||
</footer>
|
||||
|
|
|
@ -3,15 +3,15 @@
|
|||
<div class="user-options flex justify-center justify-end-ns">
|
||||
{{ if .User }}
|
||||
{{ 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 }}
|
||||
<a class="username settings" href="{{ url "/member_settings" }}"><span class="icon-settings"></span> {{ .User.Username }}</a>
|
||||
<a class="logout" href="{{ url "/logout" }}"><span class="icon-logout"></span> Logout</a>
|
||||
<a class="username settings" href="{{ .Header.MemberSettingsUrl }}"><span class="icon-settings"></span> {{ .User.Username }}</a>
|
||||
<a class="logout" href="{{ .Header.LogoutActionUrl }}"><span class="icon-logout"></span> Logout</a>
|
||||
{{ else }}
|
||||
<a class="register" id="register-link" href="{{ url "/member_register" }}">Register</a>
|
||||
<a class="login" id="login-link" href="{{ projecturl "/login" .Project }}">Log in</a>
|
||||
<a class="register" id="register-link" href="{{ .Header.RegisterUrl }}">Register</a>
|
||||
<a class="login" id="login-link" href="{{ .LoginPageUrl }}">Log in</a>
|
||||
<div id="login-popup">
|
||||
<form action="{{ projecturl "/login" .Project }}" method="post">
|
||||
<form action="{{ .Header.LoginActionUrl }}" method="post">
|
||||
{{/* TODO: CSRF */}}
|
||||
<table>
|
||||
<tr>
|
||||
|
@ -33,41 +33,41 @@
|
|||
</div>
|
||||
<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">
|
||||
<a href="{{ url "/" }}" class="logo hmdev-logo">
|
||||
<a href="{{ .Header.HMNHomepageUrl }}" class="logo hmdev-logo">
|
||||
<div class="underscore"></div>
|
||||
</a>
|
||||
<div class="items flex items-center justify-center justify-start-ns">
|
||||
{{ if not .Project.IsHMN }}
|
||||
<a class="project-logo" href="{{ projecturl "/" .Project }}">
|
||||
<a class="project-logo" href="{{ .Header.ProjectHomepageUrl }}">
|
||||
<h1>{{ .Project.Name }}</h1>
|
||||
</a>
|
||||
{{ end }}
|
||||
{{ if .Project.HasBlog }}
|
||||
<a href="{{ projecturl "/blog" .Project }}" class="blog">Blog</a>
|
||||
<a href="{{ .Header.BlogUrl }}" class="blog">Blog</a>
|
||||
{{ end }}
|
||||
{{ if .Project.HasForum }}
|
||||
<a href="{{ projecturl "/forums" .Project }}" class="forums">Forums</a>
|
||||
<a href="{{ .Header.ForumsUrl }}" class="forums">Forums</a>
|
||||
{{ end }}
|
||||
{{ if .Project.HasWiki }}
|
||||
<a href="{{ projecturl "/wiki" .Project }}" class="wiki">Wiki</a>
|
||||
<a href="{{ .Header.WikiUrl }}" class="wiki">Wiki</a>
|
||||
{{ end }}
|
||||
{{ if .Project.HasLibrary }}
|
||||
<a href="{{ projecturl "/library" .Project }}" class="library">Library</a>
|
||||
<a href="{{ .Header.LibraryUrl }}" class="library">Library</a>
|
||||
{{ end }}
|
||||
{{ if .Project.IsHMN }}
|
||||
<a href="{{ projecturl "/manifesto" .Project }}" class="misson">Mission</a>
|
||||
<a href="{{ .Header.ManifestoUrl }}" class="misson">Mission</a>
|
||||
{{ end }}
|
||||
{{/* {% if project.default_annotation_category %} */}}
|
||||
{{ 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 }}
|
||||
{{/* {% if showEditLink == True %} */}}
|
||||
{{/* {{ 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 }} */}}
|
||||
</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? */}}
|
||||
<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"/>
|
||||
|
|
|
@ -35,25 +35,25 @@
|
|||
<h2 class="di-l mr2-l">Around the Network</h2>
|
||||
<ul class="list dib-l">
|
||||
<li class="dib-ns ma0 ph2">
|
||||
<a href="{{ url "/feed" }}">View all posts on HMN</a>
|
||||
<a href="{{ .FeedUrl }}">View all posts on HMN</a>
|
||||
</li>
|
||||
<li class="dib-ns ma0 ph2">
|
||||
<a href="{{ url "/podcast" }}">Podcast</a>
|
||||
<a href="{{ .PodcastUrl }}">Podcast</a>
|
||||
</li>
|
||||
{{/* TODO: Make a better IRC intro page because the current one is trash anyway */}}
|
||||
{{/*
|
||||
<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 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 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 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>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -78,7 +78,7 @@
|
|||
{{ $c1 := hex2color $proj.Color1 }}
|
||||
<a
|
||||
class="project-title"
|
||||
href="{{ projecturl "/" $proj }}"
|
||||
href="{{ $proj.Url }}"
|
||||
>
|
||||
<h2 class="ph3">{{ $proj.Name }}</h2>
|
||||
</a>
|
||||
|
@ -92,7 +92,7 @@
|
|||
{{ end }}
|
||||
|
||||
<div class="ph3 thread unread more">
|
||||
<a class="title" href="{{ projecturl "/forums" $proj }}">
|
||||
<a class="title" href="{{ $entry.ForumsUrl }}">
|
||||
More posts →
|
||||
</a>
|
||||
</div>
|
||||
|
@ -113,7 +113,7 @@
|
|||
<h2 class="di-l mr2-l">Community Showcase</h2>
|
||||
<ul class="list dib-l">
|
||||
<li class="dib-ns ma0 ph2">
|
||||
<a href="{% url 'showcase' %}">View all</a>
|
||||
<a href="{{ .ShowcaseUrl }}">View all</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -129,7 +129,7 @@
|
|||
</div>
|
||||
|
||||
<div class="c--dimmer i pv2 ph3 ph0-ns">
|
||||
This is a selection of recent work done by community members. Want to participate? <a href="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>
|
||||
<script>
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
</style>
|
||||
{{ block "extrahead" . }}{{ end }}
|
||||
<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="60x60" href="{{ static "apple-icon-60x60.png" }}">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="{{ static "apple-icon-72x72.png" }}">
|
||||
|
|
|
@ -14,8 +14,44 @@ type BaseData struct {
|
|||
BodyClasses []string
|
||||
Breadcrumbs []Breadcrumb
|
||||
|
||||
LoginPageUrl string
|
||||
ProjectCSSUrl string
|
||||
|
||||
Project Project
|
||||
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 {
|
||||
|
@ -54,6 +90,7 @@ type Project struct {
|
|||
Subdomain string
|
||||
Color1 string
|
||||
Color2 string
|
||||
Url string
|
||||
|
||||
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 {
|
||||
templates.BaseData
|
||||
|
||||
Posts []templates.PostListItem
|
||||
Pagination templates.Pagination
|
||||
Posts []templates.PostListItem
|
||||
Pagination templates.Pagination
|
||||
AtomFeedUrl string
|
||||
MarkAllReadUrl string
|
||||
}
|
||||
|
||||
func Feed(c *RequestContext) ResponseData {
|
||||
|
@ -76,13 +78,14 @@ func Feed(c *RequestContext) ResponseData {
|
|||
|
||||
c.Perf.StartBlock("SQL", "Fetch posts")
|
||||
type feedPostQuery struct {
|
||||
Post models.Post `db:"post"`
|
||||
Thread models.Thread `db:"thread"`
|
||||
Cat models.Category `db:"cat"`
|
||||
Proj models.Project `db:"proj"`
|
||||
User models.User `db:"auth_user"`
|
||||
ThreadLastReadTime *time.Time `db:"tlri.lastread"`
|
||||
CatLastReadTime *time.Time `db:"clri.lastread"`
|
||||
Post models.Post `db:"post"`
|
||||
Thread models.Thread `db:"thread"`
|
||||
Cat models.Category `db:"cat"`
|
||||
Proj models.Project `db:"proj"`
|
||||
LibraryResource *models.LibraryResource `db:"lib_resource"`
|
||||
User models.User `db:"auth_user"`
|
||||
ThreadLastReadTime *time.Time `db:"tlri.lastread"`
|
||||
CatLastReadTime *time.Time `db:"clri.lastread"`
|
||||
}
|
||||
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_category AS cat ON cat.id = post.category_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
|
||||
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
|
||||
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
|
||||
post.category_kind = ANY ($2)
|
||||
AND post.deleted = FALSE
|
||||
|
@ -123,22 +127,6 @@ func Feed(c *RequestContext) ResponseData {
|
|||
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
||||
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")
|
||||
var postItems []templates.PostListItem
|
||||
for _, iPostResult := range posts.ToSlice() {
|
||||
|
@ -151,42 +139,16 @@ func Feed(c *RequestContext) ResponseData {
|
|||
hasRead = true
|
||||
}
|
||||
|
||||
breadcrumbs := make([]templates.Breadcrumb, 0, len(lineageBuilder.GetLineage(postResult.Cat.ID)))
|
||||
breadcrumbs = append(breadcrumbs, templates.Breadcrumb{
|
||||
Name: postResult.Proj.Name,
|
||||
Url: hmnurl.ProjectUrl("/", nil, postResult.Proj.Slug),
|
||||
})
|
||||
if postResult.Post.CategoryKind == models.CatKindLibraryResource {
|
||||
// TODO(asaf): Fetch library root topic for the project and construct breadcrumb for it
|
||||
} else {
|
||||
lineage := lineageBuilder.GetLineage(postResult.Cat.ID)
|
||||
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,
|
||||
})
|
||||
postItems = append(postItems, MakePostListItem(
|
||||
lineageBuilder,
|
||||
&postResult.Proj,
|
||||
&postResult.Thread,
|
||||
&postResult.Post,
|
||||
&postResult.User,
|
||||
postResult.LibraryResource,
|
||||
!hasRead,
|
||||
true,
|
||||
))
|
||||
}
|
||||
c.Perf.EndBlock()
|
||||
|
||||
|
@ -197,8 +159,10 @@ func Feed(c *RequestContext) ResponseData {
|
|||
res.WriteTemplate("feed.html", FeedData{
|
||||
BaseData: baseData,
|
||||
|
||||
Posts: postItems,
|
||||
Pagination: pagination,
|
||||
AtomFeedUrl: hmnurl.BuildAtomFeed(),
|
||||
MarkAllReadUrl: hmnurl.BuildMarkRead(0),
|
||||
Posts: postItems,
|
||||
Pagination: pagination,
|
||||
}, c.Perf)
|
||||
|
||||
return res
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
package website
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
@ -16,13 +14,13 @@ import (
|
|||
"git.handmade.network/hmn/hmn/src/oops"
|
||||
"git.handmade.network/hmn/hmn/src/templates"
|
||||
"git.handmade.network/hmn/hmn/src/utils"
|
||||
"github.com/jackc/pgx/v4/pgxpool"
|
||||
)
|
||||
|
||||
type forumCategoryData struct {
|
||||
templates.BaseData
|
||||
|
||||
CategoryUrl string
|
||||
NewThreadUrl string
|
||||
MarkReadUrl string
|
||||
Threads []templates.ThreadListItem
|
||||
Pagination templates.Pagination
|
||||
Subcategories []forumSubcategoryData
|
||||
|
@ -38,19 +36,17 @@ type forumSubcategoryData struct {
|
|||
func ForumCategory(c *RequestContext) ResponseData {
|
||||
const threadsPerPage = 25
|
||||
|
||||
// TODO(asaf): Consider making this more robust.
|
||||
// Right now this code allows for weird urls like:
|
||||
// "/forums/asdf/wip" which doesn't verify the lineage and displays the wip forums
|
||||
// "/forums/wip///" which fetches the main forums page because it happens to have a blank slug
|
||||
// "/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
|
||||
// "/forums/wip/1/" 404 - doesn't match the regex
|
||||
// "/forums/" 404 - doesn't match the regex
|
||||
catPath := c.PathParams["cats"]
|
||||
catSlugs := strings.Split(catPath, "/")
|
||||
currentCatId := fetchCatIdFromSlugs(c.Context(), c.Conn, catSlugs, c.CurrentProject.ID)
|
||||
// TODO(asaf): 404 if we can't find our cat.
|
||||
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)
|
||||
c.Perf.EndBlock()
|
||||
|
||||
currentCatId, valid := validateSubforums(lineageBuilder, c.CurrentProject, c.PathParams["cats"])
|
||||
if !valid {
|
||||
return FourOhFour(c)
|
||||
}
|
||||
|
||||
currentSubforumSlugs := lineageBuilder.GetSubforumLineageSlugs(currentCatId)
|
||||
|
||||
c.Perf.StartBlock("SQL", "Fetch count of page threads")
|
||||
numThreads, err := db.QueryInt(c.Context(), c.Conn,
|
||||
|
@ -76,11 +72,11 @@ func ForumCategory(c *RequestContext) ResponseData {
|
|||
if pageParsed, err := strconv.Atoi(pageString); err == nil {
|
||||
page = pageParsed
|
||||
} 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 {
|
||||
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
|
||||
|
@ -143,10 +139,8 @@ func ForumCategory(c *RequestContext) ResponseData {
|
|||
}
|
||||
|
||||
return templates.ThreadListItem{
|
||||
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: ThreadUrl(row.Thread, models.CatKindForum, categoryUrls[currentCatId]),
|
||||
|
||||
Title: row.Thread.Title,
|
||||
Url: hmnurl.BuildForumThread(c.CurrentProject.Slug, lineageBuilder.GetSubforumLineageSlugs(row.Thread.CategoryID), row.Thread.ID, row.Thread.Title, 1),
|
||||
FirstUser: templates.UserToTemplate(row.FirstUser),
|
||||
FirstDate: row.FirstPost.PostDate,
|
||||
LastUser: templates.UserToTemplate(row.LastUser),
|
||||
|
@ -168,29 +162,9 @@ func ForumCategory(c *RequestContext) ResponseData {
|
|||
|
||||
var subcats []forumSubcategoryData
|
||||
if page == 1 {
|
||||
c.Perf.StartBlock("SQL", "Fetch subcategories")
|
||||
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)
|
||||
subcatNodes := categoryTree[currentCatId].Children
|
||||
|
||||
for _, catNode := range subcatNodes {
|
||||
c.Perf.StartBlock("SQL", "Fetch count of subcategory threads")
|
||||
// TODO(asaf): [PERF] [MINOR] Consider replacing querying count per subcat with a single query for all cats with GROUP BY.
|
||||
numThreads, err := db.QueryInt(c.Context(), c.Conn,
|
||||
|
@ -201,7 +175,7 @@ func ForumCategory(c *RequestContext) ResponseData {
|
|||
thread.category_id = $1
|
||||
AND NOT thread.deleted
|
||||
`,
|
||||
catRow.Cat.ID,
|
||||
catNode.ID,
|
||||
)
|
||||
if err != nil {
|
||||
panic(oops.New(err, "failed to get count of threads"))
|
||||
|
@ -233,7 +207,7 @@ func ForumCategory(c *RequestContext) ResponseData {
|
|||
ORDER BY lastpost.postdate DESC
|
||||
LIMIT 3
|
||||
`,
|
||||
catRow.Cat.ID,
|
||||
catNode.ID,
|
||||
currentUserId,
|
||||
)
|
||||
if err != nil {
|
||||
|
@ -249,8 +223,8 @@ func ForumCategory(c *RequestContext) ResponseData {
|
|||
}
|
||||
|
||||
subcats = append(subcats, forumSubcategoryData{
|
||||
Name: *catRow.Cat.Name,
|
||||
Url: categoryUrls[catRow.Cat.ID],
|
||||
Name: *catNode.Name,
|
||||
Url: hmnurl.BuildForumCategory(c.CurrentProject.Slug, lineageBuilder.GetSubforumLineageSlugs(catNode.ID), 1),
|
||||
Threads: threads,
|
||||
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.
|
||||
{
|
||||
Name: c.CurrentProject.Name,
|
||||
Url: hmnurl.ProjectUrl("/", nil, c.CurrentProject.Slug),
|
||||
Url: hmnurl.BuildProjectHomepage(c.CurrentProject.Slug),
|
||||
},
|
||||
{
|
||||
Name: "Forums",
|
||||
Url: categoryUrls[currentCatId],
|
||||
Url: hmnurl.BuildForumCategory(c.CurrentProject.Slug, nil, 1),
|
||||
Current: true,
|
||||
},
|
||||
}
|
||||
|
||||
currentSubforums := lineageBuilder.GetSubforumLineage(currentCatId)
|
||||
for i, subforum := range currentSubforums {
|
||||
baseData.Breadcrumbs = append(baseData.Breadcrumbs, templates.Breadcrumb{
|
||||
Name: *subforum.Name, // NOTE(asaf): All subforum categories must have names.
|
||||
Url: hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs[0:i+1], 1),
|
||||
})
|
||||
}
|
||||
|
||||
var res ResponseData
|
||||
err = res.WriteTemplate("forum_category.html", forumCategoryData{
|
||||
BaseData: baseData,
|
||||
CategoryUrl: categoryUrls[currentCatId],
|
||||
Threads: threads,
|
||||
BaseData: baseData,
|
||||
NewThreadUrl: hmnurl.BuildForumNewThread(c.CurrentProject.Slug, currentSubforumSlugs),
|
||||
MarkReadUrl: hmnurl.BuildMarkRead(currentCatId),
|
||||
Threads: threads,
|
||||
Pagination: templates.Pagination{
|
||||
Current: page,
|
||||
Total: numPages,
|
||||
|
||||
FirstUrl: categoryUrls[currentCatId],
|
||||
LastUrl: fmt.Sprintf("%s/%d", categoryUrls[currentCatId], numPages),
|
||||
NextUrl: fmt.Sprintf("%s/%d", categoryUrls[currentCatId], page+1),
|
||||
PreviousUrl: fmt.Sprintf("%s/%d", categoryUrls[currentCatId], page-1),
|
||||
FirstUrl: hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs, 1),
|
||||
LastUrl: hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs, numPages),
|
||||
NextUrl: hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs, utils.IntClamp(1, page+1, numPages)),
|
||||
PreviousUrl: hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs, utils.IntClamp(1, page-1, numPages)),
|
||||
},
|
||||
Subcategories: subcats,
|
||||
}, c.Perf)
|
||||
|
@ -312,11 +295,23 @@ type forumThreadData struct {
|
|||
func ForumThread(c *RequestContext) ResponseData {
|
||||
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"])
|
||||
if err != nil {
|
||||
return FourOhFour(c)
|
||||
}
|
||||
|
||||
currentSubforumSlugs := lineageBuilder.GetSubforumLineageSlugs(currentCatId)
|
||||
|
||||
c.Perf.StartBlock("SQL", "Fetch current thread")
|
||||
type threadQueryResult struct {
|
||||
Thread models.Thread `db:"thread"`
|
||||
|
@ -330,10 +325,10 @@ func ForumThread(c *RequestContext) ResponseData {
|
|||
WHERE
|
||||
thread.id = $1
|
||||
AND NOT thread.deleted
|
||||
AND cat.project_id = $2
|
||||
AND cat.id = $2
|
||||
`,
|
||||
threadId,
|
||||
c.CurrentProject.ID,
|
||||
currentCatId, // NOTE(asaf): This verifies that the requested thread is under the requested subforum.
|
||||
)
|
||||
c.Perf.EndBlock()
|
||||
if err != nil {
|
||||
|
@ -345,14 +340,6 @@ func ForumThread(c *RequestContext) ResponseData {
|
|||
}
|
||||
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,
|
||||
`
|
||||
SELECT COUNT(*)
|
||||
|
@ -368,17 +355,17 @@ func ForumThread(c *RequestContext) ResponseData {
|
|||
}
|
||||
page, numPages, ok := getPageInfo(c.PathParams["page"], numPosts, postsPerPage)
|
||||
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)
|
||||
}
|
||||
pagination := templates.Pagination{
|
||||
Current: page,
|
||||
Total: numPages,
|
||||
|
||||
FirstUrl: hmnurl.BuildForumThread(c.CurrentProject.Slug, subforums, thread.ID, 1),
|
||||
LastUrl: hmnurl.BuildForumThread(c.CurrentProject.Slug, subforums, thread.ID, numPages),
|
||||
NextUrl: hmnurl.BuildForumThread(c.CurrentProject.Slug, subforums, thread.ID, utils.IntClamp(1, page+1, numPages)),
|
||||
PreviousUrl: hmnurl.BuildForumThread(c.CurrentProject.Slug, subforums, thread.ID, utils.IntClamp(1, page-1, numPages)),
|
||||
FirstUrl: hmnurl.BuildForumThread(c.CurrentProject.Slug, currentSubforumSlugs, thread.ID, thread.Title, 1),
|
||||
LastUrl: hmnurl.BuildForumThread(c.CurrentProject.Slug, currentSubforumSlugs, thread.ID, thread.Title, numPages),
|
||||
NextUrl: hmnurl.BuildForumThread(c.CurrentProject.Slug, currentSubforumSlugs, thread.ID, thread.Title, utils.IntClamp(1, page+1, numPages)),
|
||||
PreviousUrl: hmnurl.BuildForumThread(c.CurrentProject.Slug, currentSubforumSlugs, thread.ID, thread.Title, utils.IntClamp(1, page-1, numPages)),
|
||||
}
|
||||
|
||||
c.Perf.StartBlock("SQL", "Fetch posts")
|
||||
|
@ -418,7 +405,7 @@ func ForumThread(c *RequestContext) ResponseData {
|
|||
|
||||
post := templates.PostToTemplate(&row.Post, row.Author)
|
||||
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)
|
||||
}
|
||||
|
@ -432,8 +419,8 @@ func ForumThread(c *RequestContext) ResponseData {
|
|||
BaseData: baseData,
|
||||
Thread: templates.ThreadToTemplate(&thread),
|
||||
Posts: posts,
|
||||
CategoryUrl: categoryUrls[thread.CategoryID],
|
||||
ReplyUrl: hmnurl.BuildForumPostReply(c.CurrentProject.Slug, subforums, thread.ID, *thread.FirstID),
|
||||
CategoryUrl: hmnurl.BuildForumCategory(c.CurrentProject.Slug, currentSubforumSlugs, 1),
|
||||
ReplyUrl: hmnurl.BuildForumPostReply(c.CurrentProject.Slug, currentSubforumSlugs, thread.ID, *thread.FirstID),
|
||||
Pagination: pagination,
|
||||
}, c.Perf)
|
||||
if err != nil {
|
||||
|
@ -443,46 +430,36 @@ func ForumThread(c *RequestContext) ResponseData {
|
|||
return res
|
||||
}
|
||||
|
||||
func fetchCatIdFromSlugs(ctx context.Context, conn *pgxpool.Pool, catSlugs []string, projectId int) int {
|
||||
if len(catSlugs) == 1 {
|
||||
var err error
|
||||
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
|
||||
func validateSubforums(lineageBuilder *models.CategoryLineageBuilder, project *models.Project, catPath string) (int, bool) {
|
||||
if project.ForumID == nil {
|
||||
return -1, false
|
||||
}
|
||||
|
||||
subforumCatId := *project.ForumID
|
||||
if len(catPath) == 0 {
|
||||
return subforumCatId, true
|
||||
}
|
||||
|
||||
catPath = strings.ToLower(catPath)
|
||||
valid := false
|
||||
catSlugs := strings.Split(catPath, "/")
|
||||
lastSlug := catSlugs[len(catSlugs)-1]
|
||||
if len(lastSlug) > 0 {
|
||||
lastSlugCatId := lineageBuilder.FindIdBySlug(project.ID, lastSlug)
|
||||
if lastSlugCatId != -1 {
|
||||
subforumSlugs := lineageBuilder.GetSubforumLineageSlugs(lastSlugCatId)
|
||||
allMatch := true
|
||||
for i, subforum := range subforumSlugs {
|
||||
if subforum != catSlugs[i] {
|
||||
allMatch = false
|
||||
break
|
||||
}
|
||||
}
|
||||
valid = allMatch
|
||||
}
|
||||
if valid {
|
||||
subforumCatId = lastSlugCatId
|
||||
}
|
||||
}
|
||||
return subforumCatId, valid
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"time"
|
||||
|
||||
"git.handmade.network/hmn/hmn/src/db"
|
||||
"git.handmade.network/hmn/hmn/src/hmnurl"
|
||||
"git.handmade.network/hmn/hmn/src/models"
|
||||
"git.handmade.network/hmn/hmn/src/oops"
|
||||
"git.handmade.network/hmn/hmn/src/templates"
|
||||
|
@ -18,12 +19,21 @@ type LandingTemplateData struct {
|
|||
NewsPost LandingPageFeaturedPost
|
||||
PostColumns [][]LandingPageProject
|
||||
ShowcaseTimelineJson string
|
||||
|
||||
FeedUrl string
|
||||
PodcastUrl string
|
||||
StreamsUrl string
|
||||
IRCUrl string
|
||||
DiscordUrl string
|
||||
ShowUrl string
|
||||
ShowcaseUrl string
|
||||
}
|
||||
|
||||
type LandingPageProject struct {
|
||||
Project templates.Project
|
||||
FeaturedPost *LandingPageFeaturedPost
|
||||
Posts []templates.PostListItem
|
||||
ForumsUrl string
|
||||
}
|
||||
|
||||
type LandingPageFeaturedPost struct {
|
||||
|
@ -45,11 +55,12 @@ func Index(c *RequestContext) ResponseData {
|
|||
SELECT $columns
|
||||
FROM handmade_project
|
||||
WHERE
|
||||
flags = 0
|
||||
OR id = $1
|
||||
(flags = 0 AND NOT lifecycle = ANY($1))
|
||||
OR id = $2
|
||||
ORDER BY all_last_updated DESC
|
||||
LIMIT $2
|
||||
LIMIT $3
|
||||
`,
|
||||
[]models.ProjectLifecycle{models.ProjectLifecycleUnapproved, models.ProjectLifecycleApprovalRequired},
|
||||
models.HMNProjectID,
|
||||
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()
|
||||
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
|
||||
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))
|
||||
type projectPostQuery struct {
|
||||
Post models.Post `db:"post"`
|
||||
Thread models.Thread `db:"thread"`
|
||||
User models.User `db:"auth_user"`
|
||||
ThreadLastReadTime *time.Time `db:"tlri.lastread"`
|
||||
CatLastReadTime *time.Time `db:"clri.lastread"`
|
||||
Post models.Post `db:"post"`
|
||||
Thread models.Thread `db:"thread"`
|
||||
User models.User `db:"auth_user"`
|
||||
LibraryResource *models.LibraryResource `db:"lib_resource"`
|
||||
ThreadLastReadTime *time.Time `db:"tlri.lastread"`
|
||||
CatLastReadTime *time.Time `db:"clri.lastread"`
|
||||
}
|
||||
projectPostIter, err := db.Query(c.Context(), c.Conn, projectPostQuery{},
|
||||
`
|
||||
|
@ -98,6 +112,7 @@ func Index(c *RequestContext) ResponseData {
|
|||
AND clri.user_id = $1
|
||||
)
|
||||
LEFT JOIN auth_user ON post.author_id = auth_user.id
|
||||
LEFT JOIN handmade_libraryresource as lib_resource ON lib_resource.category_id = post.category_id
|
||||
WHERE
|
||||
post.project_id = $2
|
||||
AND post.category_kind IN ($3, $4, $5, $6)
|
||||
|
@ -117,8 +132,16 @@ func Index(c *RequestContext) ResponseData {
|
|||
}
|
||||
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{
|
||||
Project: templates.ProjectToTemplate(proj),
|
||||
Project: templates.ProjectToTemplate(proj),
|
||||
ForumsUrl: forumsUrl,
|
||||
}
|
||||
|
||||
for _, projectPostRow := range projectPosts {
|
||||
|
@ -150,28 +173,35 @@ func Index(c *RequestContext) ResponseData {
|
|||
WHERE
|
||||
post.id = $1
|
||||
`, projectPost.Post.ID)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
c.Perf.EndBlock()
|
||||
if err != nil {
|
||||
c.Logger.Error().Err(err).Msg("failed to fetch featured post content")
|
||||
continue
|
||||
}
|
||||
content := contentResult.(*featuredContentResult).Content
|
||||
|
||||
landingPageProject.FeaturedPost = &LandingPageFeaturedPost{
|
||||
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),
|
||||
Date: projectPost.Post.PostDate,
|
||||
Unread: !hasRead,
|
||||
Content: template.HTML(content),
|
||||
}
|
||||
} else {
|
||||
landingPageProject.Posts = append(landingPageProject.Posts, templates.PostListItem{
|
||||
Title: projectPost.Thread.Title,
|
||||
Url: PostUrl(projectPost.Post, projectPost.Post.CategoryKind, categoryUrls[projectPost.Post.CategoryID]),
|
||||
User: templates.UserToTemplate(&projectPost.User),
|
||||
Date: projectPost.Post.PostDate,
|
||||
Unread: !hasRead,
|
||||
})
|
||||
landingPageProject.Posts = append(
|
||||
landingPageProject.Posts,
|
||||
MakePostListItem(
|
||||
lineageBuilder,
|
||||
proj,
|
||||
&projectPost.Thread,
|
||||
&projectPost.Post,
|
||||
&projectPost.User,
|
||||
projectPost.LibraryResource,
|
||||
!hasRead,
|
||||
false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -185,30 +215,6 @@ func Index(c *RequestContext) ResponseData {
|
|||
}
|
||||
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.
|
||||
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 {
|
||||
Post models.Post `db:"post"`
|
||||
PostVersion models.PostVersion `db:"ver"`
|
||||
|
@ -251,12 +258,11 @@ func Index(c *RequestContext) ResponseData {
|
|||
FROM
|
||||
handmade_post AS post
|
||||
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 handmade_postversion AS ver ON post.current_id = ver.id
|
||||
WHERE
|
||||
cat.project_id = $1
|
||||
AND cat.kind = $2
|
||||
post.project_id = $1
|
||||
AND post.category_kind = $2
|
||||
AND post.id = thread.first_id
|
||||
AND NOT thread.deleted
|
||||
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"))
|
||||
}
|
||||
newsPostResult := newsPostRow.(*newsPostQuery)
|
||||
c.Perf.EndBlock()
|
||||
|
||||
baseData := getBaseData(c)
|
||||
baseData.BodyClasses = append(baseData.BodyClasses, "hmdev", "landing") // TODO: Is "hmdev" necessary any more?
|
||||
|
||||
var res ResponseData
|
||||
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{
|
||||
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),
|
||||
Date: newsPostResult.Post.PostDate,
|
||||
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.
|
||||
routes.POST(hmnurl.RegexLogin, Login)
|
||||
routes.GET(hmnurl.RegexLogout, Logout)
|
||||
routes.POST(hmnurl.RegexLoginAction, Login)
|
||||
routes.GET(hmnurl.RegexLogoutAction, Logout)
|
||||
routes.StdHandler(hmnurl.RegexPublic,
|
||||
http.StripPrefix("/public/", http.FileServer(http.Dir("public"))),
|
||||
)
|
||||
|
@ -130,9 +130,39 @@ func getBaseData(c *RequestContext) templates.BaseData {
|
|||
}
|
||||
|
||||
return templates.BaseData{
|
||||
Project: templates.ProjectToTemplate(c.CurrentProject),
|
||||
User: templateUser,
|
||||
Theme: "light",
|
||||
Project: templates.ProjectToTemplate(c.CurrentProject),
|
||||
LoginPageUrl: hmnurl.BuildLoginPage(c.FullUrl()),
|
||||
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()
|
||||
log := logging.Info()
|
||||
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]) {
|
||||
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)
|
||||
}
|
||||
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) {
|
||||
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
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.handmade.network/hmn/hmn/src/db"
|
||||
"git.handmade.network/hmn/hmn/src/hmnurl"
|
||||
"git.handmade.network/hmn/hmn/src/models"
|
||||
"github.com/jackc/pgx/v4/pgxpool"
|
||||
)
|
||||
|
||||
type categoryUrlQueryResult struct {
|
||||
Cat models.Category `db:"cat"`
|
||||
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.
|
||||
func BuildProjectMainCategoryUrl(projectSlug string, kind models.CategoryKind) string {
|
||||
switch kind {
|
||||
case models.CatKindBlog:
|
||||
return fmt.Sprintf("%s/p/%d/e/%d", categoryUrl, post.ThreadID, post.ID)
|
||||
return hmnurl.BuildBlog(projectSlug, 1)
|
||||
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 ""
|
||||
}
|
||||
|
||||
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 ""
|
||||
return hmnurl.BuildProjectHomepage(projectSlug)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue