From 33352e13b758838183d8e4c69b182303483cb781 Mon Sep 17 00:00:00 2001 From: Ben Visness Date: Wed, 22 Jun 2022 23:09:20 -0500 Subject: [PATCH] Initial scaffolding for education --- src/hmnurl/hmnurl_test.go | 13 ++++ src/hmnurl/urls.go | 29 ++++++++ ...022-06-23T032752Z_AddEducationResources.go | 71 +++++++++++++++++++ src/models/education.go | 32 +++++++++ src/templates/src/education_article.html | 5 ++ src/templates/src/education_glossary.html | 5 ++ src/templates/src/education_index.html | 5 ++ src/website/education.go | 45 ++++++++++++ src/website/routes.go | 4 ++ 9 files changed, 209 insertions(+) create mode 100644 src/migration/migrations/2022-06-23T032752Z_AddEducationResources.go create mode 100644 src/models/education.go create mode 100644 src/templates/src/education_article.html create mode 100644 src/templates/src/education_glossary.html create mode 100644 src/templates/src/education_index.html create mode 100644 src/website/education.go diff --git a/src/hmnurl/hmnurl_test.go b/src/hmnurl/hmnurl_test.go index a295743..3b3cfeb 100644 --- a/src/hmnurl/hmnurl_test.go +++ b/src/hmnurl/hmnurl_test.go @@ -185,6 +185,19 @@ func TestFishbowl(t *testing.T) { AssertRegexNoMatch(t, BuildFishbowl("oop")+"/otherfiles/whatever", RegexFishbowl) } +func TestEducationIndex(t *testing.T) { + AssertRegexMatch(t, BuildEducationIndex(), RegexEducationIndex, nil) +} + +func TestEducationGlossary(t *testing.T) { + AssertRegexMatch(t, BuildEducationGlossary(""), RegexEducationGlossary, map[string]string{"slug": ""}) + AssertRegexMatch(t, BuildEducationGlossary("foo"), RegexEducationGlossary, map[string]string{"slug": "foo"}) +} + +func TestEducationArticle(t *testing.T) { + AssertRegexMatch(t, BuildEducationArticle("foo"), RegexEducationArticle, map[string]string{"slug": "foo"}) +} + func TestForum(t *testing.T) { AssertRegexMatch(t, hmn.BuildForum(nil, 1), RegexForum, nil) AssertRegexMatch(t, hmn.BuildForum([]string{"wip"}, 2), RegexForum, map[string]string{"subforums": "wip", "page": "2"}) diff --git a/src/hmnurl/urls.go b/src/hmnurl/urls.go index ff76258..b1221b2 100644 --- a/src/hmnurl/urls.go +++ b/src/hmnurl/urls.go @@ -434,6 +434,35 @@ func BuildFishbowl(slug string) string { var RegexFishbowlFiles = regexp.MustCompile(`^/fishbowl/(?P[^/]+)(?P/.+)$`) +/* + * Education + */ + +var RegexEducationIndex = regexp.MustCompile(`^/education$`) + +func BuildEducationIndex() string { + defer CatchPanic() + return Url("/education", nil) +} + +var RegexEducationGlossary = regexp.MustCompile(`^/education/glossary(/(?P[^/]+))?$`) + +func BuildEducationGlossary(termSlug string) string { + defer CatchPanic() + + if termSlug == "" { + return Url("/education/glossary", nil) + } else { + return Url(fmt.Sprintf("/education/glossary/%s", termSlug), nil) + } +} + +var RegexEducationArticle = regexp.MustCompile(`^/education/(?P[^/]+)$`) + +func BuildEducationArticle(slug string) string { + return Url(fmt.Sprintf("/education/%s", slug), nil) +} + /* * Forums */ diff --git a/src/migration/migrations/2022-06-23T032752Z_AddEducationResources.go b/src/migration/migrations/2022-06-23T032752Z_AddEducationResources.go new file mode 100644 index 0000000..7e7746d --- /dev/null +++ b/src/migration/migrations/2022-06-23T032752Z_AddEducationResources.go @@ -0,0 +1,71 @@ +package migrations + +import ( + "context" + "time" + + "git.handmade.network/hmn/hmn/src/migration/types" + "git.handmade.network/hmn/hmn/src/oops" + "github.com/jackc/pgx/v4" +) + +func init() { + registerMigration(AddEducationResources{}) +} + +type AddEducationResources struct{} + +func (m AddEducationResources) Version() types.MigrationVersion { + return types.MigrationVersion(time.Date(2022, 6, 23, 3, 27, 52, 0, time.UTC)) +} + +func (m AddEducationResources) Name() string { + return "AddEducationResources" +} + +func (m AddEducationResources) Description() string { + return "Adds the tables needed for the 2022 education initiative" +} + +func (m AddEducationResources) Up(ctx context.Context, tx pgx.Tx) error { + _, err := tx.Exec(ctx, + ` + CREATE TABLE education_article_version ( + id SERIAL NOT NULL PRIMARY KEY + ); + + CREATE TABLE education_article ( + id SERIAL NOT NULL PRIMARY KEY, + title VARCHAR(255) NOT NULL, + slug VARCHAR(255) NOT NULL, + description TEXT NOT NULL, + type INT NOT NULL, + current_version INT NOT NULL REFERENCES education_article_version (id) + ); + + ALTER TABLE education_article_version + ADD article_id INT NOT NULL REFERENCES education_article (id) ON DELETE CASCADE, + ADD date TIMESTAMP WITH TIME ZONE NOT NULL, + ADD content_raw TEXT NOT NULL, + ADD content_html TEXT NOT NULL, + ADD editor_id INT REFERENCES hmn_user (id) ON DELETE SET NULL; + `, + ) + if err != nil { + return oops.New(err, "failed to create education tables") + } + + return nil +} + +func (m AddEducationResources) Down(ctx context.Context, tx pgx.Tx) error { + _, err := tx.Exec(ctx, ` + DROP TABLE education_article CASCADE; + DROP TABLE education_article_version CASCADE; + `) + if err != nil { + return oops.New(err, "failed to delete education tables") + } + + return nil +} diff --git a/src/models/education.go b/src/models/education.go new file mode 100644 index 0000000..d44292e --- /dev/null +++ b/src/models/education.go @@ -0,0 +1,32 @@ +package models + +import "time" + +type EducationArticle struct { + ID int `db:"id"` + + Title string `db:"title"` + Slug string `db:"slug"` + Description string `db:"description"` + + Type EducationArticleType `db:"type"` + + CurrentVersion int `db:"current_version"` +} + +type EducationArticleType int + +const ( + EducationArticleTypeArticle EducationArticleType = iota + 1 + EducationArticleTypeGlossary +) + +type EducationArticleVersion struct { + ID int `db:"id"` + ArticleID int `db:"article_id"` + Date time.Time `db:"date"` + EditorID *int `db:"editor_id"` + + ContentRaw string `db:"content_raw"` + ContentHTML string `db:"content_html"` +} diff --git a/src/templates/src/education_article.html b/src/templates/src/education_article.html new file mode 100644 index 0000000..e43f716 --- /dev/null +++ b/src/templates/src/education_article.html @@ -0,0 +1,5 @@ +{{ template "base.html" . }} + +{{ define "content" }} + O BOY +{{ end }} diff --git a/src/templates/src/education_glossary.html b/src/templates/src/education_glossary.html new file mode 100644 index 0000000..a76feeb --- /dev/null +++ b/src/templates/src/education_glossary.html @@ -0,0 +1,5 @@ +{{ template "base.html" . }} + +{{ define "content" }} + O YES +{{ end }} diff --git a/src/templates/src/education_index.html b/src/templates/src/education_index.html new file mode 100644 index 0000000..9503ae8 --- /dev/null +++ b/src/templates/src/education_index.html @@ -0,0 +1,5 @@ +{{ template "base.html" . }} + +{{ define "content" }} + O NO +{{ end }} diff --git a/src/website/education.go b/src/website/education.go new file mode 100644 index 0000000..7e6ae55 --- /dev/null +++ b/src/website/education.go @@ -0,0 +1,45 @@ +package website + +import "git.handmade.network/hmn/hmn/src/templates" + +func EducationIndex(c *RequestContext) ResponseData { + type indexData struct { + templates.BaseData + } + + tmpl := indexData{ + BaseData: getBaseData(c, "Handmade Education", nil), + } + + var res ResponseData + res.MustWriteTemplate("education_index.html", tmpl, c.Perf) + return res +} + +func EducationGlossary(c *RequestContext) ResponseData { + type glossaryData struct { + templates.BaseData + } + + tmpl := glossaryData{ + BaseData: getBaseData(c, "Handmade Education", nil), + } + + var res ResponseData + res.MustWriteTemplate("education_glossary.html", tmpl, c.Perf) + return res +} + +func EducationArticle(c *RequestContext) ResponseData { + type articleData struct { + templates.BaseData + } + + tmpl := articleData{ + BaseData: getBaseData(c, "Handmade Education", nil), + } + + var res ResponseData + res.MustWriteTemplate("education_article.html", tmpl, c.Perf) + return res +} diff --git a/src/website/routes.go b/src/website/routes.go index 4693160..fe286b1 100644 --- a/src/website/routes.go +++ b/src/website/routes.go @@ -117,6 +117,10 @@ func NewWebsiteRoutes(conn *pgxpool.Pool) http.Handler { hmnOnly.GET(hmnurl.RegexFishbowlIndex, FishbowlIndex) hmnOnly.GET(hmnurl.RegexFishbowl, Fishbowl) + hmnOnly.GET(hmnurl.RegexEducationIndex, EducationIndex) + hmnOnly.GET(hmnurl.RegexEducationGlossary, EducationGlossary) // Must be above article so `/glossary` does not match as an article slug + hmnOnly.GET(hmnurl.RegexEducationArticle, EducationArticle) + hmnOnly.POST(hmnurl.RegexAPICheckUsername, csrfMiddleware(APICheckUsername)) hmnOnly.GET(hmnurl.RegexLibraryAny, LibraryNotPortedYet)