Get post content migrated and loading

This commit is contained in:
Ben Visness 2021-04-22 23:07:44 -05:00
parent d7c512f1c8
commit a04b00c0a7
17 changed files with 373 additions and 92 deletions

View File

@ -102,9 +102,9 @@ func getSortedMigrationVersions() []types.MigrationVersion {
return allVersions return allVersions
} }
func getCurrentVersion(conn *pgx.Conn) (types.MigrationVersion, error) { func getCurrentVersion(ctx context.Context, conn *pgx.Conn) (types.MigrationVersion, error) {
var currentVersion time.Time var currentVersion time.Time
row := conn.QueryRow(context.Background(), "SELECT version FROM hmn_migration") row := conn.QueryRow(ctx, "SELECT version FROM hmn_migration")
err := row.Scan(&currentVersion) err := row.Scan(&currentVersion)
if err != nil { if err != nil {
return types.MigrationVersion{}, err return types.MigrationVersion{}, err
@ -115,10 +115,12 @@ func getCurrentVersion(conn *pgx.Conn) (types.MigrationVersion, error) {
} }
func ListMigrations() { func ListMigrations() {
conn := db.NewConn() ctx := context.Background()
defer conn.Close(context.Background())
currentVersion, _ := getCurrentVersion(conn) conn := db.NewConn()
defer conn.Close(ctx)
currentVersion, _ := getCurrentVersion(ctx, conn)
for _, version := range getSortedMigrationVersions() { for _, version := range getSortedMigrationVersions() {
migration := migrations.All[version] migration := migrations.All[version]
indicator := " " indicator := " "
@ -130,11 +132,13 @@ func ListMigrations() {
} }
func Migrate(targetVersion types.MigrationVersion) { func Migrate(targetVersion types.MigrationVersion) {
ctx := context.Background() // In the future, this could actually do something cool.
conn := db.NewConn() conn := db.NewConn()
defer conn.Close(context.Background()) defer conn.Close(ctx)
// create migration table // create migration table
_, err := conn.Exec(context.Background(), ` _, err := conn.Exec(ctx, `
CREATE TABLE IF NOT EXISTS hmn_migration ( CREATE TABLE IF NOT EXISTS hmn_migration (
version TIMESTAMP WITH TIME ZONE version TIMESTAMP WITH TIME ZONE
) )
@ -144,21 +148,21 @@ func Migrate(targetVersion types.MigrationVersion) {
} }
// ensure there is a row // ensure there is a row
row := conn.QueryRow(context.Background(), "SELECT COUNT(*) FROM hmn_migration") row := conn.QueryRow(ctx, "SELECT COUNT(*) FROM hmn_migration")
var numRows int var numRows int
err = row.Scan(&numRows) err = row.Scan(&numRows)
if err != nil { if err != nil {
panic(err) panic(err)
} }
if numRows < 1 { if numRows < 1 {
_, err := conn.Exec(context.Background(), "INSERT INTO hmn_migration (version) VALUES ($1)", time.Time{}) _, err := conn.Exec(ctx, "INSERT INTO hmn_migration (version) VALUES ($1)", time.Time{})
if err != nil { if err != nil {
panic(fmt.Errorf("failed to insert initial migration row: %w", err)) panic(fmt.Errorf("failed to insert initial migration row: %w", err))
} }
} }
// run migrations // run migrations
currentVersion, err := getCurrentVersion(conn) currentVersion, err := getCurrentVersion(ctx, conn)
if err != nil { if err != nil {
panic(fmt.Errorf("failed to get current version: %w", err)) panic(fmt.Errorf("failed to get current version: %w", err))
} }
@ -196,25 +200,25 @@ func Migrate(targetVersion types.MigrationVersion) {
migration := migrations.All[version] migration := migrations.All[version]
fmt.Printf("Applying migration %v (%v)\n", version, migration.Name()) fmt.Printf("Applying migration %v (%v)\n", version, migration.Name())
tx, err := conn.Begin(context.Background()) tx, err := conn.Begin(ctx)
if err != nil { if err != nil {
panic(fmt.Errorf("failed to start transaction: %w", err)) panic(fmt.Errorf("failed to start transaction: %w", err))
} }
defer tx.Rollback(context.Background()) defer tx.Rollback(ctx)
err = migration.Up(tx) err = migration.Up(ctx, tx)
if err != nil { if err != nil {
fmt.Printf("MIGRATION FAILED for migration %v.\n", version) fmt.Printf("MIGRATION FAILED for migration %v.\n", version)
fmt.Printf("Error: %v\n", err) fmt.Printf("Error: %v\n", err)
return return
} }
_, err = tx.Exec(context.Background(), "UPDATE hmn_migration SET version = $1", version) _, err = tx.Exec(ctx, "UPDATE hmn_migration SET version = $1", version)
if err != nil { if err != nil {
panic(fmt.Errorf("failed to update version in migrations table: %w", err)) panic(fmt.Errorf("failed to update version in migrations table: %w", err))
} }
err = tx.Commit(context.Background()) err = tx.Commit(ctx)
if err != nil { if err != nil {
panic(fmt.Errorf("failed to commit transaction: %w", err)) panic(fmt.Errorf("failed to commit transaction: %w", err))
} }
@ -228,27 +232,27 @@ func Migrate(targetVersion types.MigrationVersion) {
previousVersion = allVersions[i-1] previousVersion = allVersions[i-1]
} }
tx, err := conn.Begin(context.Background()) tx, err := conn.Begin(ctx)
if err != nil { if err != nil {
panic(fmt.Errorf("failed to start transaction: %w", err)) panic(fmt.Errorf("failed to start transaction: %w", err))
} }
defer tx.Rollback(context.Background()) defer tx.Rollback(ctx)
fmt.Printf("Rolling back migration %v\n", version) fmt.Printf("Rolling back migration %v\n", version)
migration := migrations.All[version] migration := migrations.All[version]
err = migration.Down(tx) err = migration.Down(ctx, tx)
if err != nil { if err != nil {
fmt.Printf("MIGRATION FAILED for migration %v.\n", version) fmt.Printf("MIGRATION FAILED for migration %v.\n", version)
fmt.Printf("Error: %v\n", err) fmt.Printf("Error: %v\n", err)
return return
} }
_, err = tx.Exec(context.Background(), "UPDATE hmn_migration SET version = $1", previousVersion) _, err = tx.Exec(ctx, "UPDATE hmn_migration SET version = $1", previousVersion)
if err != nil { if err != nil {
panic(fmt.Errorf("failed to update version in migrations table: %w", err)) panic(fmt.Errorf("failed to update version in migrations table: %w", err))
} }
err = tx.Commit(context.Background()) err = tx.Commit(ctx)
if err != nil { if err != nil {
panic(fmt.Errorf("failed to commit transaction: %w", err)) panic(fmt.Errorf("failed to commit transaction: %w", err))
} }

View File

@ -25,10 +25,10 @@ func (m %NAME%) Description() string {
return %DESCRIPTION% return %DESCRIPTION%
} }
func (m %NAME%) Up(tx pgx.Tx) error { func (m %NAME%) Up(ctx context.Context, tx pgx.Tx) error {
panic("Implement me") panic("Implement me")
} }
func (m %NAME%) Down(tx pgx.Tx) error { func (m %NAME%) Down(ctx context.Context, tx pgx.Tx) error {
panic("Implement me") panic("Implement me")
} }

View File

@ -26,8 +26,8 @@ func (m Initial) Description() string {
return "Creates all the tables from the old site" return "Creates all the tables from the old site"
} }
func (m Initial) Up(tx pgx.Tx) error { func (m Initial) Up(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(context.Background(), ` _, err := tx.Exec(ctx, `
-- --
-- PostgreSQL database dump -- PostgreSQL database dump
-- --
@ -4648,6 +4648,6 @@ func (m Initial) Up(tx pgx.Tx) error {
return nil return nil
} }
func (m Initial) Down(tx pgx.Tx) error { func (m Initial) Down(ctx context.Context, tx pgx.Tx) error {
panic("nope, ha ha, I'm the initial migration. how did you even run this function anyway") panic("nope, ha ha, I'm the initial migration. how did you even run this function anyway")
} }

View File

@ -26,8 +26,8 @@ func (m AddSessionTable) Description() string {
return "Adds a session table to replace the Django session table" return "Adds a session table to replace the Django session table"
} }
func (m AddSessionTable) Up(tx pgx.Tx) error { func (m AddSessionTable) Up(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(context.Background(), ` _, err := tx.Exec(ctx, `
CREATE TABLE sessions ( CREATE TABLE sessions (
id VARCHAR(40) PRIMARY KEY, id VARCHAR(40) PRIMARY KEY,
username VARCHAR(150) NOT NULL, username VARCHAR(150) NOT NULL,
@ -37,8 +37,8 @@ func (m AddSessionTable) Up(tx pgx.Tx) error {
return err return err
} }
func (m AddSessionTable) Down(tx pgx.Tx) error { func (m AddSessionTable) Down(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(context.Background(), ` _, err := tx.Exec(ctx, `
DROP TABLE sessions; DROP TABLE sessions;
`) `)
return err return err

View File

@ -26,16 +26,16 @@ func (m AllowLongerPasswordHashes) Description() string {
return "Increase the storage size limit on hashed passwords" return "Increase the storage size limit on hashed passwords"
} }
func (m AllowLongerPasswordHashes) Up(tx pgx.Tx) error { func (m AllowLongerPasswordHashes) Up(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(context.Background(), ` _, err := tx.Exec(ctx, `
ALTER TABLE auth_user ALTER TABLE auth_user
ALTER COLUMN password TYPE VARCHAR(256) ALTER COLUMN password TYPE VARCHAR(256)
`) `)
return err return err
} }
func (m AllowLongerPasswordHashes) Down(tx pgx.Tx) error { func (m AllowLongerPasswordHashes) Down(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(context.Background(), ` _, err := tx.Exec(ctx, `
ALTER TABLE auth_user ALTER TABLE auth_user
ALTER COLUMN password TYPE VARCHAR(128) ALTER COLUMN password TYPE VARCHAR(128)
`) `)

View File

@ -28,9 +28,9 @@ func (m DeleteOrphanedData) Description() string {
return "Delete data that doesn't have other important associated records" return "Delete data that doesn't have other important associated records"
} }
func (m DeleteOrphanedData) Up(tx pgx.Tx) error { func (m DeleteOrphanedData) Up(ctx context.Context, tx pgx.Tx) error {
// Delete orphaned users (no member) // Delete orphaned users (no member)
res, err := tx.Exec(context.Background(), ` res, err := tx.Exec(ctx, `
DELETE FROM auth_user DELETE FROM auth_user
WHERE WHERE
id IN ( id IN (
@ -58,7 +58,7 @@ func (m DeleteOrphanedData) Up(tx pgx.Tx) error {
// Delete memberextended<->links joins for memberextendeds that are about to die // Delete memberextended<->links joins for memberextendeds that are about to die
// (my kingdom for ON DELETE CASCADE, I mean come on) // (my kingdom for ON DELETE CASCADE, I mean come on)
res, err = tx.Exec(context.Background(), ` res, err = tx.Exec(ctx, `
DELETE FROM handmade_memberextended_links DELETE FROM handmade_memberextended_links
WHERE WHERE
memberextended_id IN ( memberextended_id IN (
@ -71,7 +71,7 @@ func (m DeleteOrphanedData) Up(tx pgx.Tx) error {
fmt.Printf("Deleted %v memberextended<->links joins\n", res.RowsAffected()) fmt.Printf("Deleted %v memberextended<->links joins\n", res.RowsAffected())
// Delete orphaned memberextendeds (no member) // Delete orphaned memberextendeds (no member)
res, err = tx.Exec(context.Background(), ` res, err = tx.Exec(ctx, `
DELETE FROM handmade_memberextended DELETE FROM handmade_memberextended
WHERE WHERE
id IN ( id IN (
@ -84,7 +84,7 @@ func (m DeleteOrphanedData) Up(tx pgx.Tx) error {
fmt.Printf("Deleted %v memberextendeds\n", res.RowsAffected()) fmt.Printf("Deleted %v memberextendeds\n", res.RowsAffected())
// Delete orphaned links (no member or project) // Delete orphaned links (no member or project)
res, err = tx.Exec(context.Background(), ` res, err = tx.Exec(ctx, `
DELETE FROM handmade_links DELETE FROM handmade_links
WHERE WHERE
id IN ( id IN (
@ -106,6 +106,6 @@ func (m DeleteOrphanedData) Up(tx pgx.Tx) error {
return nil return nil
} }
func (m DeleteOrphanedData) Down(tx pgx.Tx) error { func (m DeleteOrphanedData) Down(ctx context.Context, tx pgx.Tx) error {
panic("Implement me") panic("Implement me")
} }

View File

@ -32,7 +32,7 @@ func (m RemoveMemberAndExtended) Description() string {
return "Remove the member and member extended records, collapsing their data into users" return "Remove the member and member extended records, collapsing their data into users"
} }
func (m RemoveMemberAndExtended) Up(tx pgx.Tx) error { func (m RemoveMemberAndExtended) Up(ctx context.Context, tx pgx.Tx) error {
// Creates a column that will eventually be a foreign key to auth_user. // 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) { createUserColumn := func(ctx context.Context, tx pgx.Tx, table string, before string, notNull bool) {
nullConstraint := "" nullConstraint := ""
@ -51,18 +51,18 @@ func (m RemoveMemberAndExtended) Up(tx pgx.Tx) error {
} }
// Migrate a lot of simple foreign keys // Migrate a lot of simple foreign keys
createUserColumn(context.Background(), tx, "handmade_communicationchoice", "member_id", true) createUserColumn(ctx, tx, "handmade_communicationchoice", "member_id", true)
createUserColumn(context.Background(), tx, "handmade_communicationsubcategory", "member_id", true) createUserColumn(ctx, tx, "handmade_communicationsubcategory", "member_id", true)
createUserColumn(context.Background(), tx, "handmade_communicationsubthread", "member_id", true) createUserColumn(ctx, tx, "handmade_communicationsubthread", "member_id", true)
createUserColumn(context.Background(), tx, "handmade_discord", "member_id", true) createUserColumn(ctx, tx, "handmade_discord", "member_id", true)
createUserColumn(context.Background(), tx, "handmade_categorylastreadinfo", "member_id", true) createUserColumn(ctx, tx, "handmade_categorylastreadinfo", "member_id", true)
createUserColumn(context.Background(), tx, "handmade_threadlastreadinfo", "member_id", true) createUserColumn(ctx, tx, "handmade_threadlastreadinfo", "member_id", true)
createUserColumn(context.Background(), tx, "handmade_posttextversion", "editor_id", false) createUserColumn(ctx, tx, "handmade_posttextversion", "editor_id", false)
createUserColumn(context.Background(), tx, "handmade_post", "author_id", false) createUserColumn(ctx, tx, "handmade_post", "author_id", false)
createUserColumn(context.Background(), tx, "handmade_member_projects", "member_id", true) createUserColumn(ctx, tx, "handmade_member_projects", "member_id", true)
// Directly associate links with members // Directly associate links with members
_, err := tx.Exec(context.Background(), ` _, err := tx.Exec(ctx, `
ALTER TABLE handmade_links ALTER TABLE handmade_links
ADD COLUMN user_id INTEGER DEFAULT 99999, ADD COLUMN user_id INTEGER DEFAULT 99999,
ADD COLUMN project_id INTEGER DEFAULT 99999; ADD COLUMN project_id INTEGER DEFAULT 99999;
@ -92,7 +92,7 @@ func (m RemoveMemberAndExtended) Up(tx pgx.Tx) error {
return oops.New(err, "failed to associate links with members and projects") return oops.New(err, "failed to associate links with members and projects")
} }
_, err = tx.Exec(context.Background(), ` _, err = tx.Exec(ctx, `
ALTER TABLE auth_user ALTER TABLE auth_user
-- From handmade_member -- -- From handmade_member --
ADD blurb VARCHAR(140) NOT NULL DEFAULT '', ADD blurb VARCHAR(140) NOT NULL DEFAULT '',
@ -178,6 +178,6 @@ func (m RemoveMemberAndExtended) Up(tx pgx.Tx) error {
return nil return nil
} }
func (m RemoveMemberAndExtended) Down(tx pgx.Tx) error { func (m RemoveMemberAndExtended) Down(ctx context.Context, tx pgx.Tx) error {
panic("Implement me") panic("Implement me")
} }

View File

@ -32,7 +32,7 @@ func (m RemoveMemberAndExtended2) Description() string {
return "Phase 2 of the above" return "Phase 2 of the above"
} }
func (m RemoveMemberAndExtended2) Up(tx pgx.Tx) error { func (m RemoveMemberAndExtended2) Up(ctx context.Context, tx pgx.Tx) error {
dropOldColumn := func(ctx context.Context, tx pgx.Tx, table string, before, after string, onDelete string) { dropOldColumn := func(ctx context.Context, tx pgx.Tx, table string, before, after string, onDelete string) {
_, err := tx.Exec(ctx, ` _, err := tx.Exec(ctx, `
ALTER TABLE `+table+` ALTER TABLE `+table+`
@ -48,17 +48,17 @@ func (m RemoveMemberAndExtended2) Up(tx pgx.Tx) error {
} }
} }
dropOldColumn(context.Background(), tx, "handmade_communicationchoice", "member_id", "user_id", "CASCADE") dropOldColumn(ctx, tx, "handmade_communicationchoice", "member_id", "user_id", "CASCADE")
dropOldColumn(context.Background(), tx, "handmade_communicationsubcategory", "member_id", "user_id", "CASCADE") dropOldColumn(ctx, tx, "handmade_communicationsubcategory", "member_id", "user_id", "CASCADE")
dropOldColumn(context.Background(), tx, "handmade_communicationsubthread", "member_id", "user_id", "CASCADE") dropOldColumn(ctx, tx, "handmade_communicationsubthread", "member_id", "user_id", "CASCADE")
dropOldColumn(context.Background(), tx, "handmade_discord", "member_id", "hmn_user_id", "CASCADE") dropOldColumn(ctx, tx, "handmade_discord", "member_id", "hmn_user_id", "CASCADE")
dropOldColumn(context.Background(), tx, "handmade_categorylastreadinfo", "member_id", "user_id", "CASCADE") dropOldColumn(ctx, tx, "handmade_categorylastreadinfo", "member_id", "user_id", "CASCADE")
dropOldColumn(context.Background(), tx, "handmade_threadlastreadinfo", "member_id", "user_id", "CASCADE") dropOldColumn(ctx, tx, "handmade_threadlastreadinfo", "member_id", "user_id", "CASCADE")
dropOldColumn(context.Background(), tx, "handmade_posttextversion", "editor_id", "editor_id", "SET NULL") dropOldColumn(ctx, tx, "handmade_posttextversion", "editor_id", "editor_id", "SET NULL")
dropOldColumn(context.Background(), tx, "handmade_post", "author_id", "author_id", "SET NULL") dropOldColumn(ctx, tx, "handmade_post", "author_id", "author_id", "SET NULL")
dropOldColumn(context.Background(), tx, "handmade_member_projects", "member_id", "user_id", "SET NULL") dropOldColumn(ctx, tx, "handmade_member_projects", "member_id", "user_id", "SET NULL")
_, err := tx.Exec(context.Background(), ` _, err := tx.Exec(ctx, `
ALTER TABLE handmade_member_projects ALTER TABLE handmade_member_projects
RENAME TO handmade_user_projects; RENAME TO handmade_user_projects;
`) `)
@ -66,7 +66,7 @@ func (m RemoveMemberAndExtended2) Up(tx pgx.Tx) error {
return oops.New(err, "failed to rename member projects table") return oops.New(err, "failed to rename member projects table")
} }
_, err = tx.Exec(context.Background(), ` _, err = tx.Exec(ctx, `
ALTER TABLE handmade_links ALTER TABLE handmade_links
ADD FOREIGN KEY (user_id) REFERENCES auth_user ON DELETE CASCADE, ADD FOREIGN KEY (user_id) REFERENCES auth_user ON DELETE CASCADE,
ALTER user_id DROP DEFAULT, ALTER user_id DROP DEFAULT,
@ -87,7 +87,7 @@ func (m RemoveMemberAndExtended2) Up(tx pgx.Tx) error {
} }
// And now, the moment you've all been waiting for: // And now, the moment you've all been waiting for:
_, err = tx.Exec(context.Background(), ` _, err = tx.Exec(ctx, `
DROP TABLE handmade_member; DROP TABLE handmade_member;
DROP TABLE handmade_memberextended; DROP TABLE handmade_memberextended;
`) `)
@ -96,7 +96,7 @@ func (m RemoveMemberAndExtended2) Up(tx pgx.Tx) error {
} }
// And finally, a little cleanup. // And finally, a little cleanup.
_, err = tx.Exec(context.Background(), ` _, err = tx.Exec(ctx, `
ALTER INDEX handmade_member_projects_b098ad43 RENAME TO user_projects_btree; ALTER INDEX handmade_member_projects_b098ad43 RENAME TO user_projects_btree;
ALTER SEQUENCE handmade_member_projects_id_seq RENAME TO user_projects_id_seq; ALTER SEQUENCE handmade_member_projects_id_seq RENAME TO user_projects_id_seq;
ALTER INDEX handmade_member_projects_pkey RENAME TO user_projects_pkey; ALTER INDEX handmade_member_projects_pkey RENAME TO user_projects_pkey;
@ -108,6 +108,6 @@ func (m RemoveMemberAndExtended2) Up(tx pgx.Tx) error {
return nil return nil
} }
func (m RemoveMemberAndExtended2) Down(tx pgx.Tx) error { func (m RemoveMemberAndExtended2) Down(ctx context.Context, tx pgx.Tx) error {
panic("Implement me") panic("Implement me")
} }

View File

@ -0,0 +1,52 @@
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(FixOrphanedPostTextVersions{})
}
type FixOrphanedPostTextVersions struct{}
func (m FixOrphanedPostTextVersions) Version() types.MigrationVersion {
return types.MigrationVersion(time.Date(2021, 4, 23, 0, 0, 0, 0, time.UTC))
}
func (m FixOrphanedPostTextVersions) Name() string {
return "FixOrphanedPostTextVersions"
}
func (m FixOrphanedPostTextVersions) Description() string {
return "Set the post_id on posttextversions that lost track of their parents"
}
func (m FixOrphanedPostTextVersions) Up(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(ctx, `
UPDATE handmade_posttextversion AS tver
SET
post_id = (
SELECT id
FROM handmade_post AS post
WHERE
post.current_id = tver.id
)
WHERE
tver.post_id IS NULL
`)
if err != nil {
return oops.New(err, "failed to fix up half-orphaned posttextversions")
}
return nil
}
func (m FixOrphanedPostTextVersions) Down(ctx context.Context, tx pgx.Tx) error {
panic("Implement me")
}

View File

@ -0,0 +1,97 @@
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(RemovePostText{})
}
type RemovePostText struct{}
func (m RemovePostText) Version() types.MigrationVersion {
return types.MigrationVersion(time.Date(2021, 4, 23, 2, 11, 47, 0, time.UTC))
}
func (m RemovePostText) Name() string {
return "RemovePostText"
}
func (m RemovePostText) Description() string {
return "Collapse handmade_posttext and handmade_posttextversion into one table"
}
func (m RemovePostText) Up(ctx context.Context, tx pgx.Tx) error {
_, err := tx.Exec(ctx, `
CREATE TABLE handmade_postversion (
id INT PRIMARY KEY,
post_id INT NOT NULL REFERENCES handmade_post(id) ON DELETE CASCADE,
text_raw TEXT NOT NULL,
text_parsed TEXT NOT NULL,
parser INT NOT NULL,
edit_ip INET,
edit_date TIMESTAMP WITH TIME ZONE NOT NULL,
edit_reason VARCHAR(255) NOT NULL DEFAULT '',
editor_id INT REFERENCES auth_user(id) ON DELETE SET NULL
);
`)
if err != nil {
return oops.New(err, "failed to create new postversion table")
}
_, err = tx.Exec(ctx, `
INSERT INTO handmade_postversion
SELECT
tver.id,
tver.post_id,
t.text,
t.textparsed,
t.parser,
tver.editip,
COALESCE(tver.editdate, p.postdate),
COALESCE(tver.editreason, ''),
tver.editor_id
FROM
handmade_posttextversion AS tver
JOIN handmade_posttext AS t ON tver.text_id = t.id
JOIN handmade_post AS p ON tver.post_id = p.id
WHERE
tver.post_id IS NOT NULL
`)
if err != nil {
return oops.New(err, "failed to create postversions")
}
_, err = tx.Exec(ctx, `
ALTER TABLE handmade_post
DROP CONSTRAINT handmade_post_current_id_762211b7_fk_handmade_,
ADD FOREIGN KEY (current_id) REFERENCES handmade_postversion ON DELETE RESTRICT;
`)
if err != nil {
return oops.New(err, "failed to drop old current constraint")
}
_, err = tx.Exec(ctx, `
DROP TABLE handmade_posttextversion;
DROP TABLE handmade_posttext;
`)
if err != nil {
return oops.New(err, "failed to drop tables")
}
return nil
}
func (m RemovePostText) Down(ctx context.Context, tx pgx.Tx) error {
panic("Implement me")
}

View File

@ -1,6 +1,7 @@
package types package types
import ( import (
"context"
"time" "time"
"github.com/jackc/pgx/v4" "github.com/jackc/pgx/v4"
@ -10,8 +11,8 @@ type Migration interface {
Version() MigrationVersion Version() MigrationVersion
Name() string Name() string
Description() string Description() string
Up(conn pgx.Tx) error Up(ctx context.Context, conn pgx.Tx) error
Down(conn pgx.Tx) error Down(ctx context.Context, conn pgx.Tx) error
} }
type MigrationVersion time.Time type MigrationVersion time.Time

View File

@ -29,3 +29,25 @@ type Post struct {
Preview string `db:"preview"` Preview string `db:"preview"`
ReadOnly bool `db:"readonly"` ReadOnly bool `db:"readonly"`
} }
type Parser int
const (
ParserBBCode Parser = 1
ParserCleanHTML = 2
ParserMarkdown = 3
)
type PostVersion struct {
ID int `db:"id"`
PostID int `db:"post_id"`
TextRaw string `db:"text_raw"`
TextParsed string `db:"text_parsed"`
Parser Parser `db:"parser"`
EditIP *net.IPNet `db:"edit_ip"`
EditDate time.Time `db:"edit_date"`
EditReason string `db:"edit_reason"`
EditorID *int `db:"editor_id"`
}

View File

@ -5,13 +5,25 @@ import (
"git.handmade.network/hmn/hmn/src/models" "git.handmade.network/hmn/hmn/src/models"
) )
func PostToTemplate(p *models.Post) Post { func PostToTemplate(p *models.Post, author models.User) Post {
return Post{ return Post{
Author: UserToTemplate(&author),
Preview: p.Preview, Preview: p.Preview,
ReadOnly: p.ReadOnly, ReadOnly: p.ReadOnly,
// No content. Do it yourself if you care.
IP: p.IP.String(),
} }
} }
func PostToTemplateWithContent(p *models.Post, author models.User, content string) Post {
post := PostToTemplate(p, author)
post.Content = content
return post
}
func ProjectToTemplate(p *models.Project) Project { func ProjectToTemplate(p *models.Project) Project {
return Project{ return Project{
Name: maybeString(p.Name), Name: maybeString(p.Name),

View File

@ -4,7 +4,7 @@ This template is intended to display a single post or thread in the context of a
It should be called with PostListItemData. It should be called with PostListItemData.
*/}} */}}
<div data-tmpl="container" class="flex items-center ph3 pv2"> <div class="flex items-center ph3 pv2">
<img class="avatar-icon mr2" src="{{ .User.AvatarUrl }}"> <img class="avatar-icon mr2" src="{{ .User.AvatarUrl }}">
<div class="flex-grow-1 overflow-hidden"> <div class="flex-grow-1 overflow-hidden">
<div class="breadcrumbs"> <div class="breadcrumbs">

View File

@ -54,17 +54,22 @@
<h2 class="ph3">{{ $proj.Name }}</h2> <h2 class="ph3">{{ $proj.Name }}</h2>
</a> </a>
{{/* TODO: What is this? {{ with $entry.FeaturedPost }}
{% if entry.featured and proj.slug != "hmn" %} <div class="flex items-start ph3 pv2">
{% with post=entry.featured.0 has_read=entry.featured.1 %} <img class="avatar-icon mr2" src="{{ .User.AvatarUrl }}">
{% if post.category.kind == 5 and post.parent == None %} <div class="flex-grow-1">
{% include "thread_list_entry.html" with thread=post.thread %} <div class="overflow-hidden">
{% else %} <div class="title nowrap truncate"><a href="{{ .Url }}">{{ .Title }}</a></div>
{% include "blog_index_thread_list_entry.html" with align_top=True %} <div class="details">
{% endif %} <a class="user" href="{{ .User.ProfileUrl }}">{{ .User.Name }}</a> &mdash; <span class="datetime">{{ relativedate .Date }}</span>
{% endwith %} </div>
{% endif %} </div>
*/}} <div>
{{ .Content }}
</div>
</div>
</div>
{{ end }}
{{ range $post := $posts }} {{ range $post := $posts }}
{{ template "post_list_item.html" $post }} {{ template "post_list_item.html" $post }}

View File

@ -27,6 +27,8 @@ type Post struct {
Preview string Preview string
ReadOnly bool ReadOnly bool
Content string
IP string IP string
} }

View File

@ -19,10 +19,19 @@ type LandingTemplateData struct {
type LandingPageProject struct { type LandingPageProject struct {
Project templates.Project Project templates.Project
FeaturedPost *templates.PostListItem FeaturedPost *LandingPageFeaturedPost
Posts []templates.PostListItem Posts []templates.PostListItem
} }
type LandingPageFeaturedPost struct {
Title string
Url string
User templates.User
Date time.Time
Unread bool
Content string
}
func Index(c *RequestContext) ResponseData { func Index(c *RequestContext) ResponseData {
const maxPosts = 5 const maxPosts = 5
const numProjectsToGet = 7 const numProjectsToGet = 7
@ -116,8 +125,40 @@ func Index(c *RequestContext) ResponseData {
hasRead = true hasRead = true
} }
c.Logger.Debug().Time("post date", projectPost.Post.PostDate).Msg("") featurable := (!proj.IsHMN() &&
projectPost.Cat.Kind == models.CatTypeBlog &&
projectPost.Post.ParentID == nil &&
landingPageProject.FeaturedPost == nil)
if featurable {
type featuredContentResult struct {
Content string `db:"ver.text_parsed"`
}
contentResult, err := db.QueryOne(c.Context(), c.Conn, featuredContentResult{}, `
SELECT $columns
FROM
handmade_post AS post
JOIN handmade_postversion AS ver ON post.current_id = ver.id
WHERE
post.id = $1
`, projectPost.Post.ID)
if err != nil {
panic(err)
}
content := contentResult.(*featuredContentResult).Content
// c.Logger.Debug().Str("content", content).Msg("")
landingPageProject.FeaturedPost = &LandingPageFeaturedPost{
Title: projectPost.Thread.Title,
Url: templates.PostUrl(projectPost.Post, projectPost.Cat.Kind, proj.Subdomain()), // TODO
User: templates.UserToTemplate(&projectPost.User),
Date: projectPost.Post.PostDate,
Unread: !hasRead,
Content: content,
}
} else {
landingPageProject.Posts = append(landingPageProject.Posts, templates.PostListItem{ landingPageProject.Posts = append(landingPageProject.Posts, templates.PostListItem{
Title: projectPost.Thread.Title, Title: projectPost.Thread.Title,
Url: templates.PostUrl(projectPost.Post, projectPost.Cat.Kind, proj.Subdomain()), // TODO Url: templates.PostUrl(projectPost.Post, projectPost.Cat.Kind, proj.Subdomain()), // TODO
@ -126,6 +167,7 @@ func Index(c *RequestContext) ResponseData {
Unread: !hasRead, Unread: !hasRead,
}) })
} }
}
if len(projectPosts) > 0 { if len(projectPosts) > 0 {
pageProjects = append(pageProjects, landingPageProject) pageProjects = append(pageProjects, landingPageProject)
@ -158,13 +200,43 @@ func Index(c *RequestContext) ResponseData {
newsThread := newsThreadRow.(*newsThreadQuery) newsThread := newsThreadRow.(*newsThreadQuery)
_ = newsThread // TODO: NO _ = newsThread // TODO: NO
/*
Columns are filled by placing projects into the least full column.
The fill array tracks the estimated sizes.
This is all hardcoded for two columns; deal with it.
*/
cols := [][]LandingPageProject{nil, nil}
fill := []int{4, 0}
featuredIndex := []int{0, 0}
for _, pageProject := range pageProjects {
leastFullColumnIndex := indexOfSmallestInt(fill)
numNewPosts := len(pageProject.Posts)
if numNewPosts > maxPosts {
numNewPosts = maxPosts
}
fill[leastFullColumnIndex] += numNewPosts
if pageProject.FeaturedPost != nil {
fill[leastFullColumnIndex] += 2 // featured posts add more to height
// projects with featured posts go at the top of the column
cols[leastFullColumnIndex] = append(cols[leastFullColumnIndex], pageProject)
featuredIndex[leastFullColumnIndex] += 1
} else {
cols[leastFullColumnIndex] = append(cols[leastFullColumnIndex], pageProject)
}
}
baseData := getBaseData(c) baseData := getBaseData(c)
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", LandingTemplateData{ err = res.WriteTemplate("index.html", LandingTemplateData{
BaseData: getBaseData(c), BaseData: getBaseData(c),
PostColumns: [][]LandingPageProject{pageProjects}, // TODO: NO PostColumns: cols,
}) })
if err != nil { if err != nil {
panic(err) panic(err)
@ -172,3 +244,17 @@ func Index(c *RequestContext) ResponseData {
return res return res
} }
func indexOfSmallestInt(s []int) int {
result := 0
min := s[result]
for i, val := range s {
if val < min {
result = i
min = val
}
}
return result
}