diff --git a/src/hmnurl/hmnurl.go b/src/hmnurl/hmnurl.go index 8cc0886..0713542 100644 --- a/src/hmnurl/hmnurl.go +++ b/src/hmnurl/hmnurl.go @@ -4,6 +4,7 @@ import ( "net/url" "git.handmade.network/hmn/hmn/src/config" + "git.handmade.network/hmn/hmn/src/models" "git.handmade.network/hmn/hmn/src/oops" ) @@ -37,10 +38,15 @@ func Url(path string, query []Q) string { return ProjectUrl(path, query, "") } -func ProjectUrl(path string, query []Q, subdomain string) string { +func ProjectUrl(path string, query []Q, slug string) string { + subdomain := slug + if slug == models.HMNProjectSlug { + subdomain = "" + } + host := baseUrlParsed.Host if len(subdomain) > 0 { - host = subdomain + "." + host + host = slug + "." + host } url := url.URL{ diff --git a/src/hmnurl/urls.go b/src/hmnurl/urls.go index 12a9f8b..f057212 100644 --- a/src/hmnurl/urls.go +++ b/src/hmnurl/urls.go @@ -1,10 +1,11 @@ package hmnurl import ( - "git.handmade.network/hmn/hmn/src/oops" "regexp" "strconv" "strings" + + "git.handmade.network/hmn/hmn/src/oops" ) var RegexHomepage *regexp.Regexp = regexp.MustCompile("^/$") @@ -86,10 +87,6 @@ func BuildFeedWithPage(page int) string { var RegexForumThread *regexp.Regexp = regexp.MustCompile(`^/(?Pforums(/[^\d]+?)*)/t/(?P\d+)(/(?P\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)) } @@ -120,10 +117,6 @@ func BuildForumThread(projectSlug string, subforums []string, threadId int, page var RegexForumCategory *regexp.Regexp = regexp.MustCompile(`^/(?Pforums(/[^\d]+?)*)(/(?P\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)) } @@ -152,10 +145,6 @@ func BuildForumCategory(projectSlug string, subforums []string, page int) string 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 { @@ -180,7 +169,7 @@ func BuildForumPost(projectSlug string, subforums []string, threadId int, postId var RegexProjectCSS *regexp.Regexp = regexp.MustCompile("^/assets/project.css$") func BuildProjectCSS(color string) string { - return Url("/assets/project.css", []Q{Q{"color", color}}) + return Url("/assets/project.css", []Q{{"color", color}}) } var RegexPublic *regexp.Regexp = regexp.MustCompile("^/public/.+$") diff --git a/src/migration/migrations/2021-05-06T031328Z_RemoveProjectNulls.go b/src/migration/migrations/2021-05-06T031328Z_RemoveProjectNulls.go new file mode 100644 index 0000000..7352903 --- /dev/null +++ b/src/migration/migrations/2021-05-06T031328Z_RemoveProjectNulls.go @@ -0,0 +1,47 @@ +package migrations + +import ( + "context" + "time" + + "git.handmade.network/hmn/hmn/src/migration/types" + "git.handmade.network/hmn/hmn/src/oops" + "github.com/jackc/pgx/v4" +) + +func init() { + registerMigration(RemoveProjectNulls{}) +} + +type RemoveProjectNulls struct{} + +func (m RemoveProjectNulls) Version() types.MigrationVersion { + return types.MigrationVersion(time.Date(2021, 5, 6, 3, 13, 28, 0, time.UTC)) +} + +func (m RemoveProjectNulls) Name() string { + return "RemoveProjectNulls" +} + +func (m RemoveProjectNulls) Description() string { + return "Make project fields non-nullable" +} + +func (m RemoveProjectNulls) Up(ctx context.Context, tx pgx.Tx) error { + _, err := tx.Exec(ctx, ` + ALTER TABLE handmade_project + ALTER slug SET NOT NULL, + ALTER name SET NOT NULL, + ALTER blurb SET NOT NULL, + ALTER description SET NOT NULL; + `) + if err != nil { + return oops.New(err, "failed to make project fields non-null") + } + + return nil +} + +func (m RemoveProjectNulls) Down(ctx context.Context, tx pgx.Tx) error { + panic("Implement me") +} diff --git a/src/models/category.go b/src/models/category.go index d9241dd..0972b95 100644 --- a/src/models/category.go +++ b/src/models/category.go @@ -34,13 +34,66 @@ type Category struct { Depth int `db:"depth"` // TODO: What is this? } +type CategoryTree map[int]*CategoryTreeNode + +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) CategoryTree { + 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 +} + type CategoryLineageBuilder struct { - Tree map[int]*CategoryTreeNode + Tree CategoryTree CategoryCache map[int][]*Category SlugCache map[int][]string } -func MakeCategoryLineageBuilder(fullCategoryTree map[int]*CategoryTreeNode) *CategoryLineageBuilder { +func MakeCategoryLineageBuilder(fullCategoryTree CategoryTree) *CategoryLineageBuilder { return &CategoryLineageBuilder{ Tree: fullCategoryTree, CategoryCache: make(map[int][]*Category), @@ -72,93 +125,3 @@ func (cl *CategoryLineageBuilder) GetLineageSlugs(catId int) []string { } 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 -category itself. Useful for breadcrumbs and the like. -*/ -func (c *Category) GetHierarchy(ctx context.Context, conn *pgxpool.Pool) []Category { - // TODO: Make this work for a whole set of categories at once. Should be doable. - type breadcrumbRow struct { - Cat Category `db:"cats"` - } - rows, err := db.Query(ctx, conn, breadcrumbRow{}, - ` - WITH RECURSIVE cats AS ( - SELECT * - FROM handmade_category AS cat - WHERE cat.id = $1 - UNION ALL - SELECT parentcat.* - FROM - handmade_category AS parentcat - JOIN cats ON cats.parent_id = parentcat.id - ) - SELECT $columns FROM cats; - `, - c.ID, - ) - if err != nil { - panic(err) - } - - rowsSlice := rows.ToSlice() - var result []Category - for i := len(rowsSlice) - 1; i >= 0; i-- { - row := rowsSlice[i].(*breadcrumbRow) - result = append(result, row.Cat) - } - - return result -} diff --git a/src/models/project.go b/src/models/project.go index 5c25cc8..9591366 100644 --- a/src/models/project.go +++ b/src/models/project.go @@ -5,7 +5,10 @@ import ( "time" ) -const HMNProjectID = 1 +const ( + HMNProjectID = 1 + HMNProjectSlug = "hmn" +) var ProjectType = reflect.TypeOf(Project{}) @@ -24,10 +27,10 @@ const ( type Project struct { ID int `db:"id"` - Slug *string `db:"slug"` // TODO: Migrate these to NOT NULL - Name *string `db:"name"` - Blurb *string `db:"blurb"` - Description *string `db:"description"` + Slug string `db:"slug"` + Name string `db:"name"` + Blurb string `db:"blurb"` + Description string `db:"description"` Lifecycle ProjectLifecycle `db:"lifecycle"` @@ -46,5 +49,5 @@ func (p *Project) Subdomain() string { return "" } - return *p.Slug + return p.Slug } diff --git a/src/templates/mapping.go b/src/templates/mapping.go index fea2707..5255367 100644 --- a/src/templates/mapping.go +++ b/src/templates/mapping.go @@ -38,7 +38,7 @@ func PostToTemplateWithContent(p *models.Post, author *models.User, content stri func ProjectToTemplate(p *models.Project) Project { return Project{ - Name: maybeString(p.Name), + Name: p.Name, Subdomain: p.Subdomain(), Color1: p.Color1, Color2: p.Color2, diff --git a/src/website/feed.go b/src/website/feed.go index 289d86d..6db6545 100644 --- a/src/website/feed.go +++ b/src/website/feed.go @@ -124,13 +124,13 @@ func Feed(c *RequestContext) ResponseData { c.Perf.EndBlock() categoryUrlCache := make(map[int]string) - getCategoryUrl := func(subdomain string, cat *models.Category) 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(subdomain, lineageNames[1:], 1) + categoryUrlCache[cat.ID] = hmnurl.BuildForumCategory(projectSlug, lineageNames[1:], 1) // TODO(asaf): Add more kinds!!! default: categoryUrlCache[cat.ID] = "" @@ -153,8 +153,8 @@ func Feed(c *RequestContext) ResponseData { 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.Subdomain()), + 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 diff --git a/src/website/forums.go b/src/website/forums.go index 501b46a..55aac4f 100644 --- a/src/website/forums.go +++ b/src/website/forums.go @@ -261,11 +261,11 @@ func ForumCategory(c *RequestContext) ResponseData { // --------------------- baseData := getBaseData(c) - baseData.Title = *c.CurrentProject.Name + " Forums" + baseData.Title = c.CurrentProject.Name + " Forums" baseData.Breadcrumbs = []templates.Breadcrumb{ { - Name: *c.CurrentProject.Name, - Url: hmnurl.ProjectUrl("/", nil, c.CurrentProject.Subdomain()), + Name: c.CurrentProject.Name, + Url: hmnurl.ProjectUrl("/", nil, c.CurrentProject.Slug), }, { Name: "Forums", diff --git a/src/website/landing.go b/src/website/landing.go index 3cc5923..dead6fe 100644 --- a/src/website/landing.go +++ b/src/website/landing.go @@ -75,7 +75,7 @@ func Index(c *RequestContext) ResponseData { for _, projRow := range allProjects { proj := projRow.(*models.Project) - c.Perf.StartBlock("SQL", fmt.Sprintf("Fetch posts for %s", *proj.Name)) + c.Perf.StartBlock("SQL", fmt.Sprintf("Fetch posts for %s", proj.Name)) type projectPostQuery struct { Post models.Post `db:"post"` Thread models.Thread `db:"thread"` diff --git a/src/website/routes_test.go b/src/website/routes_test.go index 86ea652..0e4d088 100644 --- a/src/website/routes_test.go +++ b/src/website/routes_test.go @@ -5,6 +5,7 @@ import ( "errors" "net/http" "net/http/httptest" + "regexp" "testing" "github.com/rs/zerolog" @@ -33,7 +34,7 @@ func TestLogContextErrors(t *testing.T) { }, } - routes.GET("^/test$", func(c *RequestContext) ResponseData { + routes.GET(regexp.MustCompile("^/test$"), func(c *RequestContext) ResponseData { return ErrorResponse(http.StatusInternalServerError, err1, err2) }) diff --git a/src/website/urls.go b/src/website/urls.go index 7fce5fa..b707425 100644 --- a/src/website/urls.go +++ b/src/website/urls.go @@ -87,20 +87,20 @@ func makeCategoryUrls(rows []interface{}) map[int]string { hierarchy[len(hierarchyReverse)-1-i] = hierarchyReverse[i] } - result[row.Cat.ID] = CategoryUrl(row.Project.Subdomain(), hierarchy...) + result[row.Cat.ID] = CategoryUrl(row.Project.Slug, hierarchy...) } return result } -func CategoryUrl(subdomain string, cats ...*models.Category) string { +func CategoryUrl(projectSlug string, cats ...*models.Category) string { catNames := make([]string, 0, len(cats)) for _, cat := range cats { catNames = append(catNames, *cat.Name) } switch cats[0].Kind { case models.CatKindForum: - return hmnurl.BuildForumCategory(subdomain, catNames[1:], 1) + return hmnurl.BuildForumCategory(projectSlug, catNames[1:], 1) default: return "" }