From cbe4b7186941a2e69d7ad99559eddadede52dcbd Mon Sep 17 00:00:00 2001 From: Ben Visness Date: Sun, 11 Apr 2021 16:46:06 -0500 Subject: [PATCH] Some kind of arbitrary checkpoint I am in the middle of: - porting the landing page - making some db changes to help with that - deleting the member and memberextended tables Mainly the last one. Doing so requires us to update all the other tables that currently point at member and memberextended so that the foreign keys will point directly to users. The big thing that we still have yet to do is links, and actually copying data from the member and memberextended tables to users. --- resetdb.sh | 16 +++ src/db/db.go | 16 +-- src/hmnurl/hmnurl.go | 34 +++++- src/migration/migration.go | 2 +- .../2021-04-09T000000Z_DeleteOrphanedLinks.go | 55 +++++++++ ...21-04-10T013044Z_CopyMemberExtendedData.go | 107 ++++++++++++++++++ ...1-04-10T015339Z_DropMemberExtendedTable.go | 43 +++++++ ...1-04-11T195747Z_RemoveMemberAndExtended.go | 79 +++++++++++++ ...-04-11T210958Z_RemoveMemberAndExtended2.go | 65 +++++++++++ src/models/member.go | 22 ++++ src/models/project.go | 7 +- src/templates/mapping.go | 59 ++++++++++ src/templates/src/include/header.html | 16 +-- src/templates/src/index.html | 93 ++++++++++++++- src/templates/src/layouts/base.html | 4 +- src/templates/templates.go | 31 ++++- src/templates/types.go | 29 ++++- src/website/landing.go | 38 +++++-- src/website/routes.go | 18 +-- 19 files changed, 675 insertions(+), 59 deletions(-) create mode 100644 resetdb.sh create mode 100644 src/migration/migrations/2021-04-09T000000Z_DeleteOrphanedLinks.go create mode 100644 src/migration/migrations/2021-04-10T013044Z_CopyMemberExtendedData.go create mode 100644 src/migration/migrations/2021-04-10T015339Z_DropMemberExtendedTable.go create mode 100644 src/migration/migrations/2021-04-11T195747Z_RemoveMemberAndExtended.go create mode 100644 src/migration/migrations/2021-04-11T210958Z_RemoveMemberAndExtended2.go create mode 100644 src/models/member.go create mode 100644 src/templates/mapping.go 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", } }