Added projects atom feed and media file urls
This commit is contained in:
parent
5d9b628144
commit
63f1bf40cc
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue