Implement post links in feed / landing page

This commit is contained in:
Ben Visness 2021-04-27 22:29:13 -05:00
parent 4f9df3382f
commit 5d697e5fff
8 changed files with 219 additions and 112 deletions

View File

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

View File

@ -4,19 +4,18 @@ import (
"context" "context"
"git.handmade.network/hmn/hmn/src/db" "git.handmade.network/hmn/hmn/src/db"
"git.handmade.network/hmn/hmn/src/hmnurl"
"github.com/jackc/pgx/v4/pgxpool" "github.com/jackc/pgx/v4/pgxpool"
) )
type CategoryType int type CategoryKind int
const ( const (
CatTypeBlog CategoryType = iota + 1 CatKindBlog CategoryKind = iota + 1
CatTypeForum CatKindForum
CatTypeStatic CatKindStatic
CatTypeAnnotation CatKindAnnotation
CatTypeWiki CatKindWiki
CatTypeLibraryResource CatKindLibraryResource
) )
type Category struct { type Category struct {
@ -28,7 +27,7 @@ type Category struct {
Slug *string `db:"slug"` // TODO: Make not null Slug *string `db:"slug"` // TODO: Make not null
Name *string `db:"name"` // TODO: Make not null Name *string `db:"name"` // TODO: Make not null
Blurb *string `db:"blurb"` // 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"` Color1 string `db:"color_1"`
Color2 string `db:"color_2"` Color2 string `db:"color_2"`
Depth int `db:"depth"` // TODO: What is this? 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 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)
}

View File

@ -16,7 +16,7 @@ type Post struct {
CurrentID int `db:"current_id"` CurrentID int `db:"current_id"`
ProjectID int `db:"project_id"` ProjectID int `db:"project_id"`
CategoryType CategoryType `db:"category_kind"` CategoryKind CategoryKind `db:"category_kind"`
Depth int `db:"depth"` Depth int `db:"depth"`
Slug string `db:"slug"` Slug string `db:"slug"`

View File

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

View File

@ -7,6 +7,7 @@ import (
"time" "time"
"git.handmade.network/hmn/hmn/src/db" "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/logging"
"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"
@ -30,14 +31,11 @@ func Feed(c *RequestContext) ResponseData {
FROM FROM
handmade_post AS post handmade_post AS post
WHERE WHERE
post.category_kind IN ($1, $2, $3, $4) post.category_kind = ANY ($1)
AND NOT moderated AND NOT moderated
`, `,
models.CatTypeForum, []models.CategoryKind{models.CatKindForum, models.CatKindBlog, models.CatKindWiki, models.CatKindLibraryResource},
models.CatTypeBlog, )
models.CatTypeWiki,
models.CatTypeLibraryResource,
) // TODO(inarray)
c.Perf.EndBlock() c.Perf.EndBlock()
if err != nil { if err != nil {
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to get count of feed posts")) 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") c.Perf.StartBlock("SQL", "Fetch posts")
type feedPostQuery struct { type feedPostQuery struct {
Post models.Post `db:"post"` Post models.Post `db:"post"`
Thread models.Thread `db:"thread"` Thread models.Thread `db:"thread"`
Proj models.Project `db:"proj"` Cat models.Category `db:"cat"`
User models.User `db:"auth_user"` Proj models.Project `db:"proj"`
ThreadLastReadTime *time.Time `db:"tlri.lastread"` User models.User `db:"auth_user"`
CatLastReadTime *time.Time `db:"clri.lastread"` ThreadLastReadTime *time.Time `db:"tlri.lastread"`
CatLastReadTime *time.Time `db:"clri.lastread"`
} }
posts, err := db.Query(c.Context(), c.Conn, feedPostQuery{}, posts, err := db.Query(c.Context(), c.Conn, feedPostQuery{},
` `
@ -87,6 +86,7 @@ func Feed(c *RequestContext) ResponseData {
FROM FROM
handmade_post AS post handmade_post AS post
JOIN handmade_thread AS thread ON thread.id = post.thread_id 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 JOIN handmade_project AS proj ON proj.id = post.project_id
LEFT OUTER JOIN handmade_threadlastreadinfo AS tlri ON ( LEFT OUTER JOIN handmade_threadlastreadinfo AS tlri ON (
tlri.thread_id = post.thread_id 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 LEFT OUTER JOIN auth_user ON post.author_id = auth_user.id
WHERE WHERE
post.category_kind IN ($2, $3, $4, $5) post.category_kind = ANY ($2)
AND post.moderated = FALSE AND post.moderated = FALSE
AND post.thread_id IS NOT NULL AND post.thread_id IS NOT NULL
ORDER BY postdate DESC ORDER BY postdate DESC
LIMIT $6 OFFSET $7 LIMIT $3 OFFSET $4
`, `,
currentUserId, currentUserId,
models.CatTypeForum, []models.CategoryKind{models.CatKindForum, models.CatKindBlog, models.CatKindWiki, models.CatKindLibraryResource},
models.CatTypeBlog,
models.CatTypeWiki,
models.CatTypeLibraryResource,
postsPerPage, postsPerPage,
howManyPostsToSkip, howManyPostsToSkip,
) // TODO(inarray) )
c.Perf.EndBlock() c.Perf.EndBlock()
if err != nil { if err != nil {
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)
var postItems []templates.PostListItem var postItems []templates.PostListItem
for _, iPostResult := range posts.ToSlice() { for _, iPostResult := range posts.ToSlice() {
postResult := iPostResult.(*feedPostQuery) postResult := iPostResult.(*feedPostQuery)
@ -128,25 +127,33 @@ func Feed(c *RequestContext) ResponseData {
hasRead = true 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("") logging.Debug().Interface("parents", parents).Msg("")
var breadcrumbs []templates.Breadcrumb var breadcrumbs []templates.Breadcrumb
breadcrumbs = append(breadcrumbs, templates.Breadcrumb{ breadcrumbs = append(breadcrumbs, templates.Breadcrumb{
Name: *postResult.Proj.Name, 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{ breadcrumbs = append(breadcrumbs, templates.Breadcrumb{
Name: *parents[i].Name, Name: name,
Url: "nargle", // TODO Url: categoryUrls[parent.ID],
}) })
} }
postItems = append(postItems, templates.PostListItem{ postItems = append(postItems, templates.PostListItem{
Title: postResult.Thread.Title, 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), User: templates.UserToTemplate(&postResult.User),
Date: postResult.Post.PostDate, Date: postResult.Post.PostDate,
Breadcrumbs: breadcrumbs, Breadcrumbs: breadcrumbs,

View File

@ -64,6 +64,8 @@ func Index(c *RequestContext) ResponseData {
c.Perf.EndBlock() c.Perf.EndBlock()
c.Logger.Info().Interface("allProjects", allProjects).Msg("all the projects") c.Logger.Info().Interface("allProjects", allProjects).Msg("all the projects")
categoryUrls := GetAllCategoryUrls(c.Context(), c.Conn)
var currentUserId *int var currentUserId *int
if c.CurrentUser != nil { if c.CurrentUser != nil {
currentUserId = &c.CurrentUser.ID currentUserId = &c.CurrentUser.ID
@ -105,7 +107,7 @@ func Index(c *RequestContext) ResponseData {
`, `,
currentUserId, currentUserId,
proj.ID, proj.ID,
models.CatTypeBlog, models.CatTypeForum, models.CatTypeWiki, models.CatTypeLibraryResource, models.CatKindBlog, models.CatKindForum, models.CatKindWiki, models.CatKindLibraryResource,
maxPosts, maxPosts,
) )
c.Perf.EndBlock() c.Perf.EndBlock()
@ -130,7 +132,7 @@ func Index(c *RequestContext) ResponseData {
} }
featurable := (!proj.IsHMN() && featurable := (!proj.IsHMN() &&
projectPost.Post.CategoryType == models.CatTypeBlog && projectPost.Post.CategoryKind == models.CatKindBlog &&
projectPost.Post.ParentID == nil && projectPost.Post.ParentID == nil &&
landingPageProject.FeaturedPost == nil) landingPageProject.FeaturedPost == nil)
@ -156,7 +158,7 @@ func Index(c *RequestContext) ResponseData {
landingPageProject.FeaturedPost = &LandingPageFeaturedPost{ landingPageProject.FeaturedPost = &LandingPageFeaturedPost{
Title: projectPost.Thread.Title, 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), User: templates.UserToTemplate(&projectPost.User),
Date: projectPost.Post.PostDate, Date: projectPost.Post.PostDate,
Unread: !hasRead, Unread: !hasRead,
@ -165,7 +167,7 @@ func Index(c *RequestContext) ResponseData {
} else { } else {
landingPageProject.Posts = append(landingPageProject.Posts, templates.PostListItem{ landingPageProject.Posts = append(landingPageProject.Posts, templates.PostListItem{
Title: projectPost.Thread.Title, 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), User: templates.UserToTemplate(&projectPost.User),
Date: projectPost.Post.PostDate, Date: projectPost.Post.PostDate,
Unread: !hasRead, Unread: !hasRead,
@ -198,7 +200,7 @@ func Index(c *RequestContext) ResponseData {
AND cat.kind = $2 AND cat.kind = $2
`, `,
models.HMNProjectID, models.HMNProjectID,
models.CatTypeBlog, models.CatKindBlog,
) )
if err != nil { if err != nil {
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch latest news post")) return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch latest news post"))
@ -261,7 +263,7 @@ func Index(c *RequestContext) ResponseData {
LIMIT 1 LIMIT 1
`, `,
models.HMNProjectID, models.HMNProjectID,
models.CatTypeBlog, models.CatKindBlog,
) )
if err != nil { if err != nil {
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch news post")) 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? baseData.BodyClasses = append(baseData.BodyClasses, "hmdev", "landing") // TODO: Is "hmdev" necessary any more?
var res ResponseData var res ResponseData
err = res.WriteTemplate("index.html", LandingTemplateData{ err = res.WriteTemplate("landing.html", LandingTemplateData{
BaseData: baseData, BaseData: baseData,
NewsPost: LandingPageFeaturedPost{ NewsPost: LandingPageFeaturedPost{
Title: newsPostResult.Thread.Title, Title: newsPostResult.Thread.Title,
Url: templates.PostUrl(newsPostResult.Post, models.CatTypeBlog, ""), Url: PostUrl(newsPostResult.Post, models.CatKindBlog, ""),
User: templates.UserToTemplate(&newsPostResult.User), User: templates.UserToTemplate(&newsPostResult.User),
Date: newsPostResult.Post.PostDate, Date: newsPostResult.Post.PostDate,
Unread: true, // TODO Unread: true, // TODO

126
src/website/urls.go Normal file
View File

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