diff --git a/src/migration/migrations/2021-04-28T015537Z_RenameHMNProject.go b/src/migration/migrations/2021-04-28T015537Z_RenameHMNProject.go new file mode 100644 index 00000000..26e4032c --- /dev/null +++ b/src/migration/migrations/2021-04-28T015537Z_RenameHMNProject.go @@ -0,0 +1,41 @@ +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(RenameHMNProject{}) +} + +type RenameHMNProject struct{} + +func (m RenameHMNProject) Version() types.MigrationVersion { + return types.MigrationVersion(time.Date(2021, 4, 28, 1, 55, 37, 0, time.UTC)) +} + +func (m RenameHMNProject) Name() string { + return "RenameHMNProject" +} + +func (m RenameHMNProject) Description() string { + return "Rename the special HMN project" +} + +func (m RenameHMNProject) Up(ctx context.Context, tx pgx.Tx) error { + _, err := tx.Exec(ctx, `UPDATE handmade_project SET name = 'Handmade Network' WHERE id = 1`) + if err != nil { + return oops.New(err, "failed to rename project") + } + + return nil +} + +func (m RenameHMNProject) Down(ctx context.Context, tx pgx.Tx) error { + panic("Implement me") +} diff --git a/src/models/category.go b/src/models/category.go index 7efb429b..0aa08359 100644 --- a/src/models/category.go +++ b/src/models/category.go @@ -4,19 +4,18 @@ import ( "context" "git.handmade.network/hmn/hmn/src/db" - "git.handmade.network/hmn/hmn/src/hmnurl" "github.com/jackc/pgx/v4/pgxpool" ) -type CategoryType int +type CategoryKind int const ( - CatTypeBlog CategoryType = iota + 1 - CatTypeForum - CatTypeStatic - CatTypeAnnotation - CatTypeWiki - CatTypeLibraryResource + CatKindBlog CategoryKind = iota + 1 + CatKindForum + CatKindStatic + CatKindAnnotation + CatKindWiki + CatKindLibraryResource ) type Category struct { @@ -28,7 +27,7 @@ type Category struct { Slug *string `db:"slug"` // TODO: Make not null Name *string `db:"name"` // TODO: Make not null Blurb *string `db:"blurb"` // TODO: Make not null - Kind CategoryType `db:"kind"` + Kind CategoryKind `db:"kind"` Color1 string `db:"color_1"` Color2 string `db:"color_2"` Depth int `db:"depth"` // TODO: What is this? @@ -72,51 +71,3 @@ func (c *Category) GetHierarchy(ctx context.Context, conn *pgxpool.Pool) []Categ return result } - -func GetCategoryUrls(ctx context.Context, conn *pgxpool.Pool, cats ...*Category) map[int]string { - var projectIds []int - for _, cat := range cats { - id := *cat.ProjectID - - alreadyInList := false - for _, otherId := range projectIds { - if otherId == id { - alreadyInList = true - break - } - } - - if !alreadyInList { - projectIds = append(projectIds, id) - } - } - - // TODO(inarray)!!!!! - - //for _, cat := range cats { - // hierarchy := makeCategoryUrl(cat.GetHierarchy(ctx, conn)) - //} - - return nil -} - -func makeCategoryUrl(cats []*Category, subdomain string) string { - path := "" - for i, cat := range cats { - if i == 0 { - switch cat.Kind { - case CatTypeBlog: - path += "/blogs" - case CatTypeForum: - path += "/forums" - // TODO: All cat types? - default: - return "" - } - } else { - path += "/" + *cat.Slug - } - } - - return hmnurl.ProjectUrl(path, nil, subdomain) -} diff --git a/src/models/post.go b/src/models/post.go index 76b429de..2020a0eb 100644 --- a/src/models/post.go +++ b/src/models/post.go @@ -16,7 +16,7 @@ type Post struct { CurrentID int `db:"current_id"` ProjectID int `db:"project_id"` - CategoryType CategoryType `db:"category_kind"` + CategoryKind CategoryKind `db:"category_kind"` Depth int `db:"depth"` Slug string `db:"slug"` diff --git a/src/templates/src/index.html b/src/templates/src/landing.html similarity index 100% rename from src/templates/src/index.html rename to src/templates/src/landing.html diff --git a/src/templates/urls.go b/src/templates/urls.go deleted file mode 100644 index 38a73114..00000000 --- a/src/templates/urls.go +++ /dev/null @@ -1,20 +0,0 @@ -package templates - -import ( - "fmt" - - "git.handmade.network/hmn/hmn/src/hmnurl" - "git.handmade.network/hmn/hmn/src/models" -) - -func PostUrl(post models.Post, catType models.CategoryType, subdomain string) string { - switch catType { - // TODO: All the relevant post types. Maybe it doesn't make sense to lump them all together here. - case models.CatTypeBlog: - return hmnurl.ProjectUrl(fmt.Sprintf("blogs/p/%d/e/%d", post.ThreadID, post.ID), nil, subdomain) - case models.CatTypeForum: - return hmnurl.ProjectUrl(fmt.Sprintf("forums/t/%d/p/%d", post.ThreadID, post.ID), nil, subdomain) - } - - return "" -} diff --git a/src/website/feed.go b/src/website/feed.go index cb1e1803..4c7d7f8e 100644 --- a/src/website/feed.go +++ b/src/website/feed.go @@ -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/logging" "git.handmade.network/hmn/hmn/src/models" "git.handmade.network/hmn/hmn/src/oops" @@ -30,14 +31,11 @@ func Feed(c *RequestContext) ResponseData { FROM handmade_post AS post WHERE - post.category_kind IN ($1, $2, $3, $4) + post.category_kind = ANY ($1) AND NOT moderated `, - models.CatTypeForum, - models.CatTypeBlog, - models.CatTypeWiki, - models.CatTypeLibraryResource, - ) // TODO(inarray) + []models.CategoryKind{models.CatKindForum, models.CatKindBlog, models.CatKindWiki, models.CatKindLibraryResource}, + ) c.Perf.EndBlock() if err != nil { return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to get count of feed posts")) @@ -74,12 +72,13 @@ func Feed(c *RequestContext) ResponseData { c.Perf.StartBlock("SQL", "Fetch posts") type feedPostQuery struct { - Post models.Post `db:"post"` - Thread models.Thread `db:"thread"` - 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"` + 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{}, ` @@ -87,6 +86,7 @@ func Feed(c *RequestContext) ResponseData { FROM handmade_post AS post 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 ( tlri.thread_id = post.thread_id @@ -98,25 +98,24 @@ func Feed(c *RequestContext) ResponseData { ) LEFT OUTER JOIN auth_user ON post.author_id = auth_user.id WHERE - post.category_kind IN ($2, $3, $4, $5) + post.category_kind = ANY ($2) AND post.moderated = FALSE AND post.thread_id IS NOT NULL ORDER BY postdate DESC - LIMIT $6 OFFSET $7 + LIMIT $3 OFFSET $4 `, currentUserId, - models.CatTypeForum, - models.CatTypeBlog, - models.CatTypeWiki, - models.CatTypeLibraryResource, + []models.CategoryKind{models.CatKindForum, models.CatKindBlog, models.CatKindWiki, models.CatKindLibraryResource}, postsPerPage, howManyPostsToSkip, - ) // TODO(inarray) + ) c.Perf.EndBlock() if err != nil { return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch feed posts")) } + categoryUrls := GetAllCategoryUrls(c.Context(), c.Conn) + var postItems []templates.PostListItem for _, iPostResult := range posts.ToSlice() { postResult := iPostResult.(*feedPostQuery) @@ -128,25 +127,33 @@ func Feed(c *RequestContext) ResponseData { hasRead = true } - var parents []models.Category - // parents := postResult.Cat.GetHierarchy(c.Context(), c.Conn) + parents := postResult.Cat.GetHierarchy(c.Context(), c.Conn) logging.Debug().Interface("parents", parents).Msg("") var breadcrumbs []templates.Breadcrumb breadcrumbs = append(breadcrumbs, templates.Breadcrumb{ Name: *postResult.Proj.Name, - Url: "nargle", // TODO + Url: hmnurl.ProjectUrl("/", nil, postResult.Proj.Subdomain()), }) - for i := len(parents) - 1; i >= 0; i-- { + for _, parent := range parents { + name := *parent.Name + if parent.ParentID == nil { + switch parent.Kind { + case models.CatKindForum: + name = "Forums" + case models.CatKindBlog: + name = "Blog" + } + } breadcrumbs = append(breadcrumbs, templates.Breadcrumb{ - Name: *parents[i].Name, - Url: "nargle", // TODO + Name: name, + Url: categoryUrls[parent.ID], }) } postItems = append(postItems, templates.PostListItem{ Title: postResult.Thread.Title, - Url: templates.PostUrl(postResult.Post, postResult.Post.CategoryType, postResult.Proj.Subdomain()), + Url: PostUrl(postResult.Post, postResult.Post.CategoryKind, categoryUrls[postResult.Post.CategoryID]), User: templates.UserToTemplate(&postResult.User), Date: postResult.Post.PostDate, Breadcrumbs: breadcrumbs, diff --git a/src/website/landing.go b/src/website/landing.go index 77461f25..879e08ee 100644 --- a/src/website/landing.go +++ b/src/website/landing.go @@ -64,6 +64,8 @@ func Index(c *RequestContext) ResponseData { c.Perf.EndBlock() c.Logger.Info().Interface("allProjects", allProjects).Msg("all the projects") + categoryUrls := GetAllCategoryUrls(c.Context(), c.Conn) + var currentUserId *int if c.CurrentUser != nil { currentUserId = &c.CurrentUser.ID @@ -105,7 +107,7 @@ func Index(c *RequestContext) ResponseData { `, currentUserId, proj.ID, - models.CatTypeBlog, models.CatTypeForum, models.CatTypeWiki, models.CatTypeLibraryResource, + models.CatKindBlog, models.CatKindForum, models.CatKindWiki, models.CatKindLibraryResource, maxPosts, ) c.Perf.EndBlock() @@ -130,7 +132,7 @@ func Index(c *RequestContext) ResponseData { } featurable := (!proj.IsHMN() && - projectPost.Post.CategoryType == models.CatTypeBlog && + projectPost.Post.CategoryKind == models.CatKindBlog && projectPost.Post.ParentID == nil && landingPageProject.FeaturedPost == nil) @@ -156,7 +158,7 @@ func Index(c *RequestContext) ResponseData { landingPageProject.FeaturedPost = &LandingPageFeaturedPost{ Title: projectPost.Thread.Title, - Url: templates.PostUrl(projectPost.Post, projectPost.Post.CategoryType, proj.Subdomain()), + Url: PostUrl(projectPost.Post, projectPost.Post.CategoryKind, categoryUrls[projectPost.Post.CategoryID]), User: templates.UserToTemplate(&projectPost.User), Date: projectPost.Post.PostDate, Unread: !hasRead, @@ -165,7 +167,7 @@ func Index(c *RequestContext) ResponseData { } else { landingPageProject.Posts = append(landingPageProject.Posts, templates.PostListItem{ Title: projectPost.Thread.Title, - Url: templates.PostUrl(projectPost.Post, projectPost.Post.CategoryType, proj.Subdomain()), + Url: PostUrl(projectPost.Post, projectPost.Post.CategoryKind, categoryUrls[projectPost.Post.CategoryID]), User: templates.UserToTemplate(&projectPost.User), Date: projectPost.Post.PostDate, Unread: !hasRead, @@ -198,7 +200,7 @@ func Index(c *RequestContext) ResponseData { AND cat.kind = $2 `, models.HMNProjectID, - models.CatTypeBlog, + models.CatKindBlog, ) if err != nil { return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch latest news post")) @@ -261,7 +263,7 @@ func Index(c *RequestContext) ResponseData { LIMIT 1 `, models.HMNProjectID, - models.CatTypeBlog, + models.CatKindBlog, ) if err != nil { return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch news post")) @@ -272,11 +274,11 @@ func Index(c *RequestContext) ResponseData { baseData.BodyClasses = append(baseData.BodyClasses, "hmdev", "landing") // TODO: Is "hmdev" necessary any more? var res ResponseData - err = res.WriteTemplate("index.html", LandingTemplateData{ + err = res.WriteTemplate("landing.html", LandingTemplateData{ BaseData: baseData, NewsPost: LandingPageFeaturedPost{ Title: newsPostResult.Thread.Title, - Url: templates.PostUrl(newsPostResult.Post, models.CatTypeBlog, ""), + Url: PostUrl(newsPostResult.Post, models.CatKindBlog, ""), User: templates.UserToTemplate(&newsPostResult.User), Date: newsPostResult.Post.PostDate, Unread: true, // TODO diff --git a/src/website/urls.go b/src/website/urls.go new file mode 100644 index 00000000..984f9977 --- /dev/null +++ b/src/website/urls.go @@ -0,0 +1,126 @@ +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 + `, + ) + 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) + `, + projectId, + ) + 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.Subdomain(), hierarchy...) + } + + return result +} + +func CategoryUrl(subdomain string, cats ...*models.Category) string { + path := "" + for i, cat := range cats { + if i == 0 { + switch cat.Kind { + case models.CatKindBlog: + path += "/blogs" + case models.CatKindForum: + path += "/forums" + // TODO: All cat types? + default: + return "" + } + } else { + path += "/" + *cat.Slug + } + } + + return hmnurl.ProjectUrl(path, nil, subdomain) +} + +func PostUrl(post models.Post, catKind models.CategoryKind, categoryUrl string) string { + categoryUrl = strings.TrimRight(categoryUrl, "/") + + switch catKind { + // TODO: All the relevant post types. Maybe it doesn't make sense to lump them all together here. + case models.CatKindBlog: + return fmt.Sprintf("%s/p/%d/e/%d", categoryUrl, post.ThreadID, post.ID) + case models.CatKindForum: + return fmt.Sprintf("%s/t/%d/p/%d", categoryUrl, post.ThreadID, post.ID) + } + + return "" +}