Added jam/project association.

This commit is contained in:
Asaf Gartner 2022-06-20 01:26:33 +03:00
parent 359354f2aa
commit 9d1d249ec0
9 changed files with 373 additions and 43 deletions

116
src/hmndata/jams.go Normal file
View File

@ -0,0 +1,116 @@
package hmndata
import (
"context"
"sort"
"time"
"git.handmade.network/hmn/hmn/src/db"
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops"
)
type Jam struct {
Name string
Slug string
StartTime time.Time
EndTime time.Time
}
var WRJ2021 = Jam{
Name: "Wheel Reinvention Jam 2021",
Slug: "WRJ2021",
StartTime: time.Date(2021, 9, 27, 0, 0, 0, 0, time.UTC),
EndTime: time.Date(2021, 10, 4, 0, 0, 0, 0, time.UTC),
}
var WRJ2022 = Jam{
Name: "Wheel Reinvention Jam 2022",
Slug: "WRJ2022",
// StartTime: time.Date(2022, 8, 15, 0, 0, 0, 0, time.UTC),
StartTime: time.Date(2021, 8, 15, 0, 0, 0, 0, time.UTC),
EndTime: time.Date(2022, 8, 22, 0, 0, 0, 0, time.UTC),
}
var AllJams = []Jam{WRJ2021, WRJ2022}
func CurrentJam() *Jam {
now := time.Now()
for i, jam := range AllJams {
if jam.StartTime.Before(now) && now.Before(jam.EndTime) {
return &AllJams[i]
}
}
return nil
}
func JamBySlug(slug string) Jam {
for _, jam := range AllJams {
if jam.Slug == slug {
return jam
}
}
return Jam{Slug: slug}
}
func FetchJamsForProject(ctx context.Context, dbConn db.ConnOrTx, user *models.User, projectId int) ([]*models.JamProject, error) {
jamProjects, err := db.Query[models.JamProject](ctx, dbConn,
`
SELECT $columns
FROM jam_project
WHERE project_id = $1
`,
projectId,
)
if err != nil {
return nil, oops.New(err, "failed to fetch jams for project")
}
currentJam := CurrentJam()
foundCurrent := false
for i, _ := range jamProjects {
jam := JamBySlug(jamProjects[i].JamSlug)
jamProjects[i].JamName = jam.Name
jamProjects[i].JamStartTime = jam.StartTime
if currentJam != nil && currentJam.Slug == jamProjects[i].JamSlug {
foundCurrent = true
}
}
if currentJam != nil && !foundCurrent {
jamProjects = append(jamProjects, &models.JamProject{
ProjectID: projectId,
JamSlug: currentJam.Slug,
Participating: false,
JamName: currentJam.Name,
JamStartTime: currentJam.StartTime,
})
}
if user != nil && user.IsStaff {
for _, jam := range AllJams {
found := false
for _, jp := range jamProjects {
if jp.JamSlug == jam.Slug {
found = true
break
}
}
if !found {
jamProjects = append(jamProjects, &models.JamProject{
ProjectID: projectId,
JamSlug: jam.Slug,
Participating: false,
JamName: jam.Name,
JamStartTime: jam.StartTime,
})
}
}
}
sort.Slice(jamProjects, func(i, j int) bool {
return jamProjects[i].JamStartTime.Before(jamProjects[j].JamStartTime)
})
return jamProjects, nil
}

View File

@ -56,7 +56,26 @@ func BuildJamIndex() string {
return Url("/jam", nil) return Url("/jam", nil)
} }
var RegexJamIndex2021 = regexp.MustCompile("^/jam/2021") var RegexJamIndex2021 = regexp.MustCompile("^/jam/2021$")
func BuildJamIndex2021() string {
defer CatchPanic()
return Url("/jam/2021", nil)
}
var RegexJamIndex2022 = regexp.MustCompile("^/jam/2022$")
func BuildJamIndex2022() string {
defer CatchPanic()
return Url("/jam/2022", nil)
}
var RegexJamFeed2022 = regexp.MustCompile("^/jam/2022/feed$")
func BuildJamFeed2022() string {
defer CatchPanic()
return Url("/jam/2022/feed", nil)
}
// QUESTION(ben): Can we change these routes? // QUESTION(ben): Can we change these routes?

