diff --git a/src/hmnurl/hmnurl_test.go b/src/hmnurl/hmnurl_test.go index 512404d..2b9d8e1 100644 --- a/src/hmnurl/hmnurl_test.go +++ b/src/hmnurl/hmnurl_test.go @@ -52,6 +52,10 @@ func TestAtomFeed(t *testing.T) { AssertRegexMatch(t, BuildAtomFeed(), RegexAtomFeed, nil) AssertRegexMatch(t, BuildAtomFeedForProjects(), RegexAtomFeed, map[string]string{"feedtype": "projects"}) 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) { @@ -343,6 +347,7 @@ func TestPublic(t *testing.T) { assert.Panics(t, func() { BuildPublic("/thing/image.png?hello", false) }) AssertRegexMatch(t, BuildTheme("test.css", "light", true), RegexPublic, nil) + AssertRegexMatch(t, BuildUserFile("mylogo.png"), RegexPublic, nil) } func TestMarkRead(t *testing.T) { diff --git a/src/hmnurl/urls.go b/src/hmnurl/urls.go index 40c25a3..6c2cd37 100644 --- a/src/hmnurl/urls.go +++ b/src/hmnurl/urls.go @@ -169,7 +169,7 @@ func BuildFeedWithPage(page int) string { return Url("/feed/"+strconv.Itoa(page), nil) } -var RegexAtomFeed = regexp.MustCompile("^/atom(/(?P.+))?$") +var RegexAtomFeed = regexp.MustCompile("^/atom(/(?P[^/]+))?(/new)?$") // NOTE(asaf): `/new` for backwards compatibility with old website func BuildAtomFeed() string { defer CatchPanic() @@ -682,10 +682,16 @@ func BuildPublic(filepath string, cachebust bool) string { func BuildTheme(filepath string, theme string, cachebust bool) string { defer CatchPanic() + filepath = strings.Trim(filepath, "/") if len(theme) == 0 { 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) } /* diff --git a/src/models/project.go b/src/models/project.go index d852a24..236908b 100644 --- a/src/models/project.go +++ b/src/models/project.go @@ -39,6 +39,10 @@ type Project struct { Color1 string `db:"color_1"` 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"` } diff --git a/src/templates/mapping.go b/src/templates/mapping.go index 0491478..ba0f7a7 100644 --- a/src/templates/mapping.go +++ b/src/templates/mapping.go @@ -58,6 +58,10 @@ func ProjectToTemplate(p *models.Project) Project { Color1: p.Color1, Color2: p.Color2, Url: hmnurl.BuildProjectHomepage(p.Slug), + Blurb: p.Blurb, + + LogoLight: hmnurl.BuildUserFile(p.LogoLight), + LogoDark: hmnurl.BuildUserFile(p.LogoDark), IsHMN: p.IsHMN(), @@ -65,6 +69,8 @@ func ProjectToTemplate(p *models.Project) Project { HasForum: true, HasWiki: true, HasLibrary: true, + + DateApproved: p.DateApproved, } } @@ -78,10 +84,13 @@ func ThreadToTemplate(t *models.Thread) Thread { func UserToTemplate(u *models.User, currentTheme string) User { // TODO: Handle deleted users. Maybe not here, but if not, at call sites of this function. + if currentTheme == "" { + currentTheme = "light" + } avatar := "" if u.Avatar != nil && len(*u.Avatar) > 0 { - avatar = hmnurl.BuildPublic(*u.Avatar, false) + avatar = hmnurl.BuildUserFile(*u.Avatar) } else { avatar = hmnurl.BuildTheme("empty-avatar.svg", currentTheme, true) } diff --git a/src/templates/src/atom.xml b/src/templates/src/atom.xml index 9542ff8..9e028e2 100644 --- a/src/templates/src/atom.xml +++ b/src/templates/src/atom.xml @@ -26,6 +26,20 @@ {{ end }} {{ else if .Projects }} {{ range .Projects }} + + {{ .Name }} + + {{ .UUID }} + {{ rfc3339 .DateApproved }} + {{ range .Owners }} + + {{ .Name }} + {{ .ProfileUrl }} + + {{ end }} + {{ .LogoLight }} + {{ .Blurb }} + {{ end }} {{ else if .Snippets }} {{ range .Snippets }} diff --git a/src/templates/types.go b/src/templates/types.go index fee8b88..d18a6ff 100644 --- a/src/templates/types.go +++ b/src/templates/types.go @@ -91,6 +91,11 @@ type Project struct { Color1 string Color2 string Url string + Blurb string + Owners []User + + LogoDark string + LogoLight string IsHMN bool @@ -98,6 +103,9 @@ type Project struct { HasForum bool HasWiki bool HasLibrary bool + + UUID string + DateApproved time.Time } type User struct { diff --git a/src/website/feed.go b/src/website/feed.go index 74b7ff3..58ec7d8 100644 --- a/src/website/feed.go +++ b/src/website/feed.go @@ -136,7 +136,7 @@ type AtomFeedData struct { FeedType FeedType Posts []templates.PostListItem - Projects []int // TODO(asaf): Actually do this + Projects []templates.Project Snippets []int // TODO(asaf): Actually do this } @@ -178,7 +178,68 @@ func AtomFeed(c *RequestContext) ResponseData { } else { switch strings.ToLower(feedType) { 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": // TODO(asaf): Implement this default: