Part 1 of URL robustification
This commit is contained in:
parent
dca101fd20
commit
06bbc2b9cc
|
@ -18,7 +18,11 @@ type Q struct {
|
||||||
var baseUrlParsed url.URL
|
var baseUrlParsed url.URL
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
parsed, err := url.Parse(config.Config.BaseUrl)
|
SetGlobalBaseUrl(config.Config.BaseUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetGlobalBaseUrl(fullBaseUrl string) {
|
||||||
|
parsed, err := url.Parse(fullBaseUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(oops.New(err, "could not parse base URL"))
|
panic(oops.New(err, "could not parse base URL"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package hmnurl
|
package hmnurl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.handmade.network/hmn/hmn/src/config"
|
"git.handmade.network/hmn/hmn/src/config"
|
||||||
|
@ -8,10 +10,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUrl(t *testing.T) {
|
func TestUrl(t *testing.T) {
|
||||||
defer func(original string) {
|
defer func() {
|
||||||
config.Config.BaseUrl = original
|
SetGlobalBaseUrl(config.Config.BaseUrl)
|
||||||
}(config.Config.BaseUrl)
|
}()
|
||||||
config.Config.BaseUrl = "http://handmade.test"
|
SetGlobalBaseUrl("http://handmade.test")
|
||||||
|
|
||||||
t.Run("no query", func(t *testing.T) {
|
t.Run("no query", func(t *testing.T) {
|
||||||
result := Url("/test/foo", nil)
|
result := Url("/test/foo", nil)
|
||||||
|
@ -22,3 +24,123 @@ func TestUrl(t *testing.T) {
|
||||||
assert.Equal(t, "http://handmade.test/test/foo?bar=baz&zig%3F%3F=zig+%26+zag%21%21", result)
|
assert.Equal(t, "http://handmade.test/test/foo?bar=baz&zig%3F%3F=zig+%26+zag%21%21", result)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHomepage(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildHomepage(), RegexHomepage, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogin(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildLogin(), RegexLogin, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogout(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildLogout(), RegexLogout, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStaticPages(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildManifesto(), RegexManifesto, nil)
|
||||||
|
AssertRegexMatch(t, BuildAbout(), RegexAbout, nil)
|
||||||
|
AssertRegexMatch(t, BuildCodeOfConduct(), RegexCodeOfConduct, nil)
|
||||||
|
AssertRegexMatch(t, BuildCommunicationGuidelines(), RegexCommunicationGuidelines, nil)
|
||||||
|
AssertRegexMatch(t, BuildContactPage(), RegexContactPage, nil)
|
||||||
|
AssertRegexMatch(t, BuildMonthlyUpdatePolicy(), RegexMonthlyUpdatePolicy, nil)
|
||||||
|
AssertRegexMatch(t, BuildProjectSubmissionGuidelines(), RegexProjectSubmissionGuidelines, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFeed(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildFeed(), RegexFeed, nil)
|
||||||
|
assert.Equal(t, BuildFeed(), BuildFeedWithPage(1))
|
||||||
|
AssertRegexMatch(t, BuildFeedWithPage(1), RegexFeed, nil)
|
||||||
|
AssertRegexMatch(t, "/feed/1", RegexFeed, nil) // NOTE(asaf): We should never build this URL, but we should still accept it.
|
||||||
|
AssertRegexMatch(t, BuildFeedWithPage(5), RegexFeed, map[string]string{"page": "5"})
|
||||||
|
assert.Panics(t, func() { BuildFeedWithPage(-1) })
|
||||||
|
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"})
|
||||||
|
AssertSubdomain(t, BuildForumCategory("hmn", nil, 1), "")
|
||||||
|
AssertSubdomain(t, BuildForumCategory("", nil, 1), "")
|
||||||
|
AssertSubdomain(t, BuildForumCategory("hero", nil, 1), "hero")
|
||||||
|
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 TestProjectCSS(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildProjectCSS("000000"), RegexProjectCSS, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPublic(t *testing.T) {
|
||||||
|
AssertRegexMatch(t, BuildPublic("test"), RegexPublic, nil)
|
||||||
|
AssertRegexMatch(t, BuildPublic("/test"), RegexPublic, nil)
|
||||||
|
AssertRegexMatch(t, BuildPublic("/test/"), RegexPublic, nil)
|
||||||
|
AssertRegexMatch(t, BuildPublic("/test/thing/image.png"), RegexPublic, nil)
|
||||||
|
assert.Panics(t, func() { BuildPublic("") })
|
||||||
|
assert.Panics(t, func() { BuildPublic("/") })
|
||||||
|
assert.Panics(t, func() { BuildPublic("/thing//image.png") })
|
||||||
|
assert.Panics(t, func() { BuildPublic("/thing/ /image.png") })
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fullHost := parsed.Host
|
||||||
|
if len(expectedSubdomain) == 0 {
|
||||||
|
assert.Equal(t, baseUrlParsed.Host, fullHost, "Did not expect a subdomain")
|
||||||
|
} else {
|
||||||
|
assert.Equalf(t, expectedSubdomain+"."+baseUrlParsed.Host, fullHost, "Subdomain mismatch")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AssertRegexMatch(t *testing.T, fullUrl string, regex *regexp.Regexp, paramsToVerify map[string]string) {
|
||||||
|
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.NotNilf(t, match, "Url did not match regex: [%s] vs [%s]", requestPath, regex.String())
|
||||||
|
|
||||||
|
if paramsToVerify != nil {
|
||||||
|
subexpNames := regex.SubexpNames()
|
||||||
|
for i, matchedValue := range match {
|
||||||
|
paramName := subexpNames[i]
|
||||||
|
expectedValue, ok := paramsToVerify[paramName]
|
||||||
|
if ok {
|
||||||
|
assert.Equalf(t, expectedValue, matchedValue, "Param mismatch for [%s]", paramName)
|
||||||
|
delete(paramsToVerify, paramName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(paramsToVerify) > 0 {
|
||||||
|
unmatchedParams := make([]string, 0, len(paramsToVerify))
|
||||||
|
for paramName := range paramsToVerify {
|
||||||
|
unmatchedParams = append(unmatchedParams, paramName)
|
||||||
|
}
|
||||||
|
assert.Fail(t, "Expected match groups not found", unmatchedParams)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,207 @@
|
||||||
|
package hmnurl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.handmade.network/hmn/hmn/src/oops"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var RegexHomepage *regexp.Regexp = regexp.MustCompile("^/$")
|
||||||
|
|
||||||
|
func BuildHomepage() string {
|
||||||
|
return Url("/", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexLogin *regexp.Regexp = regexp.MustCompile("^/login$")
|
||||||
|
|
||||||
|
func BuildLogin() string {
|
||||||
|
return Url("/login", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexLogout *regexp.Regexp = regexp.MustCompile("^/logout$")
|
||||||
|
|
||||||
|
func BuildLogout() string {
|
||||||
|
return Url("/logout", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexManifesto *regexp.Regexp = regexp.MustCompile("^/manifesto$")
|
||||||
|
|
||||||
|
func BuildManifesto() string {
|
||||||
|
return Url("/manifesto", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexAbout *regexp.Regexp = regexp.MustCompile("^/about$")
|
||||||
|
|
||||||
|
func BuildAbout() string {
|
||||||
|
return Url("/about", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexCodeOfConduct *regexp.Regexp = regexp.MustCompile("^/code-of-conduct$")
|
||||||
|
|
||||||
|
func BuildCodeOfConduct() string {
|
||||||
|
return Url("/code-of-conduct", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexCommunicationGuidelines *regexp.Regexp = regexp.MustCompile("^/communication-guidelines$")
|
||||||
|
|
||||||
|
func BuildCommunicationGuidelines() string {
|
||||||
|
return Url("/communication-guidelines", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexContactPage *regexp.Regexp = regexp.MustCompile("^/contact$")
|
||||||
|
|
||||||
|
func BuildContactPage() string {
|
||||||
|
return Url("/contact", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexMonthlyUpdatePolicy *regexp.Regexp = regexp.MustCompile("^/monthly-update-policy$")
|
||||||
|
|
||||||
|
func BuildMonthlyUpdatePolicy() string {
|
||||||
|
return Url("/monthly-update-policy", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexProjectSubmissionGuidelines *regexp.Regexp = regexp.MustCompile("^/project-guidelines$")
|
||||||
|
|
||||||
|
func BuildProjectSubmissionGuidelines() string {
|
||||||
|
return Url("/project-guidelines", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexFeed *regexp.Regexp = regexp.MustCompile(`^/feed(/(?P<page>.+)?)?$`)
|
||||||
|
|
||||||
|
func BuildFeed() string {
|
||||||
|
return Url("/feed", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildFeedWithPage(page int) string {
|
||||||
|
if page < 1 {
|
||||||
|
panic(oops.New(nil, "Invalid feed page (%d), must be >= 1", page))
|
||||||
|
}
|
||||||
|
if page == 1 {
|
||||||
|
return BuildFeed()
|
||||||
|
}
|
||||||
|
return Url("/feed/"+strconv.Itoa(page), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexForumThread *regexp.Regexp = regexp.MustCompile(`^/(?P<cats>forums(/[^\d]+?)*)/t/(?P<threadid>\d+)(/(?P<page>\d+))?$`)
|
||||||
|
|
||||||
|
func BuildForumThread(projectSlug string, subforums []string, threadId int, page int) string {
|
||||||
|
if projectSlug == "hmn" {
|
||||||
|
projectSlug = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
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.WriteString("/t/")
|
||||||
|
builder.WriteString(strconv.Itoa(threadId))
|
||||||
|
if page > 1 {
|
||||||
|
builder.WriteRune('/')
|
||||||
|
builder.WriteString(strconv.Itoa(page))
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexForumCategory *regexp.Regexp = regexp.MustCompile(`^/(?P<cats>forums(/[^\d]+?)*)(/(?P<page>\d+))?$`)
|
||||||
|
|
||||||
|
func BuildForumCategory(projectSlug string, subforums []string, page int) string {
|
||||||
|
if projectSlug == "hmn" {
|
||||||
|
projectSlug = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if page > 1 {
|
||||||
|
builder.WriteRune('/')
|
||||||
|
builder.WriteString(strconv.Itoa(page))
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexForumPost *regexp.Regexp = regexp.MustCompile(``) // TODO(asaf): Complete this and test it
|
||||||
|
|
||||||
|
func BuildForumPost(projectSlug string, subforums []string, threadId int, postId int) string {
|
||||||
|
if projectSlug == "hmn" {
|
||||||
|
projectSlug = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
return ProjectUrl(builder.String(), nil, projectSlug)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexProjectCSS *regexp.Regexp = regexp.MustCompile("^/assets/project.css$")
|
||||||
|
|
||||||
|
func BuildProjectCSS(color string) string {
|
||||||
|
return Url("/assets/project.css", []Q{Q{"color", color}})
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexPublic *regexp.Regexp = regexp.MustCompile("^/public/.+$")
|
||||||
|
|
||||||
|
func BuildPublic(filepath string) string {
|
||||||
|
filepath = strings.Trim(filepath, "/")
|
||||||
|
if len(strings.TrimSpace(filepath)) == 0 {
|
||||||
|
panic(oops.New(nil, "Attempted to build a /public url with no path"))
|
||||||
|
}
|
||||||
|
var builder strings.Builder
|
||||||
|
builder.WriteString("/public")
|
||||||
|
pathParts := strings.Split(filepath, "/")
|
||||||
|
for _, part := range pathParts {
|
||||||
|
part = strings.TrimSpace(part)
|
||||||
|
if len(part) == 0 {
|
||||||
|
panic(oops.New(nil, "Attempted to build a /public url with blank path segments: %s", filepath))
|
||||||
|
}
|
||||||
|
builder.WriteRune('/')
|
||||||
|
builder.WriteString(part)
|
||||||
|
}
|
||||||
|
return Url(builder.String(), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var RegexCatchAll *regexp.Regexp = regexp.MustCompile("")
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"git.handmade.network/hmn/hmn/src/db"
|
"git.handmade.network/hmn/hmn/src/db"
|
||||||
|
"git.handmade.network/hmn/hmn/src/oops"
|
||||||
"github.com/jackc/pgx/v4/pgxpool"
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -33,6 +34,96 @@ type Category struct {
|
||||||
Depth int `db:"depth"` // TODO: What is this?
|
Depth int `db:"depth"` // TODO: What is this?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CategoryLineageBuilder struct {
|
||||||
|
Tree map[int]*CategoryTreeNode
|
||||||
|
CategoryCache map[int][]*Category
|
||||||
|
SlugCache map[int][]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeCategoryLineageBuilder(fullCategoryTree map[int]*CategoryTreeNode) *CategoryLineageBuilder {
|
||||||
|
return &CategoryLineageBuilder{
|
||||||
|
Tree: fullCategoryTree,
|
||||||
|
CategoryCache: make(map[int][]*Category),
|
||||||
|
SlugCache: make(map[int][]string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cl *CategoryLineageBuilder) GetLineage(catId int) []*Category {
|
||||||
|
_, ok := cl.CategoryCache[catId]
|
||||||
|
if !ok {
|
||||||
|
cl.CategoryCache[catId] = cl.Tree[catId].GetLineage()
|
||||||
|
}
|
||||||
|
return cl.CategoryCache[catId]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cl *CategoryLineageBuilder) GetLineageSlugs(catId int) []string {
|
||||||
|
_, ok := cl.SlugCache[catId]
|
||||||
|
if !ok {
|
||||||
|
lineage := cl.GetLineage(catId)
|
||||||
|
result := make([]string, 0, len(lineage))
|
||||||
|
for _, cat := range lineage {
|
||||||
|
name := ""
|
||||||
|
if cat.Slug != nil {
|
||||||
|
name = *cat.Slug
|
||||||
|
}
|
||||||
|
result = append(result, name)
|
||||||
|
}
|
||||||
|
cl.SlugCache[catId] = result
|
||||||
|
}
|
||||||
|
return cl.SlugCache[catId]
|
||||||
|
}
|
||||||
|
|
||||||
|
type CategoryTreeNode struct {
|
||||||
|
Category
|
||||||
|
Parent *CategoryTreeNode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (node *CategoryTreeNode) GetLineage() []*Category {
|
||||||
|
current := node
|
||||||
|
length := 0
|
||||||
|
for current != nil {
|
||||||
|
current = current.Parent
|
||||||
|
length += 1
|
||||||
|
}
|
||||||
|
result := make([]*Category, length)
|
||||||
|
current = node
|
||||||
|
for i := length - 1; i >= 0; i -= 1 {
|
||||||
|
result[i] = ¤t.Category
|
||||||
|
current = current.Parent
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFullCategoryTree(ctx context.Context, conn *pgxpool.Pool) map[int]*CategoryTreeNode {
|
||||||
|
type categoryRow struct {
|
||||||
|
Cat Category `db:"cat"`
|
||||||
|
}
|
||||||
|
rows, err := db.Query(ctx, conn, categoryRow{},
|
||||||
|
`
|
||||||
|
SELECT $columns
|
||||||
|
FROM
|
||||||
|
handmade_category as cat
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
panic(oops.New(err, "Failed to fetch category tree"))
|
||||||
|
}
|
||||||
|
|
||||||
|
rowsSlice := rows.ToSlice()
|
||||||
|
catTreeMap := make(map[int]*CategoryTreeNode, len(rowsSlice))
|
||||||
|
for _, row := range rowsSlice {
|
||||||
|
cat := row.(*categoryRow).Cat
|
||||||
|
catTreeMap[cat.ID] = &CategoryTreeNode{Category: cat}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, node := range catTreeMap {
|
||||||
|
if node.ParentID != nil {
|
||||||
|
node.Parent = catTreeMap[*node.ParentID]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return catTreeMap
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Gets the category and its parent categories, starting from the root and working toward the
|
Gets the category and its parent categories, starting from the root and working toward the
|
||||||
category itself. Useful for breadcrumbs and the like.
|
category itself. Useful for breadcrumbs and the like.
|
||||||
|
|
|
@ -9,6 +9,18 @@ const HMNProjectID = 1
|
||||||
|
|
||||||
var ProjectType = reflect.TypeOf(Project{})
|
var ProjectType = reflect.TypeOf(Project{})
|
||||||
|
|
||||||
|
type ProjectLifecycle int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProjectLifecycleUnapproved = iota
|
||||||
|
ProjectLifecycleApprovalRequired
|
||||||
|
ProjectLifecycleActive
|
||||||
|
ProjectLifecycleHiatus
|
||||||
|
ProjectLifecycleDead
|
||||||
|
ProjectLifecycleLTSRequired
|
||||||
|
ProjectLifecycleLTS
|
||||||
|
)
|
||||||
|
|
||||||
type Project struct {
|
type Project struct {
|
||||||
ID int `db:"id"`
|
ID int `db:"id"`
|
||||||
|
|
||||||
|
@ -17,6 +29,8 @@ type Project struct {
|
||||||
Blurb *string `db:"blurb"`
|
Blurb *string `db:"blurb"`
|
||||||
Description *string `db:"description"`
|
Description *string `db:"description"`
|
||||||
|
|
||||||
|
Lifecycle ProjectLifecycle `db:"lifecycle"`
|
||||||
|
|
||||||
Color1 string `db:"color_1"`
|
Color1 string `db:"color_1"`
|
||||||
Color2 string `db:"color_2"`
|
Color2 string `db:"color_2"`
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
func IntMin(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func IntMax(a, b int) int {
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func IntClamp(min, t, max int) int {
|
||||||
|
return IntMax(min, IntMin(t, max))
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
package website
|
package website
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -12,6 +11,7 @@ import (
|
||||||
"git.handmade.network/hmn/hmn/src/models"
|
"git.handmade.network/hmn/hmn/src/models"
|
||||||
"git.handmade.network/hmn/hmn/src/oops"
|
"git.handmade.network/hmn/hmn/src/oops"
|
||||||
"git.handmade.network/hmn/hmn/src/templates"
|
"git.handmade.network/hmn/hmn/src/templates"
|
||||||
|
"git.handmade.network/hmn/hmn/src/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FeedData struct {
|
type FeedData struct {
|
||||||
|
@ -32,7 +32,8 @@ func Feed(c *RequestContext) ResponseData {
|
||||||
handmade_post AS post
|
handmade_post AS post
|
||||||
WHERE
|
WHERE
|
||||||
post.category_kind = ANY ($1)
|
post.category_kind = ANY ($1)
|
||||||
AND NOT deleted
|
AND deleted = FALSE
|
||||||
|
AND post.thread_id IS NOT NULL
|
||||||
`,
|
`,
|
||||||
[]models.CategoryKind{models.CatKindForum, models.CatKindBlog, models.CatKindWiki, models.CatKindLibraryResource},
|
[]models.CategoryKind{models.CatKindForum, models.CatKindBlog, models.CatKindWiki, models.CatKindLibraryResource},
|
||||||
)
|
)
|
||||||
|
@ -49,11 +50,11 @@ func Feed(c *RequestContext) ResponseData {
|
||||||
if pageParsed, err := strconv.Atoi(pageString); err == nil {
|
if pageParsed, err := strconv.Atoi(pageString); err == nil {
|
||||||
page = pageParsed
|
page = pageParsed
|
||||||
} else {
|
} else {
|
||||||
return c.Redirect("/feed", http.StatusSeeOther)
|
return c.Redirect(hmnurl.BuildFeed(), http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if page < 1 || numPages < page {
|
if page < 1 || numPages < page {
|
||||||
return c.Redirect("/feed", http.StatusSeeOther)
|
return c.Redirect(hmnurl.BuildFeedWithPage(utils.IntClamp(1, page, numPages)), http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
howManyPostsToSkip := (page - 1) * postsPerPage
|
howManyPostsToSkip := (page - 1) * postsPerPage
|
||||||
|
@ -62,10 +63,10 @@ func Feed(c *RequestContext) ResponseData {
|
||||||
Current: page,
|
Current: page,
|
||||||
Total: numPages,
|
Total: numPages,
|
||||||
|
|
||||||
FirstUrl: hmnurl.Url("/feed", nil),
|
FirstUrl: hmnurl.BuildFeed(),
|
||||||
LastUrl: hmnurl.Url(fmt.Sprintf("/feed/%d", numPages), nil),
|
LastUrl: hmnurl.BuildFeedWithPage(numPages),
|
||||||
NextUrl: hmnurl.Url(fmt.Sprintf("/feed/%d", page+1), nil),
|
NextUrl: hmnurl.BuildFeedWithPage(utils.IntClamp(1, page+1, numPages)),
|
||||||
PreviousUrl: hmnurl.Url(fmt.Sprintf("/feed/%d", page-1), nil),
|
PreviousUrl: hmnurl.BuildFeedWithPage(utils.IntClamp(1, page-1, numPages)),
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentUserId *int
|
var currentUserId *int
|
||||||
|
@ -117,8 +118,28 @@ func Feed(c *RequestContext) ResponseData {
|
||||||
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch feed posts"))
|
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch feed posts"))
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
|
categoryUrlCache := make(map[int]string)
|
||||||
|
getCategoryUrl := func(subdomain 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(subdomain, 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
|
var postItems []templates.PostListItem
|
||||||
for _, iPostResult := range posts.ToSlice() {
|
for _, iPostResult := range posts.ToSlice() {
|
||||||
postResult := iPostResult.(*feedPostQuery)
|
postResult := iPostResult.(*feedPostQuery)
|
||||||
|
@ -130,32 +151,35 @@ func Feed(c *RequestContext) ResponseData {
|
||||||
hasRead = true
|
hasRead = true
|
||||||
}
|
}
|
||||||
|
|
||||||
parents := postResult.Cat.GetHierarchy(c.Context(), c.Conn)
|
breadcrumbs := make([]templates.Breadcrumb, 0, len(lineageBuilder.GetLineage(postResult.Cat.ID)))
|
||||||
|
|
||||||
var breadcrumbs []templates.Breadcrumb
|
|
||||||
breadcrumbs = append(breadcrumbs, templates.Breadcrumb{
|
breadcrumbs = append(breadcrumbs, templates.Breadcrumb{
|
||||||
Name: *postResult.Proj.Name,
|
Name: *postResult.Proj.Name,
|
||||||
Url: hmnurl.ProjectUrl("/", nil, postResult.Proj.Subdomain()),
|
Url: hmnurl.ProjectUrl("/", nil, postResult.Proj.Subdomain()),
|
||||||
})
|
})
|
||||||
for _, parent := range parents {
|
if postResult.Post.CategoryKind == models.CatKindLibraryResource {
|
||||||
name := *parent.Name
|
// TODO(asaf): Fetch library root topic for the project and construct breadcrumb for it
|
||||||
if parent.ParentID == nil {
|
} else {
|
||||||
switch parent.Kind {
|
lineage := lineageBuilder.GetLineage(postResult.Cat.ID)
|
||||||
case models.CatKindForum:
|
for i, cat := range lineage {
|
||||||
name = "Forums"
|
name := *cat.Name
|
||||||
case models.CatKindBlog:
|
if i == 0 {
|
||||||
name = "Blog"
|
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),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
breadcrumbs = append(breadcrumbs, templates.Breadcrumb{
|
|
||||||
Name: name,
|
|
||||||
Url: categoryUrls[parent.ID],
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
postItems = append(postItems, templates.PostListItem{
|
postItems = append(postItems, templates.PostListItem{
|
||||||
Title: postResult.Thread.Title,
|
Title: postResult.Thread.Title,
|
||||||
Url: PostUrl(postResult.Post, postResult.Post.CategoryKind, categoryUrls[postResult.Post.CategoryID]),
|
Url: hmnurl.BuildForumPost(postResult.Proj.Subdomain(), lineageBuilder.GetLineageSlugs(postResult.Cat.ID)[1:], postResult.Post.ID, postResult.Post.ThreadID),
|
||||||
User: templates.UserToTemplate(&postResult.User),
|
User: templates.UserToTemplate(&postResult.User),
|
||||||
Date: postResult.Post.PostDate,
|
Date: postResult.Post.PostDate,
|
||||||
Breadcrumbs: breadcrumbs,
|
Breadcrumbs: breadcrumbs,
|
||||||
|
@ -164,6 +188,7 @@ func Feed(c *RequestContext) ResponseData {
|
||||||
Content: postResult.Post.Preview,
|
Content: postResult.Post.Preview,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
baseData := getBaseData(c)
|
baseData := getBaseData(c)
|
||||||
baseData.BodyClasses = append(baseData.BodyClasses, "feed")
|
baseData.BodyClasses = append(baseData.BodyClasses, "feed")
|
||||||
|
|
|
@ -46,29 +46,29 @@ func WrapStdHandler(h http.Handler) Handler {
|
||||||
|
|
||||||
type Middleware func(h Handler) Handler
|
type Middleware func(h Handler) Handler
|
||||||
|
|
||||||
func (rb *RouteBuilder) Handle(method string, regexStr string, h Handler) {
|
func (rb *RouteBuilder) Handle(method string, regex *regexp.Regexp, h Handler) {
|
||||||
h = rb.Middleware(h)
|
h = rb.Middleware(h)
|
||||||
rb.Router.Routes = append(rb.Router.Routes, Route{
|
rb.Router.Routes = append(rb.Router.Routes, Route{
|
||||||
Method: method,
|
Method: method,
|
||||||
Regex: regexp.MustCompile(regexStr),
|
Regex: regex,
|
||||||
Handler: h,
|
Handler: h,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rb *RouteBuilder) AnyMethod(regexStr string, h Handler) {
|
func (rb *RouteBuilder) AnyMethod(regex *regexp.Regexp, h Handler) {
|
||||||
rb.Handle("", regexStr, h)
|
rb.Handle("", regex, h)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rb *RouteBuilder) GET(regexStr string, h Handler) {
|
func (rb *RouteBuilder) GET(regex *regexp.Regexp, h Handler) {
|
||||||
rb.Handle(http.MethodGet, regexStr, h)
|
rb.Handle(http.MethodGet, regex, h)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rb *RouteBuilder) POST(regexStr string, h Handler) {
|
func (rb *RouteBuilder) POST(regex *regexp.Regexp, h Handler) {
|
||||||
rb.Handle(http.MethodPost, regexStr, h)
|
rb.Handle(http.MethodPost, regex, h)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rb *RouteBuilder) StdHandler(regexStr string, h http.Handler) {
|
func (rb *RouteBuilder) StdHandler(regex *regexp.Regexp, h http.Handler) {
|
||||||
rb.Handle("", regexStr, WrapStdHandler(h))
|
rb.Handle("", regex, WrapStdHandler(h))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Router) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
func (r *Router) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||||
|
|
|
@ -31,9 +31,10 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt
|
||||||
logPerf := TrackRequestPerf(c, perfCollector)
|
logPerf := TrackRequestPerf(c, perfCollector)
|
||||||
defer logPerf()
|
defer logPerf()
|
||||||
|
|
||||||
defer LogContextErrors(c, res)
|
res = h(c)
|
||||||
|
|
||||||
return h(c)
|
LogContextErrors(c, res)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -46,14 +47,15 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt
|
||||||
logPerf := TrackRequestPerf(c, perfCollector)
|
logPerf := TrackRequestPerf(c, perfCollector)
|
||||||
defer logPerf()
|
defer logPerf()
|
||||||
|
|
||||||
defer LogContextErrors(c, res)
|
|
||||||
|
|
||||||
ok, errRes := LoadCommonWebsiteData(c)
|
ok, errRes := LoadCommonWebsiteData(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
return errRes
|
return errRes
|
||||||
}
|
}
|
||||||
|
|
||||||
return h(c)
|
res = h(c)
|
||||||
|
|
||||||
|
LogContextErrors(c, res)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,29 +67,31 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt
|
||||||
logPerf := TrackRequestPerf(c, perfCollector)
|
logPerf := TrackRequestPerf(c, perfCollector)
|
||||||
defer logPerf()
|
defer logPerf()
|
||||||
|
|
||||||
defer LogContextErrors(c, res)
|
|
||||||
|
|
||||||
ok, errRes := LoadCommonWebsiteData(c)
|
ok, errRes := LoadCommonWebsiteData(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
return errRes
|
return errRes
|
||||||
}
|
}
|
||||||
|
|
||||||
if !c.CurrentProject.IsHMN() {
|
if !c.CurrentProject.IsHMN() {
|
||||||
res := c.Redirect(hmnurl.Url(c.URL().String(), nil), http.StatusMovedPermanently)
|
res = c.Redirect(hmnurl.Url(c.URL().String(), nil), http.StatusMovedPermanently)
|
||||||
return res
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return h(c)
|
res = h(c)
|
||||||
|
|
||||||
|
LogContextErrors(c, res)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
routes.POST("^/login$", Login)
|
// TODO(asaf): login/logout shouldn't happen on subdomains. We should verify that in the middleware.
|
||||||
routes.GET("^/logout$", Logout)
|
routes.POST(hmnurl.RegexLogin, Login)
|
||||||
routes.StdHandler("^/public/.*$",
|
routes.GET(hmnurl.RegexLogout, Logout)
|
||||||
|
routes.StdHandler(hmnurl.RegexPublic,
|
||||||
http.StripPrefix("/public/", http.FileServer(http.Dir("public"))),
|
http.StripPrefix("/public/", http.FileServer(http.Dir("public"))),
|
||||||
)
|
)
|
||||||
|
|
||||||
mainRoutes.GET("^/$", func(c *RequestContext) ResponseData {
|
mainRoutes.GET(hmnurl.RegexHomepage, func(c *RequestContext) ResponseData {
|
||||||
if c.CurrentProject.IsHMN() {
|
if c.CurrentProject.IsHMN() {
|
||||||
return Index(c)
|
return Index(c)
|
||||||
} else {
|
} else {
|
||||||
|
@ -95,24 +99,24 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt
|
||||||
panic("route not implemented")
|
panic("route not implemented")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
staticPages.GET("^/manifesto$", Manifesto)
|
staticPages.GET(hmnurl.RegexManifesto, Manifesto)
|
||||||
staticPages.GET("^/about$", About)
|
staticPages.GET(hmnurl.RegexAbout, About)
|
||||||
staticPages.GET("^/code-of-conduct$", CodeOfConduct)
|
staticPages.GET(hmnurl.RegexCodeOfConduct, CodeOfConduct)
|
||||||
staticPages.GET("^/communication-guidelines$", CommunicationGuidelines)
|
staticPages.GET(hmnurl.RegexCommunicationGuidelines, CommunicationGuidelines)
|
||||||
staticPages.GET("^/contact$", ContactPage)
|
staticPages.GET(hmnurl.RegexContactPage, ContactPage)
|
||||||
staticPages.GET("^/monthly-update-policy$", MonthlyUpdatePolicy)
|
staticPages.GET(hmnurl.RegexMonthlyUpdatePolicy, MonthlyUpdatePolicy)
|
||||||
staticPages.GET("^/project-guidelines$", ProjectSubmissionGuidelines)
|
staticPages.GET(hmnurl.RegexProjectSubmissionGuidelines, ProjectSubmissionGuidelines)
|
||||||
|
|
||||||
mainRoutes.GET(`^/feed(/(?P<page>.+)?)?$`, Feed)
|
mainRoutes.GET(hmnurl.RegexFeed, Feed)
|
||||||
|
|
||||||
// TODO(asaf): Trailing slashes break these
|
// TODO(asaf): Trailing slashes break these
|
||||||
mainRoutes.GET(`^/(?P<cats>forums(/[^\d]+?)*)/t/(?P<threadid>\d+)(/(?P<page>\d+))?$`, ForumThread)
|
mainRoutes.GET(hmnurl.RegexForumThread, ForumThread)
|
||||||
// mainRoutes.GET(`^/(?P<cats>forums(/cat)*)/t/(?P<threadid>\d+)/p/(?P<postid>\d+)$`, ForumPost)
|
// mainRoutes.GET(`^/(?P<cats>forums(/cat)*)/t/(?P<threadid>\d+)/p/(?P<postid>\d+)$`, ForumPost)
|
||||||
mainRoutes.GET(`^/(?P<cats>forums(/[^\d]+?)*)(/(?P<page>\d+))?$`, ForumCategory)
|
mainRoutes.GET(hmnurl.RegexForumCategory, ForumCategory)
|
||||||
|
|
||||||
mainRoutes.GET("^/assets/project.css$", ProjectCSS)
|
mainRoutes.GET(hmnurl.RegexProjectCSS, ProjectCSS)
|
||||||
|
|
||||||
mainRoutes.AnyMethod("", FourOhFour)
|
mainRoutes.AnyMethod(hmnurl.RegexCatchAll, FourOhFour)
|
||||||
|
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,9 @@ func GetAllCategoryUrls(ctx context.Context, conn *pgxpool.Pool) map[int]string
|
||||||
FROM
|
FROM
|
||||||
handmade_category AS cat
|
handmade_category AS cat
|
||||||
JOIN handmade_project AS project ON project.id = cat.project_id
|
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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
@ -42,7 +44,8 @@ func GetProjectCategoryUrls(ctx context.Context, conn *pgxpool.Pool, projectId .
|
||||||
JOIN handmade_project AS project ON project.id = cat.project_id
|
JOIN handmade_project AS project ON project.id = cat.project_id
|
||||||
WHERE
|
WHERE
|
||||||
project.id = ANY ($1)
|
project.id = ANY ($1)
|
||||||
`,
|
AND cat.kind != 6
|
||||||
|
`, // TODO(asaf): Clean up the db and remove the cat.kind != 6 check
|
||||||
projectId,
|
projectId,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -91,24 +94,16 @@ func makeCategoryUrls(rows []interface{}) map[int]string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func CategoryUrl(subdomain string, cats ...*models.Category) string {
|
func CategoryUrl(subdomain string, cats ...*models.Category) string {
|
||||||
path := ""
|
catNames := make([]string, 0, len(cats))
|
||||||
for i, cat := range cats {
|
for _, cat := range cats {
|
||||||
if i == 0 {
|
catNames = append(catNames, *cat.Name)
|
||||||
switch cat.Kind {
|
}
|
||||||
case models.CatKindBlog:
|
switch cats[0].Kind {
|
||||||
path += "/blogs"
|
case models.CatKindForum:
|
||||||
case models.CatKindForum:
|
return hmnurl.BuildForumCategory(subdomain, catNames[1:], 1)
|
||||||
path += "/forums"
|
default:
|
||||||
// TODO: All cat types?
|
return ""
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
path += "/" + *cat.Slug
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return hmnurl.ProjectUrl(path, nil, subdomain)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func PostUrl(post models.Post, catKind models.CategoryKind, categoryUrl string) string {
|
func PostUrl(post models.Post, catKind models.CategoryKind, categoryUrl string) string {
|
||||||
|
|
Loading…
Reference in New Issue