View File

@ -0,0 +1,54 @@
package migrations
import (
"context"
"time"
"git.handmade.network/hmn/hmn/src/migration/types"
"github.com/jackc/pgx/v4"
)
func init() {
registerMigration(AddJamProjects{})
}
type AddJamProjects struct{}
func (m AddJamProjects) Version() types.MigrationVersion {
return types.MigrationVersion(time.Date(2022, 6, 18, 1, 3, 39, 0, time.UTC))
}
func (m AddJamProjects) Name() string {
return "AddJamProjects"
}
func (m AddJamProjects) Description() string {
return "Add jam and project association table"
}
func (m AddJamProjects) Up(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(ctx,
`
CREATE TABLE jam_project (
project_id INT REFERENCES project (id) ON DELETE CASCADE,
jam_slug VARCHAR(64) NOT NULL,
participating BOOLEAN NOT NULL DEFAULT FALSE,
UNIQUE (project_id, jam_slug)
);
CREATE INDEX jam_project_jam_slug ON jam_project (jam_slug);
CREATE INDEX jam_project_project_id ON jam_project (project_id);
`,
)
return err
}
func (m AddJamProjects) Down(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(ctx,
`
DROP INDEX jam_project_jam_slug;
DROP INDEX jam_project_project_id;
DROP TABLE jam_project;
`,
)
return err
}

11
src/models/jamproject.go Normal file
View File

@ -0,0 +1,11 @@
package models
import "time"
type JamProject struct {
ProjectID int `db:"project_id"`
JamSlug string `db:"jam_slug"`
Participating bool `db:"participating"`
JamName string
JamStartTime time.Time
}

View File

@ -20,7 +20,7 @@
{{ if .Editing }} {{ if .Editing }}
<h1>Edit {{ .ProjectSettings.Name }}</h1> <h1>Edit {{ .ProjectSettings.Name }}</h1>
{{ else }} {{ else }}
<h1>Create a new project</h1> <h1>Create a new {{ if .ProjectSettings.JamParticipation }}jam {{ end }}project</h1>
{{ end }} {{ end }}
<form id="project_form" class="tabbed edit-form" method="POST" enctype="multipart/form-data"> <form id="project_form" class="tabbed edit-form" method="POST" enctype="multipart/form-data">
{{ csrftoken .Session }} {{ csrftoken .Session }}
@ -81,6 +81,20 @@
<div class="c--dim f7" id="tag-discord-info">If you have linked your Discord account, any #project-showcase messages with the tag "&amp;<span id="tag-preview"></span>" will automatically be associated with this project.</div> <div class="c--dim f7" id="tag-discord-info">If you have linked your Discord account, any #project-showcase messages with the tag "&amp;<span id="tag-preview"></span>" will automatically be associated with this project.</div>
</div> </div>
</div> </div>
{{ if .ProjectSettings.JamParticipation }}
<div class="edit-form-row">
<div class="pt-input-ns">Jam Participation</div>
</div>
{{ range .ProjectSettings.JamParticipation }}
<div class="edit-form-row">
<div>{{ .JamName }}:</div>
<div>
<input id="jam_{{ .JamSlug }}" type="checkbox" name="jam_participation" value="{{ .JamSlug }}" {{ if .Participating }}checked{{ end }} />
<label for="jam_{{ .JamSlug }}">Participating</label>
</div>
</div>
{{ end }}
{{ end }}
{{ if and .Editing .User.IsStaff }} {{ if and .Editing .User.IsStaff }}
<div class="edit-form-row"> <div class="edit-form-row">
<div class="pt-input-ns">Admin settings</div> <div class="pt-input-ns">Admin settings</div>

