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.
This commit is contained in:
Ben Visness 2021-04-11 16:46:06 -05:00
parent e827f47834
commit cbe4b71869
19 changed files with 675 additions and 59 deletions

16
resetdb.sh Normal file
View File

@ -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

View File

@ -5,7 +5,6 @@ import (
"errors" "errors"
"reflect" "reflect"
"strings" "strings"
"time"
"git.handmade.network/hmn/hmn/src/config" "git.handmade.network/hmn/hmn/src/config"
"git.handmade.network/hmn/hmn/src/oops" "git.handmade.network/hmn/hmn/src/oops"
@ -94,6 +93,10 @@ func (it *StructQueryIterator) ToSlice() []interface{} {
for { for {
row, ok := it.Next() row, ok := it.Next()
if !ok { if !ok {
err := it.rows.Err()
if err != nil {
panic(oops.New(err, "error while iterating through db results"))
}
break break
} }
result = append(result, row) result = append(result, row)
@ -119,14 +122,11 @@ func followPathThroughStructs(structVal reflect.Value, path []int) reflect.Value
return val return val
} }
func Query(ctx context.Context, conn *pgxpool.Pool, destExample interface{}, query string, args ...interface{}) (StructQueryIterator, error) { 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()
destType := reflect.TypeOf(destExample) destType := reflect.TypeOf(destExample)
columnNames, fieldPaths, err := getColumnNamesAndPaths(destType, nil, "") columnNames, fieldPaths, err := getColumnNamesAndPaths(destType, nil, "")
if err != 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, ", ") 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) { if errors.Is(err, context.DeadlineExceeded) {
panic("query exceeded its deadline") panic("query exceeded its deadline")
} }
return StructQueryIterator{}, err return nil, err
} }
return StructQueryIterator{ return &StructQueryIterator{
fieldPaths: fieldPaths, fieldPaths: fieldPaths,
rows: rows, rows: rows,
destType: destType, destType: destType,

View File

@ -4,6 +4,7 @@ import (
"net/url" "net/url"
"git.handmade.network/hmn/hmn/src/config" "git.handmade.network/hmn/hmn/src/config"
"git.handmade.network/hmn/hmn/src/oops"
) )
const StaticPath = "/public" const StaticPath = "/public"
@ -14,12 +15,35 @@ type Q struct {
Value string Value string
} }
func Url(path string, query []Q) string { var baseUrlParsed url.URL
result := config.Config.BaseUrl + "/" + trim(path)
if q := encodeQuery(query); q != "" { func init() {
result += "?" + q 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 { func StaticUrl(path string, query []Q) string {

View File

@ -193,8 +193,8 @@ func Migrate(targetVersion types.MigrationVersion) {
// roll forward // roll forward
for i := currentIndex + 1; i <= targetIndex; i++ { for i := currentIndex + 1; i <= targetIndex; i++ {
version := allVersions[i] version := allVersions[i]
fmt.Printf("Applying migration %v\n", version)
migration := migrations.All[version] migration := migrations.All[version]
fmt.Printf("Applying migration %v (%v)\n", version, migration.Name())
tx, err := conn.Begin(context.Background()) tx, err := conn.Begin(context.Background())
if err != nil { if err != nil {

View File

@ -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")
}

View File

@ -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")
}

View File

@ -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")
}

View File

@ -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")
}

View File

@ -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")
}

22
src/models/member.go Normal file
View File

@ -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"`
}

View File

@ -1,6 +1,9 @@
package models package models
import "reflect" import (
"reflect"
"time"
)
const HMNProjectID = 1 const HMNProjectID = 1
@ -16,6 +19,8 @@ type Project struct {
Color1 string `db:"color_1"` Color1 string `db:"color_1"`
Color2 string `db:"color_2"` Color2 string `db:"color_2"`
AllLastUpdated time.Time `db:"all_last_updated"`
} }
func (p *Project) IsHMN() bool { func (p *Project) IsHMN() bool {

59
src/templates/mapping.go Normal file
View File

@ -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
}

View File

@ -9,9 +9,9 @@
<a class="logout" href="{{ url "/logout" }}"><span class="icon-logout"></span> Logout</a> <a class="logout" href="{{ url "/logout" }}"><span class="icon-logout"></span> Logout</a>
{{ else }} {{ else }}
<a class="register" id="register-link" href="{{ url "/member_register" }}">Register</a> <a class="register" id="register-link" href="{{ url "/member_register" }}">Register</a>
<a class="login" id="login-link" href="{{ projecturl "/login" }}">Log in</a> <a class="login" id="login-link" href="{{ currentprojecturl "/login" }}">Log in</a>
<div id="login-popup"> <div id="login-popup">
<form action="{{ projecturl "/login" }}" method="post"> <form action="{{ currentprojecturl "/login" }}" method="post">
{{/* TODO: CSRF */}} {{/* TODO: CSRF */}}
<table> <table>
<tr> <tr>
@ -38,24 +38,24 @@
</a> </a>
<div class="items flex items-center justify-center justify-start-ns"> <div class="items flex items-center justify-center justify-start-ns">
{{ if not .Project.IsHMN }} {{ if not .Project.IsHMN }}
<a class="project-logo" href="{{ projecturl "/" }}"> <a class="project-logo" href="{{ currentprojecturl "/" }}">
<h1>{{ .Project.Name }}</h1> <h1>{{ .Project.Name }}</h1>
</a> </a>
{{ end }} {{ end }}
{{ if .Project.HasBlog }} {{ if .Project.HasBlog }}
<a href="{{ projecturl "/blog" }}" class="blog">Blog</a> <a href="{{ currentprojecturl "/blog" }}" class="blog">Blog</a>
{{ end }} {{ end }}
{{ if .Project.HasForum }} {{ if .Project.HasForum }}
<a href="{{ projecturl "/forum" }}" class="forums">Forums</a> <a href="{{ currentprojecturl "/forum" }}" class="forums">Forums</a>
{{ end }} {{ end }}
{{ if .Project.HasWiki }} {{ if .Project.HasWiki }}
<a href="{{ projecturl "/wiki" }}" class="wiki">Wiki</a> <a href="{{ currentprojecturl "/wiki" }}" class="wiki">Wiki</a>
{{ end }} {{ end }}
{{ if .Project.HasLibrary }} {{ if .Project.HasLibrary }}
<a href="{{ projecturl "/library" }}" class="library">Library</a> <a href="{{ currentprojecturl "/library" }}" class="library">Library</a>
{{ end }} {{ end }}
{{ if .Project.IsHMN }} {{ if .Project.IsHMN }}
<a href="{{ projecturl "/manifesto" }}" class="misson">Mission</a> <a href="{{ currentprojecturl "/manifesto" }}" class="misson">Mission</a>
{{ end }} {{ end }}
{{/* {% if project.default_annotation_category %} */}} {{/* {% if project.default_annotation_category %} */}}
{{ if false }} {{ if false }}

View File

@ -1,7 +1,98 @@
{{ template "base.html" . }} {{ template "base.html" . }}
{{ define "content" }} {{ define "content" }}
This is the index page. <div class="content-block">
<div class="optionbar pb2">
<div class="tc tl-l w-100">
<h2 class="di-l mr2-l">Around the Network</h2>
<ul class="list dib-l">
<li class="dib-ns ma0 ph2">
<a href="{{ url "/feed" }}">View all posts on HMN</a>
</li>
<li class="dib-ns ma0 ph2">
<a href="{{ url "/podcast" }}">Podcast</a>
</li>
{{/* TODO: Make a better IRC intro page because the current one is trash anyway */}}
{{/*
<li class="dib-ns ma0 ph2">
<a href="{{ url "/streams" }}">See who's live</a>
</li>
<li class="dib-ns ma0 ph2">
<a href="/blogs/p/1138-%5Btutorial%5D_handmade_network_irc" target="_blank">Chat in IRC</a>
</li>
*/}}
<li class="dib-ns ma0 ph2">
<a href="https://discord.gg/hxWxDee" target="_blank">Chat on Discord</a>
</li>
<li class="dib-ns ma0 ph2">
<a href="https://handmadedev.show/" target="_blank">See the Show</a>
</li>
</ul>
</div>
</div>
</div>
<div class="content-block news cf">
{{ range $i, $col := .PostColumns }}
<div class="fl w-100 w-50-l">
<div class="mw7 mw-none-l center-layout">
{{ if eq $i 0 }}
<div class="pt3">
Wow, a featured post!
{{/* {% include "blog_index_thread_list_entry.html" with post=featured_post align_top=True %} */}}
</div>
{{ end }}
{{ range $entry := $col }}
{{ $proj := $entry.Project }}
{{ $posts := $entry.Posts }}
<div class="pt3" id="p{{ $proj.Subdomain }}"> {{/* TODO: Is this ID used for anything? */}}
<a {{/* TODO: Replace this special-case style with a CSS class */}}
href="{{ projecturl "/" $proj }}"
style="color: #{{ eq $.Theme "dark" | ternary (brighten $proj.Color1 0.1) (darken $proj.Color1 0.2) }}"
>
<h2 class="ph3">{{ $proj.Name }}</h2>
</a>
{{/* 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 }}
<div>
{{ .Post.Preview }}
</div>
{{/*
{% 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 %}
<div class="ph3 thread unread more">
<a class="title"
href="{% url 'project_forum' subdomain=proj.slug %}"
>{{ more }} more recently &rarr;</a>
</div>
{% endif %}
{% endwith %}
*/}}
</div>
{{ end }}
</div>
</div>
{{ end }}
</div>
{{ end }} {{ end }}
{{/* {{/*

View File

@ -31,7 +31,7 @@
background-size: "{{ . }}"; background-size: "{{ . }}";
{{ end }} {{ end }}
{{ else }} {{ 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-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-image: url('data:image/png;base64,{{ eq .Theme "dark" | ternary $bgdark $bglight }}');
background-size: auto; background-size: auto;
@ -40,7 +40,7 @@
</style> </style>
{{ block "extrahead" . }}{{ end }} {{ block "extrahead" . }}{{ end }}
<link rel="stylesheet" href="{{ statictheme .Theme "theme.css" }}" /> <link rel="stylesheet" href="{{ statictheme .Theme "theme.css" }}" />
<link rel="stylesheet" href="{{ urlq "assets/project.css" (query "color" .Project.Color) }}" /> <link rel="stylesheet" href="{{ urlq "assets/project.css" (query "color" .Project.Color1) }}" />
<link rel="apple-touch-icon" sizes="57x57" href="{{ static "apple-icon-57x57.png" }}"> <link rel="apple-touch-icon" sizes="57x57" href="{{ static "apple-icon-57x57.png" }}">
<link rel="apple-touch-icon" sizes="60x60" href="{{ static "apple-icon-60x60.png" }}"> <link rel="apple-touch-icon" sizes="60x60" href="{{ static "apple-icon-60x60.png" }}">
<link rel="apple-touch-icon" sizes="72x72" href="{{ static "apple-icon-72x72.png" }}"> <link rel="apple-touch-icon" sizes="72x72" href="{{ static "apple-icon-72x72.png" }}">

View File

@ -69,18 +69,25 @@ var HMNTemplateFuncs = template.FuncMap{
"cachebust": func() string { "cachebust": func() string {
return cachebust 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) { "darken": func(hexColor string, amount float64) (string, error) {
if len(hexColor) < 6 { if len(hexColor) < 6 {
return "", fmt.Errorf("couldn't darken invalid hex color: %v", hexColor) return "", fmt.Errorf("couldn't darken invalid hex color: %v", hexColor)
} }
return noire.NewHex(hexColor).Shade(amount).Hex(), nil return noire.NewHex(hexColor).Shade(amount).Hex(), nil
}, },
"projecturl": func(url string) string { "projecturl": func(url string, proj interface{}) string {
return hmnurl.Url(url, nil) // TODO: Use project subdomain return hmnurl.ProjectUrl(url, nil, getProjectSubdomain(proj))
}, },
"projecturlq": func(url string, query string) string { "projecturlq": func(url string, proj interface{}, query string) string {
absUrl := hmnurl.Url(url, nil) absUrl := hmnurl.ProjectUrl(url, nil, getProjectSubdomain(proj))
return fmt.Sprintf("%s?%s", absUrl, query) // TODO: Use project subdomain return fmt.Sprintf("%s?%s", absUrl, query)
}, },
"query": func(args ...string) string { "query": func(args ...string) string {
query := url.Values{} query := url.Values{}
@ -117,3 +124,17 @@ type ErrInvalidHexColor struct {
func (e ErrInvalidHexColor) Error() string { func (e ErrInvalidHexColor) Error() string {
return fmt.Sprintf("invalid hex color: %s", e.color) 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
}

View File

@ -12,10 +12,34 @@ type BaseData struct {
User *User 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 { type Project struct {
Name string Name string
Subdomain string Subdomain string
Color string Color1 string
Color2 string
IsHMN bool IsHMN bool
@ -42,6 +66,3 @@ type BackgroundImage struct {
Url string Url string
Size string // A valid CSS background-size value Size string // A valid CSS background-size value
} }
type Post struct {
}

View File

@ -33,8 +33,17 @@ func Index(c *RequestContext) ResponseData {
const numProjectsToGet = 7 const numProjectsToGet = 7
iterProjects, err := db.Query(c.Context(), c.Conn, models.Project{}, 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, models.HMNProjectID,
numProjectsToGet*2, // hedge your bets against projects that don't have any content
) )
if err != nil { if err != nil {
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to get projects for home page")) 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() defer iterProjects.Close()
var pageProjects []LandingPageProject 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) proj := projRow.(*models.Project)
type ProjectPost struct { type ProjectPost struct {
@ -89,11 +100,7 @@ func Index(c *RequestContext) ResponseData {
projectPosts := projectPostIter.ToSlice() projectPosts := projectPostIter.ToSlice()
landingPageProject := LandingPageProject{ landingPageProject := LandingPageProject{
Project: templates.Project{ // TODO: Use a common function to map from model to template data Project: templates.ProjectToTemplate(proj),
Name: *proj.Name,
Subdomain: *proj.Slug,
// ...
},
} }
for _, projectPostRow := range projectPosts { for _, projectPostRow := range projectPosts {
@ -107,10 +114,18 @@ func Index(c *RequestContext) ResponseData {
} }
landingPageProject.Posts = append(landingPageProject.Posts, LandingPagePost{ 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, HasRead: hasRead,
}) })
} }
if len(projectPosts) > 0 {
pageProjects = append(pageProjects, landingPageProject)
}
if len(pageProjects) >= numProjectsToGet {
break
}
} }
type newsThreadQuery struct { 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? baseData.BodyClasses = append(baseData.BodyClasses, "hmdev", "landing") // TODO: Is "hmdev" necessary any more?
var res ResponseData 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 { if err != nil {
panic(err) panic(err)
} }

View File

@ -27,6 +27,7 @@ func NewWebsiteRoutes(conn *pgxpool.Pool) http.Handler {
c.Conn = conn c.Conn = conn
return true, ResponseData{} return true, ResponseData{}
}, },
// TODO: Add a timeout? We don't want routes hanging forever
}, },
AfterHandlers: []HMNAfterHandler{ErrorLoggingHandler}, AfterHandlers: []HMNAfterHandler{ErrorLoggingHandler},
} }
@ -90,18 +91,7 @@ func getBaseData(c *RequestContext) templates.BaseData {
} }
return templates.BaseData{ return templates.BaseData{
Project: templates.Project{ Project: templates.ProjectToTemplate(c.CurrentProject),
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, User: templateUser,
Theme: "dark", Theme: "dark",
} }