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:
parent
e827f47834
commit
cbe4b71869
|
@ -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
|
16
src/db/db.go
16
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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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"`
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -9,9 +9,9 @@
|
|||
<a class="logout" href="{{ url "/logout" }}"><span class="icon-logout"></span> Logout</a>
|
||||
{{ else }}
|
||||
<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">
|
||||
<form action="{{ projecturl "/login" }}" method="post">
|
||||
<form action="{{ currentprojecturl "/login" }}" method="post">
|
||||
{{/* TODO: CSRF */}}
|
||||
<table>
|
||||
<tr>
|
||||
|
@ -38,24 +38,24 @@
|
|||
</a>
|
||||
<div class="items flex items-center justify-center justify-start-ns">
|
||||
{{ if not .Project.IsHMN }}
|
||||
<a class="project-logo" href="{{ projecturl "/" }}">
|
||||
<a class="project-logo" href="{{ currentprojecturl "/" }}">
|
||||
<h1>{{ .Project.Name }}</h1>
|
||||
</a>
|
||||
{{ end }}
|
||||
{{ if .Project.HasBlog }}
|
||||
<a href="{{ projecturl "/blog" }}" class="blog">Blog</a>
|
||||
<a href="{{ currentprojecturl "/blog" }}" class="blog">Blog</a>
|
||||
{{ end }}
|
||||
{{ if .Project.HasForum }}
|
||||
<a href="{{ projecturl "/forum" }}" class="forums">Forums</a>
|
||||
<a href="{{ currentprojecturl "/forum" }}" class="forums">Forums</a>
|
||||
{{ end }}
|
||||
{{ if .Project.HasWiki }}
|
||||
<a href="{{ projecturl "/wiki" }}" class="wiki">Wiki</a>
|
||||
<a href="{{ currentprojecturl "/wiki" }}" class="wiki">Wiki</a>
|
||||
{{ end }}
|
||||
{{ if .Project.HasLibrary }}
|
||||
<a href="{{ projecturl "/library" }}" class="library">Library</a>
|
||||
<a href="{{ currentprojecturl "/library" }}" class="library">Library</a>
|
||||
{{ end }}
|
||||
{{ if .Project.IsHMN }}
|
||||
<a href="{{ projecturl "/manifesto" }}" class="misson">Mission</a>
|
||||
<a href="{{ currentprojecturl "/manifesto" }}" class="misson">Mission</a>
|
||||
{{ end }}
|
||||
{{/* {% if project.default_annotation_category %} */}}
|
||||
{{ if false }}
|
||||
|
|
|
@ -1,7 +1,98 @@
|
|||
{{ template "base.html" . }}
|
||||
|
||||
{{ 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 →</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
*/}}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{/*
|
||||
|
|
|
@ -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 @@
|
|||
</style>
|
||||
{{ block "extrahead" . }}{{ end }}
|
||||
<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="60x60" href="{{ static "apple-icon-60x60.png" }}">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="{{ static "apple-icon-72x72.png" }}">
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue