From 9d1d249ec058e77a45d0c68633c752cfb889ade6 Mon Sep 17 00:00:00 2001 From: Asaf Gartner Date: Mon, 20 Jun 2022 01:26:33 +0300 Subject: [PATCH] Added jam/project association. --- src/hmndata/jams.go | 116 ++++++++++++++ src/hmnurl/urls.go | 21 ++- .../2022-06-18T010339Z_AddJamProjects.go | 54 +++++++ src/models/jamproject.go | 11 ++ src/templates/src/project_edit.html | 16 +- src/templates/types.go | 21 ++- src/website/jam.go | 24 ++- src/website/projects.go | 151 +++++++++++++++--- src/website/routes.go | 2 + 9 files changed, 373 insertions(+), 43 deletions(-) create mode 100644 src/hmndata/jams.go create mode 100644 src/migration/migrations/2022-06-18T010339Z_AddJamProjects.go create mode 100644 src/models/jamproject.go diff --git a/src/hmndata/jams.go b/src/hmndata/jams.go new file mode 100644 index 0000000..107fb6d --- /dev/null +++ b/src/hmndata/jams.go @@ -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 +} diff --git a/src/hmnurl/urls.go b/src/hmnurl/urls.go index 74775d2..9212477 100644 --- a/src/hmnurl/urls.go +++ b/src/hmnurl/urls.go @@ -56,7 +56,26 @@ func BuildJamIndex() string { 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? diff --git a/src/migration/migrations/2022-06-18T010339Z_AddJamProjects.go b/src/migration/migrations/2022-06-18T010339Z_AddJamProjects.go new file mode 100644 index 0000000..93ca060 --- /dev/null +++ b/src/migration/migrations/2022-06-18T010339Z_AddJamProjects.go @@ -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 +} diff --git a/src/models/jamproject.go b/src/models/jamproject.go new file mode 100644 index 0000000..d89403e --- /dev/null +++ b/src/models/jamproject.go @@ -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 +} diff --git a/src/templates/src/project_edit.html b/src/templates/src/project_edit.html index 3fb5d1c..1df121b 100644 --- a/src/templates/src/project_edit.html +++ b/src/templates/src/project_edit.html @@ -20,7 +20,7 @@ {{ if .Editing }}

Edit {{ .ProjectSettings.Name }}

{{ else }} -

Create a new project

+

Create a new {{ if .ProjectSettings.JamParticipation }}jam {{ end }}project

