Added projects atom feed and media file urls

This commit is contained in:
Asaf Gartner 2021-06-01 02:23:04 +03:00
parent 5d9b628144
commit 63f1bf40cc
7 changed files with 112 additions and 5 deletions

View File

@ -52,6 +52,10 @@ func TestAtomFeed(t *testing.T) {
AssertRegexMatch(t, BuildAtomFeed(), RegexAtomFeed, nil) AssertRegexMatch(t, BuildAtomFeed(), RegexAtomFeed, nil)
AssertRegexMatch(t, BuildAtomFeedForProjects(), RegexAtomFeed, map[string]string{"feedtype": "projects"}) AssertRegexMatch(t, BuildAtomFeedForProjects(), RegexAtomFeed, map[string]string{"feedtype": "projects"})
AssertRegexMatch(t, BuildAtomFeedForShowcase(), RegexAtomFeed, map[string]string{"feedtype": "showcase"}) AssertRegexMatch(t, BuildAtomFeedForShowcase(), RegexAtomFeed, map[string]string{"feedtype": "showcase"})
// NOTE(asaf): The following tests are for backwards compatibity
AssertRegexMatch(t, "/atom/projects/new", RegexAtomFeed, map[string]string{"feedtype": "projects"})
AssertRegexMatch(t, "/atom/showcase/new", RegexAtomFeed, map[string]string{"feedtype": "showcase"})
} }
func TestLoginAction(t *testing.T) { func TestLoginAction(t *testing.T) {
@ -343,6 +347,7 @@ func TestPublic(t *testing.T) {
assert.Panics(t, func() { BuildPublic("/thing/image.png?hello", false) }) assert.Panics(t, func() { BuildPublic("/thing/image.png?hello", false) })
AssertRegexMatch(t, BuildTheme("test.css", "light", true), RegexPublic, nil) AssertRegexMatch(t, BuildTheme("test.css", "light", true), RegexPublic, nil)
AssertRegexMatch(t, BuildUserFile("mylogo.png"), RegexPublic, nil)
} }
func TestMarkRead(t *testing.T) { func TestMarkRead(t *testing.T) {

View File

@ -169,7 +169,7 @@ func BuildFeedWithPage(page int) string {
return Url("/feed/"+strconv.Itoa(page), nil) return Url("/feed/"+strconv.Itoa(page), nil)
} }
var RegexAtomFeed = regexp.MustCompile("^/atom(/(?P<feedtype>.+))?$") var RegexAtomFeed = regexp.MustCompile("^/atom(/(?P<feedtype>[^/]+))?(/new)?$") // NOTE(asaf): `/new` for backwards compatibility with old website
func BuildAtomFeed() string { func BuildAtomFeed() string {
defer CatchPanic() defer CatchPanic()
@ -682,10 +682,16 @@ func BuildPublic(filepath string, cachebust bool) string {
func BuildTheme(filepath string, theme string, cachebust bool) string { func BuildTheme(filepath string, theme string, cachebust bool) string {
defer CatchPanic() defer CatchPanic()
filepath = strings.Trim(filepath, "/")
if len(theme) == 0 { if len(theme) == 0 {
panic(oops.New(nil, "Theme can't be blank")) panic(oops.New(nil, "Theme can't be blank"))
} }
return BuildPublic(fmt.Sprintf("themes/%s/%s", theme, strings.Trim(filepath, "/")), cachebust) return BuildPublic(fmt.Sprintf("themes/%s/%s", theme, filepath), cachebust)
}
func BuildUserFile(filepath string) string {
filepath = strings.Trim(filepath, "/")
return BuildPublic(fmt.Sprintf("media/%s", filepath), false)
} }
/* /*

View File

@ -39,6 +39,10 @@ type Project struct {
Color1 string `db:"color_1"` Color1 string `db:"color_1"`
Color2 string `db:"color_2"` Color2 string `db:"color_2"`
LogoLight string `db:"logolight"`
LogoDark string `db:"logodark"`
DateApproved time.Time `db:"date_approved"`
AllLastUpdated time.Time `db:"all_last_updated"` AllLastUpdated time.Time `db:"all_last_updated"`
} }

View File

@ -58,6 +58,10 @@ func ProjectToTemplate(p *models.Project) Project {
Color1: p.Color1, Color1: p.Color1,
Color2: p.Color2, Color2: p.Color2,
Url: hmnurl.BuildProjectHomepage(p.Slug), Url: hmnurl.BuildProjectHomepage(p.Slug),
Blurb: p.Blurb,
LogoLight: hmnurl.BuildUserFile(p.LogoLight),
LogoDark: hmnurl.BuildUserFile(p.LogoDark),
IsHMN: p.IsHMN(), IsHMN: p.IsHMN(),
@ -65,6 +69,8 @@ func ProjectToTemplate(p *models.Project) Project {
HasForum: true, HasForum: true,
HasWiki: true, HasWiki: true,
HasLibrary: true, HasLibrary: true,
DateApproved: p.DateApproved,
} }
} }
@ -78,10 +84,13 @@ func ThreadToTemplate(t *models.Thread) Thread {
func UserToTemplate(u *models.User, currentTheme string) User { func UserToTemplate(u *models.User, currentTheme string) User {
// TODO: Handle deleted users. Maybe not here, but if not, at call sites of this function. // TODO: Handle deleted users. Maybe not here, but if not, at call sites of this function.
if currentTheme == "" {
currentTheme = "light"
}
avatar := "" avatar := ""
if u.Avatar != nil && len(*u.Avatar) > 0 { if u.Avatar != nil && len(*u.Avatar) > 0 {
avatar = hmnurl.BuildPublic(*u.Avatar, false) avatar = hmnurl.BuildUserFile(*u.Avatar)
} else { } else {
avatar = hmnurl.BuildTheme("empty-avatar.svg", currentTheme, true) avatar = hmnurl.BuildTheme("empty-avatar.svg", currentTheme, true)
} }

View File

@ -26,6 +26,20 @@
{{ end }} {{ end }}
{{ else if .Projects }} {{ else if .Projects }}
{{ range .Projects }} {{ range .Projects }}
<entry>
<title>{{ .Name }}</title>
<link rel="alternate" type="text/html" href="{{ .Url }}" />
<id>{{ .UUID }}</id>
<published>{{ rfc3339 .DateApproved }}</published>
{{ range .Owners }}
<author>
<name>{{ .Name }}</name>
<uri>{{ .ProfileUrl }}</uri>
</author>
{{ end }}
<logo>{{ .LogoLight }}</logo>
<summary type="html">{{ .Blurb }}</summary>
</entry>
{{ end }} {{ end }}
{{ else if .Snippets }} {{ else if .Snippets }}
{{ range .Snippets }} {{ range .Snippets }}

View File

@ -91,6 +91,11 @@ type Project struct {
Color1 string Color1 string
Color2 string Color2 string
Url string Url string
Blurb string
Owners []User
LogoDark string
LogoLight string
IsHMN bool IsHMN bool
@ -98,6 +103,9 @@ type Project struct {
HasForum bool HasForum bool
HasWiki bool HasWiki bool
HasLibrary bool HasLibrary bool
UUID string
DateApproved time.Time
} }
type User struct { type User struct {

View File

@ -136,7 +136,7 @@ type AtomFeedData struct {
FeedType FeedType FeedType FeedType
Posts []templates.PostListItem Posts []templates.PostListItem
Projects []int // TODO(asaf): Actually do this Projects []templates.Project
Snippets []int // TODO(asaf): Actually do this Snippets []int // TODO(asaf): Actually do this
} }
@ -178,7 +178,68 @@ func AtomFeed(c *RequestContext) ResponseData {
} else { } else {
switch strings.ToLower(feedType) { switch strings.ToLower(feedType) {
case "projects": case "projects":
// TODO(asaf): Implement this feedData.Title = "New Projects | Site-wide | Handmade.Network"
feedData.Subtitle = feedData.Title
feedData.FeedType = FeedTypeProjects
feedData.FeedID = FeedIDProjects
feedData.AtomFeedUrl = hmnurl.BuildAtomFeedForProjects()
feedData.FeedUrl = hmnurl.BuildProjectIndex()
c.Perf.StartBlock("SQL", "Fetching projects")
type projectResult struct {
Project models.Project `db:"project"`
}
projects, err := db.Query(c.Context(), c.Conn, projectResult{},
`
SELECT $columns
FROM handmade_project AS project
ORDER BY date_approved DESC
LIMIT $1
`,
itemsPerFeed,
)
if err != nil {
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch feed projects"))
}
var projectIds []int
projectMap := make(map[int]*templates.Project)
for _, p := range projects.ToSlice() {
project := p.(*projectResult).Project
templateProject := templates.ProjectToTemplate(&project)
templateProject.UUID = uuid.NewSHA1(uuid.NameSpaceURL, []byte(templateProject.Url)).URN()
projectIds = append(projectIds, project.ID)
projectMap[project.ID] = &templateProject
feedData.Projects = append(feedData.Projects, templateProject)
}
c.Perf.EndBlock()
c.Perf.StartBlock("SQL", "Fetching project owners")
type ownerResult struct {
User models.User `db:"auth_user"`
ProjectID int `db:"project_groups.project_id"`
}
owners, err := db.Query(c.Context(), c.Conn, ownerResult{},
`
SELECT $columns
FROM
auth_user
INNER JOIN auth_user_groups AS user_groups ON auth_user.id = user_groups.user_id
INNER JOIN handmade_project_groups AS project_groups ON user_groups.group_id = project_groups.group_id
WHERE
project_groups.project_id = ANY($1)
`,
projectIds,
)
if err != nil {
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch feed projects owners"))
}
for _, res := range owners.ToSlice() {
owner := res.(*ownerResult)
templateProject := projectMap[owner.ProjectID]
templateProject.Owners = append(templateProject.Owners, templates.UserToTemplate(&owner.User, ""))
}
c.Perf.EndBlock()
case "showcase": case "showcase":
// TODO(asaf): Implement this // TODO(asaf): Implement this
default: default: