Make some project fields not null

This commit is contained in:
Ben Visness 2021-05-05 23:04:58 -05:00
parent e8d1859d0a
commit c8231750aa
11 changed files with 136 additions and 127 deletions

View File

@ -4,6 +4,7 @@ import (
"net/url" "net/url"
"git.handmade.network/hmn/hmn/src/config" "git.handmade.network/hmn/hmn/src/config"
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops" "git.handmade.network/hmn/hmn/src/oops"
) )
@ -37,10 +38,15 @@ func Url(path string, query []Q) string {
return ProjectUrl(path, query, "") 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 host := baseUrlParsed.Host
if len(subdomain) > 0 { if len(subdomain) > 0 {
host = subdomain + "." + host host = slug + "." + host
} }
url := url.URL{ url := url.URL{

View File

@ -1,10 +1,11 @@
package hmnurl package hmnurl
import ( import (
"git.handmade.network/hmn/hmn/src/oops"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"git.handmade.network/hmn/hmn/src/oops"
) )
var RegexHomepage *regexp.Regexp = regexp.MustCompile("^/$") var RegexHomepage *regexp.Regexp = regexp.MustCompile("^/$")
@ -86,10 +87,6 @@ func BuildFeedWithPage(page int) string {
var RegexForumThread *regexp.Regexp = regexp.MustCompile(`^/(?P<cats>forums(/[^\d]+?)*)/t/(?P<threadid>\d+)(/(?P<page>\d+))?$`) 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 { func BuildForumThread(projectSlug string, subforums []string, threadId int, page int) string {
if projectSlug == "hmn" {
projectSlug = ""
}
if page < 1 { if page < 1 {
panic(oops.New(nil, "Invalid forum thread page (%d), must be >= 1", page)) panic(oops.New(nil, "Invalid forum thread page (%d), must be >= 1", page))
} }
@ -120,10 +117,6 @@ func BuildForumThread(projectSlug string, subforums []string, threadId int, page
var RegexForumCategory *regexp.Regexp = regexp.MustCompile(`^/(?P<cats>forums(/[^\d]+?)*)(/(?P<page>\d+))?$`) var RegexForumCategory *regexp.Regexp = regexp.MustCompile(`^/(?P<cats>forums(/[^\d]+?)*)(/(?P<page>\d+))?$`)
func BuildForumCategory(projectSlug string, subforums []string, page int) string { func BuildForumCategory(projectSlug string, subforums []string, page int) string {
if projectSlug == "hmn" {
projectSlug = ""
}
if page < 1 { if page < 1 {
panic(oops.New(nil, "Invalid forum thread page (%d), must be >= 1", page)) panic(oops.New(nil, "Invalid forum thread page (%d), must be >= 1", page))
} }
@ -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 var RegexForumPost *regexp.Regexp = regexp.MustCompile(``) // TODO(asaf): Complete this and test it
func BuildForumPost(projectSlug string, subforums []string, threadId int, postId int) string { func BuildForumPost(projectSlug string, subforums []string, threadId int, postId int) string {
if projectSlug == "hmn" {
projectSlug = ""
}
var builder strings.Builder var builder strings.Builder
builder.WriteString("/forums") builder.WriteString("/forums")
for _, subforum := range subforums { 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$") var RegexProjectCSS *regexp.Regexp = regexp.MustCompile("^/assets/project.css$")
func BuildProjectCSS(color string) string { 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/.+$") var RegexPublic *regexp.Regexp = regexp.MustCompile("^/public/.+$")

View File

@ -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")
}

View File

@ -34,13 +34,66 @@ type Category struct {
Depth int `db:"depth"` // TODO: What is this? 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] = &current.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 { type CategoryLineageBuilder struct {
Tree map[int]*CategoryTreeNode Tree CategoryTree
CategoryCache map[int][]*Category CategoryCache map[int][]*Category
SlugCache map[int][]string SlugCache map[int][]string
} }
func MakeCategoryLineageBuilder(fullCategoryTree map[int]*CategoryTreeNode) *CategoryLineageBuilder { func MakeCategoryLineageBuilder(fullCategoryTree CategoryTree) *CategoryLineageBuilder {
return &CategoryLineageBuilder{ return &CategoryLineageBuilder{
Tree: fullCategoryTree, Tree: fullCategoryTree,
CategoryCache: make(map[int][]*Category), CategoryCache: make(map[int][]*Category),
@ -72,93 +125,3 @@ func (cl *CategoryLineageBuilder) GetLineageSlugs(catId int) []string {
} }
return cl.SlugCache[catId] 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] = &current.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
}

View File

@ -5,7 +5,10 @@ import (
"time" "time"
) )
const HMNProjectID = 1 const (
HMNProjectID = 1
HMNProjectSlug = "hmn"
)
var ProjectType = reflect.TypeOf(Project{}) var ProjectType = reflect.TypeOf(Project{})
@ -24,10 +27,10 @@ const (
type Project struct { type Project struct {
ID int `db:"id"` ID int `db:"id"`
Slug *string `db:"slug"` // TODO: Migrate these to NOT NULL Slug string `db:"slug"`
Name *string `db:"name"` Name string `db:"name"`
Blurb *string `db:"blurb"` Blurb string `db:"blurb"`
Description *string `db:"description"` Description string `db:"description"`
Lifecycle ProjectLifecycle `db:"lifecycle"` Lifecycle ProjectLifecycle `db:"lifecycle"`
@ -46,5 +49,5 @@ func (p *Project) Subdomain() string {
return "" return ""
} }
return *p.Slug return p.Slug
} }

View File

@ -38,7 +38,7 @@ func PostToTemplateWithContent(p *models.Post, author *models.User, content stri
func ProjectToTemplate(p *models.Project) Project { func ProjectToTemplate(p *models.Project) Project {
return Project{ return Project{
Name: maybeString(p.Name), Name: p.Name,
Subdomain: p.Subdomain(), Subdomain: p.Subdomain(),
Color1: p.Color1, Color1: p.Color1,
Color2: p.Color2, Color2: p.Color2,

View File

@ -124,13 +124,13 @@ func Feed(c *RequestContext) ResponseData {
c.Perf.EndBlock() c.Perf.EndBlock()
categoryUrlCache := make(map[int]string) 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] _, ok := categoryUrlCache[cat.ID]
if !ok { if !ok {
lineageNames := lineageBuilder.GetLineageSlugs(cat.ID) lineageNames := lineageBuilder.GetLineageSlugs(cat.ID)
switch cat.Kind { switch cat.Kind {
case models.CatKindForum: 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!!! // TODO(asaf): Add more kinds!!!
default: default:
categoryUrlCache[cat.ID] = "" categoryUrlCache[cat.ID] = ""
@ -153,8 +153,8 @@ func Feed(c *RequestContext) ResponseData {
breadcrumbs := make([]templates.Breadcrumb, 0, len(lineageBuilder.GetLineage(postResult.Cat.ID))) breadcrumbs := make([]templates.Breadcrumb, 0, len(lineageBuilder.GetLineage(postResult.Cat.ID)))
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.Slug),
}) })
if postResult.Post.CategoryKind == models.CatKindLibraryResource { if postResult.Post.CategoryKind == models.CatKindLibraryResource {
// TODO(asaf): Fetch library root topic for the project and construct breadcrumb for it // TODO(asaf): Fetch library root topic for the project and construct breadcrumb for it

View File

@ -261,11 +261,11 @@ func ForumCategory(c *RequestContext) ResponseData {
// --------------------- // ---------------------
baseData := getBaseData(c) baseData := getBaseData(c)
baseData.Title = *c.CurrentProject.Name + " Forums" baseData.Title = c.CurrentProject.Name + " Forums"
baseData.Breadcrumbs = []templates.Breadcrumb{ baseData.Breadcrumbs = []templates.Breadcrumb{
{ {
Name: *c.CurrentProject.Name, Name: c.CurrentProject.Name,
Url: hmnurl.ProjectUrl("/", nil, c.CurrentProject.Subdomain()), Url: hmnurl.ProjectUrl("/", nil, c.CurrentProject.Slug),
}, },
{ {
Name: "Forums", Name: "Forums",

View File

@ -75,7 +75,7 @@ func Index(c *RequestContext) ResponseData {
for _, projRow := range allProjects { for _, projRow := range allProjects {
proj := projRow.(*models.Project) 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 { type projectPostQuery struct {
Post models.Post `db:"post"` Post models.Post `db:"post"`
Thread models.Thread `db:"thread"` Thread models.Thread `db:"thread"`

View File

@ -5,6 +5,7 @@ import (
"errors" "errors"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"regexp"
"testing" "testing"
"github.com/rs/zerolog" "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) return ErrorResponse(http.StatusInternalServerError, err1, err2)
}) })

View File

@ -87,20 +87,20 @@ func makeCategoryUrls(rows []interface{}) map[int]string {
hierarchy[len(hierarchyReverse)-1-i] = hierarchyReverse[i] 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 return result
} }
func CategoryUrl(subdomain string, cats ...*models.Category) string { func CategoryUrl(projectSlug string, cats ...*models.Category) string {
catNames := make([]string, 0, len(cats)) catNames := make([]string, 0, len(cats))
for _, cat := range cats { for _, cat := range cats {
catNames = append(catNames, *cat.Name) catNames = append(catNames, *cat.Name)
} }
switch cats[0].Kind { switch cats[0].Kind {
case models.CatKindForum: case models.CatKindForum:
return hmnurl.BuildForumCategory(subdomain, catNames[1:], 1) return hmnurl.BuildForumCategory(projectSlug, catNames[1:], 1)
default: default:
return "" return ""
} }