diff --git a/resetdb.sh b/resetdb.sh new file mode 100644 index 0000000..e628e08 --- /dev/null +++ b/resetdb.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +THIS_PATH=$(pwd) +BETA_PATH='/mnt/c/Users/bvisn/Developer/handmade/handmade-beta' + +cd $BETA_PATH +docker-compose down -v +docker-compose up -d postgres +sleep 3 +./scripts/db_import -d -n hmn_two -c + +cd $THIS_PATH +go run src/main.go migrate 2021-03-10T05:16:21Z + +cd $BETA_PATH +./scripts/db_import -d -n hmn_two -a ./dbdumps/hmn_pg_dump_2020-11-10 diff --git a/src/db/db.go b/src/db/db.go index 0f3404e..3407b1f 100644 --- a/src/db/db.go +++ b/src/db/db.go @@ -5,7 +5,6 @@ import ( "errors" "reflect" "strings" - "time" "git.handmade.network/hmn/hmn/src/config" "git.handmade.network/hmn/hmn/src/oops" @@ -94,6 +93,10 @@ func (it *StructQueryIterator) ToSlice() []interface{} { for { row, ok := it.Next() if !ok { + err := it.rows.Err() + if err != nil { + panic(oops.New(err, "error while iterating through db results")) + } break } result = append(result, row) @@ -119,14 +122,11 @@ func followPathThroughStructs(structVal reflect.Value, path []int) reflect.Value return val } -func Query(ctx context.Context, conn *pgxpool.Pool, destExample interface{}, query string, args ...interface{}) (StructQueryIterator, error) { - ctx, cancel := context.WithTimeout(ctx, 5*time.Second) - defer cancel() - +func Query(ctx context.Context, conn *pgxpool.Pool, destExample interface{}, query string, args ...interface{}) (*StructQueryIterator, error) { destType := reflect.TypeOf(destExample) columnNames, fieldPaths, err := getColumnNamesAndPaths(destType, nil, "") if err != nil { - return StructQueryIterator{}, oops.New(err, "failed to generate column names") + return nil, oops.New(err, "failed to generate column names") } columnNamesString := strings.Join(columnNames, ", ") @@ -137,10 +137,10 @@ func Query(ctx context.Context, conn *pgxpool.Pool, destExample interface{}, que if errors.Is(err, context.DeadlineExceeded) { panic("query exceeded its deadline") } - return StructQueryIterator{}, err + return nil, err } - return StructQueryIterator{ + return &StructQueryIterator{ fieldPaths: fieldPaths, rows: rows, destType: destType, diff --git a/src/hmnurl/hmnurl.go b/src/hmnurl/hmnurl.go index d2be1e7..166c268 100644 --- a/src/hmnurl/hmnurl.go +++ b/src/hmnurl/hmnurl.go @@ -4,6 +4,7 @@ import ( "net/url" "git.handmade.network/hmn/hmn/src/config" + "git.handmade.network/hmn/hmn/src/oops" ) const StaticPath = "/public" @@ -14,12 +15,35 @@ type Q struct { Value string } -func Url(path string, query []Q) string { - result := config.Config.BaseUrl + "/" + trim(path) - if q := encodeQuery(query); q != "" { - result += "?" + q +var baseUrlParsed url.URL + +func init() { + parsed, err := url.Parse(config.Config.BaseUrl) + if err != nil { + panic(oops.New(err, "could not parse base URL")) } - return result + + baseUrlParsed = *parsed +} + +func Url(path string, query []Q) string { + return ProjectUrl(path, query, "") +} + +func ProjectUrl(path string, query []Q, subdomain string) string { + host := baseUrlParsed.Host + if len(subdomain) > 0 { + host = subdomain + "." + host + } + + url := url.URL{ + Scheme: baseUrlParsed.Scheme, + Host: host, + Path: trim(path), + RawQuery: encodeQuery(query), + } + + return url.String() } func StaticUrl(path string, query []Q) string { diff --git a/src/migration/migration.go b/src/migration/migration.go index 252273c..af1af98 100644 --- a/src/migration/migration.go +++ b/src/migration/migration.go @@ -193,8 +193,8 @@ func Migrate(targetVersion types.MigrationVersion) { // roll forward for i := currentIndex + 1; i <= targetIndex; i++ { version := allVersions[i] - fmt.Printf("Applying migration %v\n", version) migration := migrations.All[version] + fmt.Printf("Applying migration %v (%v)\n", version, migration.Name()) tx, err := conn.Begin(context.Background()) if err != nil { diff --git a/src/migration/migrations/2021-04-09T000000Z_DeleteOrphanedLinks.go b/src/migration/migrations/2021-04-09T000000Z_DeleteOrphanedLinks.go new file mode 100644 index 0000000..ec58bd4 --- /dev/null +++ b/src/migration/migrations/2021-04-09T000000Z_DeleteOrphanedLinks.go @@ -0,0 +1,55 @@ +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(DeleteOrphanedLinks{}) +} + +type DeleteOrphanedLinks struct{} + +func (m DeleteOrphanedLinks) Version() types.MigrationVersion { + return types.MigrationVersion(time.Date(2021, 4, 9, 0, 0, 0, 0, time.UTC)) +} + +func (m DeleteOrphanedLinks) Name() string { + return "DeleteOrphanedLinks" +} + +func (m DeleteOrphanedLinks) Description() string { + return "Delete links with no member or project" +} + +func (m DeleteOrphanedLinks) Up(tx pgx.Tx) error { + // Delete orphaned links (no member or project) + _, err := tx.Exec(context.Background(), ` + DELETE FROM handmade_links + WHERE + id IN ( + SELECT links.id + FROM + handmade_links AS links + LEFT JOIN handmade_memberextended_links AS mlinks ON mlinks.links_id = links.id + LEFT JOIN handmade_project_links AS plinks ON plinks.links_id = links.id + WHERE + mlinks.id IS NULL + AND plinks.id IS NULL + ) + `) + if err != nil { + return oops.New(err, "failed to delete orphaned links") + } + + return nil +} + +func (m DeleteOrphanedLinks) Down(tx pgx.Tx) error { + panic("Implement me") +} diff --git a/src/migration/migrations/2021-04-10T013044Z_CopyMemberExtendedData.go b/src/migration/migrations/2021-04-10T013044Z_CopyMemberExtendedData.go new file mode 100644 index 0000000..45b5f23 --- /dev/null +++ b/src/migration/migrations/2021-04-10T013044Z_CopyMemberExtendedData.go @@ -0,0 +1,107 @@ +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(CopyMemberExtendedData{}) +} + +type CopyMemberExtendedData struct{} + +func (m CopyMemberExtendedData) Version() types.MigrationVersion { + return types.MigrationVersion(time.Date(2021, 4, 10, 1, 30, 44, 0, time.UTC)) +} + +func (m CopyMemberExtendedData) Name() string { + return "CopyMemberExtendedData" +} + +func (m CopyMemberExtendedData) Description() string { + return "Copy MemberExtended data into Member" +} + +func (m CopyMemberExtendedData) Up(tx pgx.Tx) error { + // Add columns to member table + _, err := tx.Exec(context.Background(), ` + ALTER TABLE handmade_member + ADD COLUMN bio TEXT NOT NULL DEFAULT '', + ADD COLUMN showemail BOOLEAN NOT NULL DEFAULT FALSE + `) + if err != nil { + return oops.New(err, "failed to add columns to member table") + } + + // Copy data to members from memberextended + _, err = tx.Exec(context.Background(), ` + UPDATE handmade_member + SET (bio, showemail) = ( + SELECT COALESCE(bio, ''), showemail + FROM handmade_memberextended + WHERE handmade_memberextended.id = handmade_member.extended_id + ); + `) + if err != nil { + return oops.New(err, "failed to copy data from the memberextended table") + } + + // Directly associate links with members + _, err = tx.Exec(context.Background(), ` + ALTER TABLE handmade_links + ADD COLUMN member_id INTEGER REFERENCES handmade_member, + ADD COLUMN project_id INTEGER REFERENCES handmade_project, + ADD CONSTRAINT exactly_one_foreign_key CHECK ( + ( + CASE WHEN member_id IS NULL THEN 0 ELSE 1 END + + CASE WHEN project_id IS NULL THEN 0 ELSE 1 END + ) = 1 + ); + + UPDATE handmade_links + SET (member_id) = ( + SELECT mem.user_id + FROM + handmade_memberextended_links AS mlinks + JOIN handmade_memberextended AS memext ON memext.id = mlinks.memberextended_id + JOIN handmade_member AS mem ON mem.extended_id = memext.id + WHERE + mlinks.links_id = handmade_links.id + ); + + UPDATE handmade_links + SET (project_id) = ( + SELECT proj.id + FROM + handmade_project_links AS plinks + JOIN handmade_project AS proj ON proj.id = plinks.project_id + WHERE + plinks.links_id = handmade_links.id + ); + `) + if err != nil { + return oops.New(err, "failed to associate links with members") + } + + return nil +} + +func (m CopyMemberExtendedData) Down(tx pgx.Tx) error { + // _, err := tx.Exec(context.Background(), ` + // ALTER TABLE handmade_member + // DROP COLUMN bio, + // DROP COLUMN showemail + // `) + // if err != nil { + // return oops.New(err, "failed to drop columns from member table") + // } + // + // return nil + + panic("you do not want to do this") +} diff --git a/src/migration/migrations/2021-04-10T015339Z_DropMemberExtendedTable.go b/src/migration/migrations/2021-04-10T015339Z_DropMemberExtendedTable.go new file mode 100644 index 0000000..83ef016 --- /dev/null +++ b/src/migration/migrations/2021-04-10T015339Z_DropMemberExtendedTable.go @@ -0,0 +1,43 @@ +package migrations + +import ( + "context" + "time" + + "git.handmade.network/hmn/hmn/src/migration/types" + "github.com/jackc/pgx/v4" +) + +func init() { + // TODO: Delete this migration + // registerMigration(DropMemberExtendedTable{}) +} + +type DropMemberExtendedTable struct{} + +func (m DropMemberExtendedTable) Version() types.MigrationVersion { + return types.MigrationVersion(time.Date(2021, 4, 10, 1, 53, 39, 0, time.UTC)) +} + +func (m DropMemberExtendedTable) Name() string { + return "DropMemberExtendedTable" +} + +func (m DropMemberExtendedTable) Description() string { + return "Remove the MemberExtended record outright" +} + +func (m DropMemberExtendedTable) Up(tx pgx.Tx) error { + _, err := tx.Exec(context.Background(), ` + ALTER TABLE handmade_member + DROP COLUMN extended_id; + + DROP TABLE handmade_memberextended_links; + DROP TABLE handmade_memberextended; + `) + return err +} + +func (m DropMemberExtendedTable) Down(tx pgx.Tx) error { + panic("you do not want to do this") +} diff --git a/src/migration/migrations/2021-04-11T195747Z_RemoveMemberAndExtended.go b/src/migration/migrations/2021-04-11T195747Z_RemoveMemberAndExtended.go new file mode 100644 index 0000000..928172f --- /dev/null +++ b/src/migration/migrations/2021-04-11T195747Z_RemoveMemberAndExtended.go @@ -0,0 +1,79 @@ +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" +) + +/* +Phase 1. Migrate the schemas for all the tables that will stick around through this +whole process. +*/ + +func init() { + registerMigration(RemoveMemberAndExtended{}) +} + +type RemoveMemberAndExtended struct{} + +func (m RemoveMemberAndExtended) Version() types.MigrationVersion { + return types.MigrationVersion(time.Date(2021, 4, 11, 19, 57, 47, 0, time.UTC)) +} + +func (m RemoveMemberAndExtended) Name() string { + return "RemoveMemberAndExtended" +} + +func (m RemoveMemberAndExtended) Description() string { + return "Remove the member and member extended records, collapsing their data into users" +} + +func (m RemoveMemberAndExtended) Up(tx pgx.Tx) error { + // Creates a column that will eventually be a foreign key to auth_user. + createUserColumn := func(ctx context.Context, tx pgx.Tx, table string, before string, notNull bool) { + nullConstraint := "" + if notNull { + nullConstraint = "NOT NULL" + } + + _, err := tx.Exec(ctx, ` + ALTER TABLE `+table+` + ADD plz_rename INT `+nullConstraint+` DEFAULT 99999; + UPDATE `+table+` SET plz_rename = `+before+`; + `) + if err != nil { + panic(oops.New(err, "failed to update table %s to point at users instead of members", table)) + } + } + + /* + Models referencing handmade_member: + - CommunicationChoice + - CommunicationSubCategory + - CommunicationSubThread + - Discord + - CategoryLastReadInfo + - ThreadLastReadInfo + - PostTextVersion + - Post + - handmade_member_projects + */ + createUserColumn(context.Background(), tx, "handmade_communicationchoice", "member_id", true) + createUserColumn(context.Background(), tx, "handmade_communicationsubcategory", "member_id", true) + createUserColumn(context.Background(), tx, "handmade_communicationsubthread", "member_id", true) + createUserColumn(context.Background(), tx, "handmade_discord", "member_id", true) + createUserColumn(context.Background(), tx, "handmade_categorylastreadinfo", "member_id", true) + createUserColumn(context.Background(), tx, "handmade_threadlastreadinfo", "member_id", true) + createUserColumn(context.Background(), tx, "handmade_posttextversion", "editor_id", false) + createUserColumn(context.Background(), tx, "handmade_post", "author_id", false) + + return nil +} + +func (m RemoveMemberAndExtended) Down(tx pgx.Tx) error { + panic("Implement me") +} diff --git a/src/migration/migrations/2021-04-11T210958Z_RemoveMemberAndExtended2.go b/src/migration/migrations/2021-04-11T210958Z_RemoveMemberAndExtended2.go new file mode 100644 index 0000000..9ee7755 --- /dev/null +++ b/src/migration/migrations/2021-04-11T210958Z_RemoveMemberAndExtended2.go @@ -0,0 +1,65 @@ +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" +) + +/* +Phase 2. Clean up the schema, adding constraints and dropping tables. Do not do any row updates +or deletes, because that causes trigger events, which make me sad. +*/ + +func init() { + registerMigration(RemoveMemberAndExtended2{}) +} + +type RemoveMemberAndExtended2 struct{} + +func (m RemoveMemberAndExtended2) Version() types.MigrationVersion { + return types.MigrationVersion(time.Date(2021, 4, 11, 21, 9, 58, 0, time.UTC)) +} + +func (m RemoveMemberAndExtended2) Name() string { + return "RemoveMemberAndExtended2" +} + +func (m RemoveMemberAndExtended2) Description() string { + return "Phase 2 of the above" +} + +func (m RemoveMemberAndExtended2) Up(tx pgx.Tx) error { + dropOldColumn := func(ctx context.Context, tx pgx.Tx, table string, before, after string, onDelete string) { + _, err := tx.Exec(ctx, ` + ALTER TABLE `+table+` + DROP `+before+`; + ALTER TABLE `+table+` + RENAME plz_rename TO `+after+`; + ALTER TABLE `+table+` + ADD FOREIGN KEY (`+after+`) REFERENCES auth_user ON DELETE `+onDelete+`, + ALTER `+after+` DROP DEFAULT; + `) + if err != nil { + panic(oops.New(err, "failed to update table %s to point at users instead of members", table)) + } + } + + dropOldColumn(context.Background(), tx, "handmade_communicationchoice", "member_id", "user_id", "CASCADE") + dropOldColumn(context.Background(), tx, "handmade_communicationsubcategory", "member_id", "user_id", "CASCADE") + dropOldColumn(context.Background(), tx, "handmade_communicationsubthread", "member_id", "user_id", "CASCADE") + dropOldColumn(context.Background(), tx, "handmade_discord", "member_id", "hmn_user_id", "CASCADE") + dropOldColumn(context.Background(), tx, "handmade_categorylastreadinfo", "member_id", "user_id", "CASCADE") + dropOldColumn(context.Background(), tx, "handmade_threadlastreadinfo", "member_id", "user_id", "CASCADE") + dropOldColumn(context.Background(), tx, "handmade_posttextversion", "editor_id", "editor_id", "SET NULL") + dropOldColumn(context.Background(), tx, "handmade_post", "author_id", "author_id", "SET NULL") + + return nil +} + +func (m RemoveMemberAndExtended2) Down(tx pgx.Tx) error { + panic("Implement me") +} diff --git a/src/models/member.go b/src/models/member.go new file mode 100644 index 0000000..f41cb26 --- /dev/null +++ b/src/models/member.go @@ -0,0 +1,22 @@ +package models + +type Member struct { + UserID int + + Name *string `db:"name"` // TODO: Migrate to not null + Bio *string `db:"bio"` + Blurb *string `db:"blurb"` + Signature *string `db:"signature"` + Avatar *string `db:"avatar"` // TODO: Image field stuff? + + DarkTheme bool `db:"darktheme"` + Timezone string `db:"timezone"` + ProfileColor1 string `db:"color_1"` + ProfileColor2 string `db:"color_2"` + + ShowEmail bool `db:"showemail"` + CanEditLibrary bool `db:"edit_library"` + + DiscordSaveShowcase bool `db:"discord_save_showcase"` + DiscordDeleteSnippetOnMessageDelete bool `db:"discord_delete_snippet_on_message_delete"` +} diff --git a/src/models/project.go b/src/models/project.go index f84c051..f6d4a6f 100644 --- a/src/models/project.go +++ b/src/models/project.go @@ -1,6 +1,9 @@ package models -import "reflect" +import ( + "reflect" + "time" +) const HMNProjectID = 1 @@ -16,6 +19,8 @@ type Project struct { Color1 string `db:"color_1"` Color2 string `db:"color_2"` + + AllLastUpdated time.Time `db:"all_last_updated"` } func (p *Project) IsHMN() bool { diff --git a/src/templates/mapping.go b/src/templates/mapping.go new file mode 100644 index 0000000..978dd0d --- /dev/null +++ b/src/templates/mapping.go @@ -0,0 +1,59 @@ +package templates + +import "git.handmade.network/hmn/hmn/src/models" + +func MemberToTemplate(m *models.Member) Member { + return Member{ + Name: maybeString(m.Name), + Blurb: maybeString(m.Blurb), + Signature: maybeString(m.Signature), + + DarkTheme: m.DarkTheme, + Timezone: m.Timezone, + ProfileColor1: m.ProfileColor1, + ProfileColor2: m.ProfileColor2, + + CanEditLibrary: m.CanEditLibrary, + DiscordSaveShowcase: m.DiscordSaveShowcase, + DiscordDeleteSnippetOnMessageDelete: m.DiscordDeleteSnippetOnMessageDelete, + } +} + +func PostToTemplate(p *models.Post) Post { + return Post{ + Preview: p.Preview, + ReadOnly: p.ReadOnly, + } +} + +func ProjectToTemplate(p *models.Project) Project { + return Project{ + Name: maybeString(p.Name), + Subdomain: maybeString(p.Slug), + Color1: p.Color1, + Color2: p.Color2, + + IsHMN: p.IsHMN(), + + HasBlog: true, // TODO: Check flag sets or whatever + HasForum: true, + HasWiki: true, + HasLibrary: true, + } +} + +func UserToTemplate(u *models.User) User { + return User{ + Username: u.Username, + Email: u.Email, + IsSuperuser: u.IsSuperuser, + IsStaff: u.IsStaff, + } +} + +func maybeString(s *string) string { + if s == nil { + return "" + } + return *s +} diff --git a/src/templates/src/include/header.html b/src/templates/src/include/header.html index 83677be..fb111f5 100644 --- a/src/templates/src/include/header.html +++ b/src/templates/src/include/header.html @@ -9,9 +9,9 @@ Logout {{ else }} Register - Log in + Log in
-
+ {{/* TODO: CSRF */}} @@ -38,24 +38,24 @@
{{ if not .Project.IsHMN }} - {{ end }} {{ if .Project.HasBlog }} - Blog + Blog {{ end }} {{ if .Project.HasForum }} - Forums + Forums {{ end }} {{ if .Project.HasWiki }} - Wiki + Wiki {{ end }} {{ if .Project.HasLibrary }} - Library + Library {{ end }} {{ if .Project.IsHMN }} - Mission + Mission {{ end }} {{/* {% if project.default_annotation_category %} */}} {{ if false }} diff --git a/src/templates/src/index.html b/src/templates/src/index.html index bf5419c..2cb4e0a 100644 --- a/src/templates/src/index.html +++ b/src/templates/src/index.html @@ -1,7 +1,98 @@ {{ template "base.html" . }} {{ define "content" }} - This is the index page. +
+
+
+

Around the Network

+ +
+
+
+ +
+ {{ range $i, $col := .PostColumns }} +
+
+ {{ if eq $i 0 }} +
+ Wow, a featured post! + {{/* {% include "blog_index_thread_list_entry.html" with post=featured_post align_top=True %} */}} +
+ {{ end }} + + {{ range $entry := $col }} + {{ $proj := $entry.Project }} + {{ $posts := $entry.Posts }} +
{{/* TODO: Is this ID used for anything? */}} + +

{{ $proj.Name }}

+
+ + {{/* TODO: What is this? + {% if entry.featured and proj.slug != "hmn" %} + {% with post=entry.featured.0 has_read=entry.featured.1 %} + {% if post.category.kind == 5 and post.parent == None %} + {% include "thread_list_entry.html" with thread=post.thread %} + {% else %} + {% include "blog_index_thread_list_entry.html" with align_top=True %} + {% endif %} + {% endwith %} + {% endif %} + */}} + + {{ range $posts }} +
+ {{ .Post.Preview }} +
+ {{/* + {% if forloop.counter0 < max_posts %} + {% include "thread_list_entry.html" with thread=post.thread %} + {% endif %} + */}} + {{ end }} + {{/* + {% with more=posts|length|add:-5|clamp_lower:0 %} + {% if more > 0 %} + + {% endif %} + {% endwith %} + */}} +
+ {{ end }} +
+
+ {{ end }} +
{{ end }} {{/* diff --git a/src/templates/src/layouts/base.html b/src/templates/src/layouts/base.html index 032433e..149c6a2 100644 --- a/src/templates/src/layouts/base.html +++ b/src/templates/src/layouts/base.html @@ -31,7 +31,7 @@ background-size: "{{ . }}"; {{ end }} {{ else }} - {{ $bgcolor := or .Project.Color "999999" }} + {{ $bgcolor := or .Project.Color1 "999999" }} background-color: #{{ eq .Theme "dark" | ternary (darken $bgcolor 0.6) (brighten $bgcolor 0.6) }}; background-image: url('data:image/png;base64,{{ eq .Theme "dark" | ternary $bgdark $bglight }}'); background-size: auto; @@ -40,7 +40,7 @@ {{ block "extrahead" . }}{{ end }} - + diff --git a/src/templates/templates.go b/src/templates/templates.go index 0088ed2..5017a2c 100644 --- a/src/templates/templates.go +++ b/src/templates/templates.go @@ -69,18 +69,25 @@ var HMNTemplateFuncs = template.FuncMap{ "cachebust": func() string { return cachebust }, + "currentprojecturl": func(url string) string { + return hmnurl.Url(url, nil) // TODO: Use project subdomain + }, + "currentprojecturlq": func(url string, query string) string { + absUrl := hmnurl.Url(url, nil) + return fmt.Sprintf("%s?%s", absUrl, query) // TODO: Use project subdomain + }, "darken": func(hexColor string, amount float64) (string, error) { if len(hexColor) < 6 { return "", fmt.Errorf("couldn't darken invalid hex color: %v", hexColor) } return noire.NewHex(hexColor).Shade(amount).Hex(), nil }, - "projecturl": func(url string) string { - return hmnurl.Url(url, nil) // TODO: Use project subdomain + "projecturl": func(url string, proj interface{}) string { + return hmnurl.ProjectUrl(url, nil, getProjectSubdomain(proj)) }, - "projecturlq": func(url string, query string) string { - absUrl := hmnurl.Url(url, nil) - return fmt.Sprintf("%s?%s", absUrl, query) // TODO: Use project subdomain + "projecturlq": func(url string, proj interface{}, query string) string { + absUrl := hmnurl.ProjectUrl(url, nil, getProjectSubdomain(proj)) + return fmt.Sprintf("%s?%s", absUrl, query) }, "query": func(args ...string) string { query := url.Values{} @@ -117,3 +124,17 @@ type ErrInvalidHexColor struct { func (e ErrInvalidHexColor) Error() string { return fmt.Sprintf("invalid hex color: %s", e.color) } + +func getProjectSubdomain(proj interface{}) string { + subdomain := "" + switch p := proj.(type) { + case Project: + subdomain = p.Subdomain + case int: + // TODO: Look up project from the database + default: + panic(fmt.Errorf("projecturl requires either a templates.Project or a project ID, got %+v", proj)) + } + + return subdomain +} diff --git a/src/templates/types.go b/src/templates/types.go index 1c88c27..5747283 100644 --- a/src/templates/types.go +++ b/src/templates/types.go @@ -12,10 +12,34 @@ type BaseData struct { User *User } +type Member struct { + Name string + Blurb string + Signature string + // Avatar?? + + DarkTheme bool + Timezone string + ProfileColor1 string + ProfileColor2 string + + CanEditLibrary bool + DiscordSaveShowcase bool + DiscordDeleteSnippetOnMessageDelete bool +} + +type Post struct { + Preview string + ReadOnly bool + + IP string +} + type Project struct { Name string Subdomain string - Color string + Color1 string + Color2 string IsHMN bool @@ -42,6 +66,3 @@ type BackgroundImage struct { Url string Size string // A valid CSS background-size value } - -type Post struct { -} diff --git a/src/website/landing.go b/src/website/landing.go index ad2af6d..fdca316 100644 --- a/src/website/landing.go +++ b/src/website/landing.go @@ -33,8 +33,17 @@ func Index(c *RequestContext) ResponseData { const numProjectsToGet = 7 iterProjects, err := db.Query(c.Context(), c.Conn, models.Project{}, - "SELECT $columns FROM handmade_project WHERE flags = 0 OR id = $1", + ` + SELECT $columns + FROM handmade_project + WHERE + flags = 0 + OR id = $1 + ORDER BY all_last_updated DESC + LIMIT $2 + `, models.HMNProjectID, + numProjectsToGet*2, // hedge your bets against projects that don't have any content ) if err != nil { return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to get projects for home page")) @@ -42,9 +51,11 @@ func Index(c *RequestContext) ResponseData { defer iterProjects.Close() var pageProjects []LandingPageProject - _ = pageProjects // TODO: NO - for _, projRow := range iterProjects.ToSlice() { + allProjects := iterProjects.ToSlice() + c.Logger.Info().Interface("allProjects", allProjects).Msg("all the projects") + + for _, projRow := range allProjects { proj := projRow.(*models.Project) type ProjectPost struct { @@ -89,11 +100,7 @@ func Index(c *RequestContext) ResponseData { projectPosts := projectPostIter.ToSlice() landingPageProject := LandingPageProject{ - Project: templates.Project{ // TODO: Use a common function to map from model to template data - Name: *proj.Name, - Subdomain: *proj.Slug, - // ... - }, + Project: templates.ProjectToTemplate(proj), } for _, projectPostRow := range projectPosts { @@ -107,10 +114,18 @@ func Index(c *RequestContext) ResponseData { } landingPageProject.Posts = append(landingPageProject.Posts, LandingPagePost{ - Post: templates.Post{}, // TODO: Use a common function to map from model to template again + Post: templates.PostToTemplate(&projectPost.Post), HasRead: hasRead, }) } + + if len(projectPosts) > 0 { + pageProjects = append(pageProjects, landingPageProject) + } + + if len(pageProjects) >= numProjectsToGet { + break + } } type newsThreadQuery struct { @@ -139,7 +154,10 @@ func Index(c *RequestContext) ResponseData { baseData.BodyClasses = append(baseData.BodyClasses, "hmdev", "landing") // TODO: Is "hmdev" necessary any more? var res ResponseData - err = res.WriteTemplate("index.html", getBaseData(c)) + err = res.WriteTemplate("index.html", LandingTemplateData{ + BaseData: getBaseData(c), + PostColumns: [][]LandingPageProject{pageProjects}, // TODO: NO + }) if err != nil { panic(err) } diff --git a/src/website/routes.go b/src/website/routes.go index 777280e..c7ad47f 100644 --- a/src/website/routes.go +++ b/src/website/routes.go @@ -27,6 +27,7 @@ func NewWebsiteRoutes(conn *pgxpool.Pool) http.Handler { c.Conn = conn return true, ResponseData{} }, + // TODO: Add a timeout? We don't want routes hanging forever }, AfterHandlers: []HMNAfterHandler{ErrorLoggingHandler}, } @@ -90,20 +91,9 @@ func getBaseData(c *RequestContext) templates.BaseData { } return templates.BaseData{ - Project: templates.Project{ - Name: *c.CurrentProject.Name, - Subdomain: *c.CurrentProject.Slug, - Color: c.CurrentProject.Color1, - - IsHMN: c.CurrentProject.IsHMN(), - - HasBlog: true, - HasForum: true, - HasWiki: true, - HasLibrary: true, - }, - User: templateUser, - Theme: "dark", + Project: templates.ProjectToTemplate(c.CurrentProject), + User: templateUser, + Theme: "dark", } }