{{ end }}
{{ csrftoken .Session }} @@ -81,6 +81,20 @@
If you have linked your Discord account, any #project-showcase messages with the tag "&" will automatically be associated with this project.
+ {{ if .ProjectSettings.JamParticipation }} +
+
Jam Participation
+
+ {{ range .ProjectSettings.JamParticipation }} +
+
{{ .JamName }}:
+
+ + +
+
+ {{ end }} + {{ end }} {{ if and .Editing .User.IsStaff }}
Admin settings
diff --git a/src/templates/types.go b/src/templates/types.go index 33e9c6d..82adf4f 100644 --- a/src/templates/types.go +++ b/src/templates/types.go @@ -138,13 +138,14 @@ type Project struct { } type ProjectSettings struct { - Name string - Slug string - Hidden bool - Featured bool - Personal bool - Lifecycle string - Tag string + Name string + Slug string + Hidden bool + Featured bool + Personal bool + Lifecycle string + Tag string + JamParticipation []ProjectJamParticipation Blurb string Description string @@ -155,6 +156,12 @@ type ProjectSettings struct { DarkLogo string } +type ProjectJamParticipation struct { + JamName string + JamSlug string + Participating bool +} + type User struct { ID int Username string diff --git a/src/website/jam.go b/src/website/jam.go index 89a8fbc..17fbff1 100644 --- a/src/website/jam.go +++ b/src/website/jam.go @@ -14,12 +14,13 @@ import ( func JamIndex2022(c *RequestContext) ResponseData { var res ResponseData - jamStartTime := time.Date(2022, 8, 15, 0, 0, 0, 0, time.UTC) - jamEndTime := time.Date(2022, 8, 22, 0, 0, 0, 0, time.UTC) - daysUntilStart := daysUntil(jamStartTime) - daysUntilEnd := daysUntil(jamEndTime) + // If logged in, fetch jam project + // Link to project page if found, otherwise link to project creation page with ?jam=1 - 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{ {Property: "og:site_name", Value: "Handmade.Network"}, {Property: "og:type", Value: "website"}, @@ -41,11 +42,18 @@ func JamIndex2022(c *RequestContext) ResponseData { 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 { var res ResponseData - jamStartTime := time.Date(2021, 9, 27, 0, 0, 0, 0, time.UTC) - daysUntilJam := daysUntil(jamStartTime) + daysUntilJam := daysUntil(hmndata.WRJ2021.StartTime) if daysUntilJam < 0 { daysUntilJam = 0 } @@ -79,7 +87,7 @@ func JamIndex2021(c *RequestContext) ResponseData { showcaseJson := templates.TimelineItemsToJSON(showcaseItems) c.Perf.EndBlock() - baseData := getBaseDataAutocrumb(c, "Wheel Reinvention Jam") + baseData := getBaseDataAutocrumb(c, hmndata.WRJ2021.Name) baseData.OpenGraphItems = []templates.OpenGraphItem{ {Property: "og:site_name", Value: "Handmade.Network"}, {Property: "og:type", Value: "website"}, diff --git a/src/website/projects.go b/src/website/projects.go index 878d92a..95e04bb 100644 --- a/src/website/projects.go +++ b/src/website/projects.go @@ -378,6 +378,21 @@ func ProjectNew(c *RequestContext) ResponseData { var project templates.ProjectSettings project.Owners = append(project.Owners, templates.UserToTemplate(c.CurrentUser, c.Theme)) 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 res.MustWriteTemplate("project_edit.html", ProjectEditData{ BaseData: getBaseDataAutocrumb(c, "New Project"), @@ -489,6 +504,13 @@ func ProjectEdit(c *RequestContext) ResponseData { } 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") darkLogoUrl := templates.ProjectLogoUrl(&p.Project, p.LogoLightAsset, p.LogoDarkAsset, "dark") @@ -502,6 +524,15 @@ func ProjectEdit(c *RequestContext) ResponseData { 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 res.MustWriteTemplate("project_edit.html", ProjectEditData{ BaseData: getBaseDataAutocrumb(c, "Edit Project"), @@ -553,18 +584,19 @@ func ProjectEditSubmit(c *RequestContext) ResponseData { } type ProjectPayload struct { - ProjectID int - Name string - Blurb string - Links []ParsedLink - Description string - ParsedDescription string - Lifecycle models.ProjectLifecycle - Hidden bool - OwnerUsernames []string - LightLogo FormImage - DarkLogo FormImage - Tag string + ProjectID int + Name string + Blurb string + Links []ParsedLink + Description string + ParsedDescription string + Lifecycle models.ProjectLifecycle + Hidden bool + OwnerUsernames []string + LightLogo FormImage + DarkLogo FormImage + Tag string + JamParticipationSlugs []string Slug string Featured bool @@ -647,21 +679,24 @@ func ParseProjectEditForm(c *RequestContext) ProjectEditFormResult { return res } + jamParticipationSlugs := c.Req.Form["jam_participation"] + res.Payload = ProjectPayload{ - Name: projectName, - Blurb: shortDesc, - Links: links, - Description: description, - ParsedDescription: parsedDescription, - Lifecycle: lifecycle, - Hidden: hidden, - OwnerUsernames: owners, - LightLogo: lightLogo, - DarkLogo: darkLogo, - Tag: tag, - Slug: slug, - Personal: !official, - Featured: featured, + Name: projectName, + Blurb: shortDesc, + Links: links, + Description: description, + ParsedDescription: parsedDescription, + Lifecycle: lifecycle, + Hidden: hidden, + OwnerUsernames: owners, + LightLogo: lightLogo, + DarkLogo: darkLogo, + Tag: tag, + JamParticipationSlugs: jamParticipationSlugs, + Slug: slug, + Personal: !official, + Featured: featured, } return res @@ -861,6 +896,70 @@ func updateProject(ctx context.Context, tx pgx.Tx, user *models.User, payload *P 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 } diff --git a/src/website/routes.go b/src/website/routes.go index 88393b0..abc34e0 100644 --- a/src/website/routes.go +++ b/src/website/routes.go @@ -168,6 +168,8 @@ func NewWebsiteRoutes(longRequestContext context.Context, conn *pgxpool.Pool) ht hmnOnly.GET(hmnurl.RegexWhenIsIt, WhenIsIt) hmnOnly.GET(hmnurl.RegexJamIndex, JamIndex2022) hmnOnly.GET(hmnurl.RegexJamIndex2021, JamIndex2021) + hmnOnly.GET(hmnurl.RegexJamIndex2022, JamIndex2022) + hmnOnly.GET(hmnurl.RegexJamFeed2022, JamFeed2022) hmnOnly.GET(hmnurl.RegexOldHome, Index)