View File

@ -138,13 +138,14 @@ type Project struct {
} }
type ProjectSettings struct { type ProjectSettings struct {
Name string Name string
Slug string Slug string
Hidden bool Hidden bool
Featured bool Featured bool
Personal bool Personal bool
Lifecycle string Lifecycle string
Tag string Tag string
JamParticipation []ProjectJamParticipation
Blurb string Blurb string
Description string Description string
@ -155,6 +156,12 @@ type ProjectSettings struct {
DarkLogo string DarkLogo string
} }
type ProjectJamParticipation struct {
JamName string
JamSlug string
Participating bool
}
type User struct { type User struct {
ID int ID int
Username string Username string

View File

@ -14,12 +14,13 @@ import (
func JamIndex2022(c *RequestContext) ResponseData { func JamIndex2022(c *RequestContext) ResponseData {
var res ResponseData var res ResponseData
jamStartTime := time.Date(2022, 8, 15, 0, 0, 0, 0, time.UTC) // If logged in, fetch jam project
jamEndTime := time.Date(2022, 8, 22, 0, 0, 0, 0, time.UTC) // Link to project page if found, otherwise link to project creation page with ?jam=1
daysUntilStart := daysUntil(jamStartTime)
daysUntilEnd := daysUntil(jamEndTime)
baseData := getBaseDataAutocrumb(c, "Wheel Reinvention Jam 2022") daysUntilStart := daysUntil(hmndata.WRJ2022.StartTime)
daysUntilEnd := daysUntil(hmndata.WRJ2022.EndTime)
baseData := getBaseDataAutocrumb(c, hmndata.WRJ2022.Name)
baseData.OpenGraphItems = []templates.OpenGraphItem{ baseData.OpenGraphItems = []templates.OpenGraphItem{
{Property: "og:site_name", Value: "Handmade.Network"}, {Property: "og:site_name", Value: "Handmade.Network"},
{Property: "og:type", Value: "website"}, {Property: "og:type", Value: "website"},
@ -41,11 +42,18 @@ func JamIndex2022(c *RequestContext) ResponseData {
return res return res
} }
func JamFeed2022(c *RequestContext) ResponseData {
// List newly-created jam projects
// list snippets from jam projects
// list forum posts from jam project threads
// timeline everything
return FourOhFour(c)
}
func JamIndex2021(c *RequestContext) ResponseData { func JamIndex2021(c *RequestContext) ResponseData {
var res ResponseData var res ResponseData
jamStartTime := time.Date(2021, 9, 27, 0, 0, 0, 0, time.UTC) daysUntilJam := daysUntil(hmndata.WRJ2021.StartTime)
daysUntilJam := daysUntil(jamStartTime)
if daysUntilJam < 0 { if daysUntilJam < 0 {
daysUntilJam = 0 daysUntilJam = 0
} }
@ -79,7 +87,7 @@ func JamIndex2021(c *RequestContext) ResponseData {
showcaseJson := templates.TimelineItemsToJSON(showcaseItems) showcaseJson := templates.TimelineItemsToJSON(showcaseItems)
c.Perf.EndBlock() c.Perf.EndBlock()
baseData := getBaseDataAutocrumb(c, "Wheel Reinvention Jam") baseData := getBaseDataAutocrumb(c, hmndata.WRJ2021.Name)
baseData.OpenGraphItems = []templates.OpenGraphItem{ baseData.OpenGraphItems = []templates.OpenGraphItem{
{Property: "og:site_name", Value: "Handmade.Network"}, {Property: "og:site_name", Value: "Handmade.Network"},
{Property: "og:type", Value: "website"}, {Property: "og:type", Value: "website"},

View File

@ -378,6 +378,21 @@ func ProjectNew(c *RequestContext) ResponseData {
var project templates.ProjectSettings var project templates.ProjectSettings
project.Owners = append(project.Owners, templates.UserToTemplate(c.CurrentUser, c.Theme)) project.Owners = append(project.Owners, templates.UserToTemplate(c.CurrentUser, c.Theme))
project.Personal = true project.Personal = true
var currentJam *hmndata.Jam
if c.Req.URL.Query().Has("jam") {
currentJam = hmndata.CurrentJam()
if currentJam != nil {
project.JamParticipation = []templates.ProjectJamParticipation{
templates.ProjectJamParticipation{
JamName: currentJam.Name,
JamSlug: currentJam.Slug,
Participating: true,
},
}
}
}
var res ResponseData var res ResponseData
res.MustWriteTemplate("project_edit.html", ProjectEditData{ res.MustWriteTemplate("project_edit.html", ProjectEditData{
BaseData: getBaseDataAutocrumb(c, "New Project"), BaseData: getBaseDataAutocrumb(c, "New Project"),
@ -489,6 +504,13 @@ func ProjectEdit(c *RequestContext) ResponseData {
} }
c.Perf.EndBlock() c.Perf.EndBlock()
c.Perf.StartBlock("SQL", "Fetching project jams")
projectJams, err := hmndata.FetchJamsForProject(c.Context(), c.Conn, c.CurrentUser, p.Project.ID)
if err != nil {
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch jams for project"))
}
c.Perf.EndBlock()
lightLogoUrl := templates.ProjectLogoUrl(&p.Project, p.LogoLightAsset, p.LogoDarkAsset, "light") lightLogoUrl := templates.ProjectLogoUrl(&p.Project, p.LogoLightAsset, p.LogoDarkAsset, "light")
darkLogoUrl := templates.ProjectLogoUrl(&p.Project, p.LogoLightAsset, p.LogoDarkAsset, "dark") darkLogoUrl := templates.ProjectLogoUrl(&p.Project, p.LogoLightAsset, p.LogoDarkAsset, "dark")
@ -502,6 +524,15 @@ func ProjectEdit(c *RequestContext) ResponseData {
projectSettings.LinksText = LinksToText(projectLinks) projectSettings.LinksText = LinksToText(projectLinks)
projectSettings.JamParticipation = make([]templates.ProjectJamParticipation, 0, len(projectJams))
for _, jam := range projectJams {
projectSettings.JamParticipation = append(projectSettings.JamParticipation, templates.ProjectJamParticipation{
JamName: jam.JamName,
JamSlug: jam.JamSlug,
Participating: jam.Participating,
})
}
var res ResponseData var res ResponseData
res.MustWriteTemplate("project_edit.html", ProjectEditData{ res.MustWriteTemplate("project_edit.html", ProjectEditData{
BaseData: getBaseDataAutocrumb(c, "Edit Project"), BaseData: getBaseDataAutocrumb(c, "Edit Project"),
@ -553,18 +584,19 @@ func ProjectEditSubmit(c *RequestContext) ResponseData {
} }
type ProjectPayload struct { type ProjectPayload struct {
ProjectID int ProjectID int
Name string Name string
Blurb string Blurb string
Links []ParsedLink Links []ParsedLink
Description string Description string
ParsedDescription string ParsedDescription string
Lifecycle models.ProjectLifecycle Lifecycle models.ProjectLifecycle
Hidden bool Hidden bool
OwnerUsernames []string OwnerUsernames []string
LightLogo FormImage LightLogo FormImage
DarkLogo FormImage DarkLogo FormImage
Tag string Tag string
JamParticipationSlugs []string
Slug string Slug string
Featured bool Featured bool
@ -647,21 +679,24 @@ func ParseProjectEditForm(c *RequestContext) ProjectEditFormResult {
return res return res
} }
jamParticipationSlugs := c.Req.Form["jam_participation"]
res.Payload = ProjectPayload{ res.Payload = ProjectPayload{
Name: projectName, Name: projectName,
Blurb: shortDesc, Blurb: shortDesc,
Links: links, Links: links,
Description: description, Description: description,
ParsedDescription: parsedDescription, ParsedDescription: parsedDescription,
Lifecycle: lifecycle, Lifecycle: lifecycle,
Hidden: hidden, Hidden: hidden,
OwnerUsernames: owners, OwnerUsernames: owners,
LightLogo: lightLogo, LightLogo: lightLogo,
DarkLogo: darkLogo, DarkLogo: darkLogo,
Tag: tag, Tag: tag,
Slug: slug, JamParticipationSlugs: jamParticipationSlugs,
Personal: !official, Slug: slug,
Featured: featured, Personal: !official,
Featured: featured,
} }
return res return res
@ -861,6 +896,70 @@ func updateProject(ctx context.Context, tx pgx.Tx, user *models.User, payload *P
twitch.UserOrProjectLinksUpdated(twitchLoginsPreChange, twitchLoginsPostChange) twitch.UserOrProjectLinksUpdated(twitchLoginsPreChange, twitchLoginsPostChange)
} }
// NOTE(asaf): Regular users can only edit the jam participation status of the current jam or
// jams the project was previously a part of.
var possibleJamSlugs []string
if user.IsStaff {
possibleJamSlugs = make([]string, 0, len(hmndata.AllJams))
for _, jam := range hmndata.AllJams {
possibleJamSlugs = append(possibleJamSlugs, jam.Slug)
}
} else {
possibleJamSlugs, err = db.QueryScalar[string](ctx, tx,
`
SELECT jam_slug
FROM jam_project
WHERE project_id = $1
`,
payload.ProjectID,
)
if err != nil {
return oops.New(err, "Failed to fetch jam participation for project")
}
currentJam := hmndata.CurrentJam()
if currentJam != nil {
possibleJamSlugs = append(possibleJamSlugs, currentJam.Slug)
}
}
_, err = tx.Exec(ctx,
`
UPDATE jam_project
SET participating = FALSE
WHERE project_id = $1
`,
payload.ProjectID,
)
if err != nil {
return oops.New(err, "Failed to remove jam participation for project")
}
for _, jamSlug := range payload.JamParticipationSlugs {
found := false
for _, possibleSlug := range possibleJamSlugs {
if possibleSlug == jamSlug {
found = true
break
}
}
if found {
_, err = tx.Exec(ctx,
`
INSERT INTO jam_project (project_id, jam_slug, participating)
VALUES ($1, $2, $3)
ON CONFLICT (project_id, jam_slug) DO UPDATE SET
participating = EXCLUDED.participating
`,
payload.ProjectID,
jamSlug,
true,
)
if err != nil {
return oops.New(err, "Failed to insert/update jam participation for project")
}
}
}
return nil return nil
} }

View File

@ -168,6 +168,8 @@ func NewWebsiteRoutes(longRequestContext context.Context, conn *pgxpool.Pool) ht
hmnOnly.GET(hmnurl.RegexWhenIsIt, WhenIsIt) hmnOnly.GET(hmnurl.RegexWhenIsIt, WhenIsIt)
hmnOnly.GET(hmnurl.RegexJamIndex, JamIndex2022) hmnOnly.GET(hmnurl.RegexJamIndex, JamIndex2022)
hmnOnly.GET(hmnurl.RegexJamIndex2021, JamIndex2021) hmnOnly.GET(hmnurl.RegexJamIndex2021, JamIndex2021)
hmnOnly.GET(hmnurl.RegexJamIndex2022, JamIndex2022)
hmnOnly.GET(hmnurl.RegexJamFeed2022, JamFeed2022)
hmnOnly.GET(hmnurl.RegexOldHome, Index) hmnOnly.GET(hmnurl.RegexOldHome, Index)