forked from tildetown/town
retooling review for sql
parent
717c1b93f1
commit
df41bb4df2
|
@ -1,27 +1,23 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.tilde.town/tildetown/town/models"
|
||||
//"git.tilde.town/tildetown/town/review"
|
||||
"git.tilde.town/tildetown/town/signup"
|
||||
tuser "git.tilde.town/tildetown/town/user"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
const (
|
||||
signupDir = "/town/signups"
|
||||
acceptedDir = "/town/signups/accepted"
|
||||
rejectedDir = "/town/signups/rejected"
|
||||
)
|
||||
|
||||
func getTitle() string {
|
||||
titles := []string{
|
||||
"yo bum rush the show",
|
||||
|
@ -34,37 +30,36 @@ func getTitle() string {
|
|||
return titles[rand.Intn(len(titles))]
|
||||
}
|
||||
|
||||
type townSignup struct {
|
||||
When time.Time
|
||||
DecisionTime time.Time
|
||||
Decision string
|
||||
Filename string
|
||||
Answers map[string]string
|
||||
// TODO affordance for cleaning up email response
|
||||
|
||||
type reviewer struct {
|
||||
db *sql.DB
|
||||
adminName string
|
||||
}
|
||||
|
||||
func (s townSignup) Accept() error {
|
||||
return s.review("accept")
|
||||
func newReviewer(db *sql.DB, adminName string) *reviewer {
|
||||
return &reviewer{db: db, adminName: adminName}
|
||||
}
|
||||
|
||||
func (s townSignup) Reject() error {
|
||||
return s.review("reject")
|
||||
}
|
||||
|
||||
func (s townSignup) review(decision string) error {
|
||||
func (r *reviewer) Review(s *models.TownSignup, decision models.SignupDecision) error {
|
||||
s.DecisionTime = time.Now()
|
||||
s.Decision = "decision"
|
||||
oldpath := path.Join(signupDir, s.Filename)
|
||||
newpath := path.Join(acceptedDir, s.Filename)
|
||||
if decision == "reject" {
|
||||
newpath = path.Join(rejectedDir, s.Filename)
|
||||
}
|
||||
return os.Rename(oldpath, newpath)
|
||||
s.Decision = decision
|
||||
s.DecidedBy = r.adminName
|
||||
s.Insert(r.db)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s townSignup) Render() string {
|
||||
out := fmt.Sprintf("[-:-:b]submitted:[-:-:-] %s\n", s.When.Format("2006-01-02 15:04"))
|
||||
func renderSignup(s models.TownSignup) string {
|
||||
out := fmt.Sprintf("[-:-:b]submitted:[-:-:-] %s\n", s.Created.Format("2006-01-02 15:04"))
|
||||
|
||||
for k, v := range s.Answers {
|
||||
pairs := map[string]string{
|
||||
"e-mail": s.Email,
|
||||
"how found / referral": s.How,
|
||||
"why like town / what do": s.Why,
|
||||
"links": s.Links,
|
||||
}
|
||||
|
||||
for k, v := range pairs {
|
||||
out += fmt.Sprintf("[-:-:b]%s[-:-:-]\n", k)
|
||||
out += strings.TrimSpace(v)
|
||||
out += "\n\n"
|
||||
|
@ -73,39 +68,20 @@ func (s townSignup) Render() string {
|
|||
return out
|
||||
}
|
||||
|
||||
func getSignups() ([]townSignup, error) {
|
||||
entries, err := os.ReadDir(signupDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read '%s': %w", signupDir, err)
|
||||
}
|
||||
out := []townSignup{}
|
||||
for _, entry := range entries {
|
||||
if !strings.HasSuffix(entry.Name(), "json") {
|
||||
continue
|
||||
}
|
||||
|
||||
abs := path.Join(signupDir, entry.Name())
|
||||
data, err := os.ReadFile(abs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read signup file '%s': %w", abs, err)
|
||||
}
|
||||
|
||||
fmt.Println(string(data))
|
||||
|
||||
var signup townSignup
|
||||
err = json.Unmarshal(data, &signup)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not unmarshal signup file '%s': %w", abs, err)
|
||||
}
|
||||
signup.Filename = entry.Name()
|
||||
|
||||
out = append(out, signup)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func _main() error {
|
||||
/*
|
||||
TODO will use this for invites
|
||||
userDB, err := review.ConnectDB()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not connect to user database: %w", err)
|
||||
}
|
||||
*/
|
||||
|
||||
signupDB, err := signup.ConnectDB()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not connect to signups database: %w", err)
|
||||
}
|
||||
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
return fmt.Errorf("that's my purse. I don't know you! %w", err)
|
||||
|
@ -118,11 +94,16 @@ func _main() error {
|
|||
if !isAdmin {
|
||||
return errors.New("this command can only be run by a town admin")
|
||||
}
|
||||
|
||||
r := newReviewer(signupDB, u.Username)
|
||||
|
||||
rand.Seed(time.Now().Unix())
|
||||
|
||||
signups, err := getSignups()
|
||||
su := models.TownSignup{}
|
||||
|
||||
signups, err := su.All(signupDB)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get signups: %w", err)
|
||||
return fmt.Errorf("could not fetch signups: %w", err)
|
||||
}
|
||||
|
||||
signupIx := 0
|
||||
|
@ -138,7 +119,7 @@ func _main() error {
|
|||
if len(signups) == 0 {
|
||||
appView.SetText("no signups found.")
|
||||
} else {
|
||||
appView.SetText(signups[signupIx].Render())
|
||||
appView.SetText(renderSignup(*signups[signupIx]))
|
||||
}
|
||||
|
||||
legend := tview.NewTextView()
|
||||
|
@ -176,7 +157,7 @@ func _main() error {
|
|||
notate := tview.NewForm()
|
||||
notate.AddTextArea("note", "", 80, 10, 1000, func(string) {})
|
||||
notate.AddButton("submit", func() {
|
||||
// TODO add note; this will require re-serializing and saving the file
|
||||
// add note and update
|
||||
pages.SwitchToPage("main")
|
||||
})
|
||||
notate.AddButton("cancel", func() {
|
||||
|
@ -200,11 +181,11 @@ func _main() error {
|
|||
if signupIx == len(signups) {
|
||||
signupIx = 0
|
||||
}
|
||||
appView.SetText(signups[signupIx].Render())
|
||||
appView.SetText(renderSignup(*signups[signupIx]))
|
||||
}
|
||||
|
||||
removeSignup := func(signup townSignup) {
|
||||
newSignups := []townSignup{}
|
||||
removeSignup := func(signup *models.TownSignup) {
|
||||
newSignups := []*models.TownSignup{}
|
||||
for ix, s := range signups {
|
||||
if ix != signupIx {
|
||||
newSignups = append(newSignups, s)
|
||||
|
@ -216,7 +197,7 @@ func _main() error {
|
|||
signupIx = 0
|
||||
}
|
||||
}
|
||||
appView.SetText(signups[signupIx].Render())
|
||||
appView.SetText(renderSignup(*signups[signupIx]))
|
||||
}
|
||||
|
||||
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
|
@ -226,29 +207,32 @@ func _main() error {
|
|||
case 'r':
|
||||
if len(signups) > 0 {
|
||||
signupIx = rand.Intn(len(signups))
|
||||
appView.SetText(signups[signupIx].Render())
|
||||
appView.SetText(renderSignup(*signups[signupIx]))
|
||||
}
|
||||
case 'A':
|
||||
if len(signups) == 0 {
|
||||
return nil
|
||||
}
|
||||
// TODO modal for collecting clean email
|
||||
signup := signups[signupIx]
|
||||
err := signup.Accept()
|
||||
err := r.Review(signup, models.SignupAccepted)
|
||||
if err != nil {
|
||||
errorModal.SetText(fmt.Sprintf("error! failed to approve '%s': %s", signup.Filename, err.Error()))
|
||||
errorModal.SetText(fmt.Sprintf("error! failed to approve '%d': %s", signup.ID, err.Error()))
|
||||
pages.SwitchToPage("error")
|
||||
return nil
|
||||
}
|
||||
removeSignup(signup)
|
||||
updateCount()
|
||||
// TODO generate invite token
|
||||
// TODO send invite email
|
||||
case 'R':
|
||||
if len(signups) == 0 {
|
||||
return nil
|
||||
}
|
||||
signup := signups[signupIx]
|
||||
err = signup.Reject()
|
||||
err = r.Review(signup, models.SignupRejected)
|
||||
if err != nil {
|
||||
errorModal.SetText(fmt.Sprintf("error! failed to reject '%s': %s", signup.Filename, err.Error()))
|
||||
errorModal.SetText(fmt.Sprintf("error! failed to reject '%d': %s", signup.ID, err.Error()))
|
||||
pages.SwitchToPage("error")
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package main
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
|
@ -10,9 +11,11 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"git.tilde.town/tildetown/town/models"
|
||||
"git.tilde.town/tildetown/town/signup"
|
||||
"github.com/MakeNowJust/heredoc/v2"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
|
@ -115,7 +118,7 @@ func main() {
|
|||
|
||||
logger := log.New(logF, "", log.Ldate|log.Ltime)
|
||||
|
||||
db, err := signup.NewDB()
|
||||
db, err := signup.ConnectDB()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(2)
|
||||
|
@ -128,7 +131,7 @@ func main() {
|
|||
}
|
||||
}
|
||||
|
||||
func _main(l *log.Logger, db *signup.DB) error {
|
||||
func _main(l *log.Logger, db *sql.DB) error {
|
||||
l.Println("starting a session")
|
||||
pages := tview.NewPages()
|
||||
mainFlex := tview.NewFlex()
|
||||
|
@ -163,11 +166,11 @@ func _main(l *log.Logger, db *signup.DB) error {
|
|||
|
||||
player := newCharacter("you", "TODO")
|
||||
|
||||
su := &signup.TownSignup{ID: -1}
|
||||
su := &models.TownSignup{ID: -1}
|
||||
|
||||
save := func() {
|
||||
su.Created = time.Now()
|
||||
err := db.InsertSignup(su)
|
||||
err := su.Insert(db)
|
||||
|
||||
if err != nil {
|
||||
l.Printf("failed to write to db: %s", err.Error())
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
// shared database related code
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
// TODO this is in flux; might want SignupNote and UserNote structs separately
|
||||
type AdminNote struct {
|
||||
ID int64
|
||||
Admin string
|
||||
Note string
|
||||
When time.Time
|
||||
}
|
||||
|
||||
type SignupDecision string
|
||||
|
||||
const (
|
||||
SignupAccepted SignupDecision = "accepted"
|
||||
SignupRejected SignupDecision = "rejected"
|
||||
)
|
||||
|
||||
// TODO add DecisionTime column to DB
|
||||
// TODO add DecisionBy column to DB
|
||||
// TODO add CleanEmail column to DB
|
||||
type TownSignup struct {
|
||||
ID int64
|
||||
Created time.Time
|
||||
Email string
|
||||
How string
|
||||
Why string
|
||||
Links string
|
||||
|
||||
Notes []AdminNote
|
||||
Decision SignupDecision
|
||||
DecisionTime time.Time
|
||||
DecidedBy string
|
||||
CleanEmail string
|
||||
}
|
||||
|
||||
func (s *TownSignup) Insert(db *sql.DB) error {
|
||||
stmt, err := db.Prepare(`
|
||||
INSERT INTO signups (created, email, how, why, links) VALUES(
|
||||
?, ?, ?, ?, ?
|
||||
) RETURNING id
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := stmt.Exec(s.Created.Unix(), s.Email, s.How, s.Why, s.Links)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer stmt.Close()
|
||||
|
||||
liid, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.ID = liid
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *TownSignup) All(db *sql.DB) ([]*TownSignup, error) {
|
||||
// TODO notes; circle back once can author them
|
||||
rows, err := db.Query(`
|
||||
SELECT
|
||||
id, created, email, how, why, links
|
||||
FROM signups WHERE decision = ""`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := []*TownSignup{}
|
||||
for rows.Next() {
|
||||
su := &TownSignup{}
|
||||
if err = rows.Scan(
|
||||
&su.ID,
|
||||
&su.Created,
|
||||
&su.Email,
|
||||
&su.How,
|
||||
&su.Why,
|
||||
&su.Links,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO fetch notes
|
||||
out = append(out, su)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// below is all TODO and unused rn
|
||||
type UserState string
|
||||
|
||||
const (
|
||||
StateActive = "active"
|
||||
StateTempBan = "temp_banned"
|
||||
StateBan = "banned"
|
||||
)
|
||||
|
||||
type TownAccount struct {
|
||||
ID int64
|
||||
Emails []string
|
||||
Username string
|
||||
Signup int
|
||||
Notes []AdminNote
|
||||
State UserState
|
||||
Admin bool
|
||||
}
|
|
@ -2,92 +2,17 @@ package signup
|
|||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
type AdminNote struct {
|
||||
ID int64
|
||||
Admin string
|
||||
Note string
|
||||
When time.Time
|
||||
}
|
||||
const dsn = "/town/var/signups/signups.db?mode=rw"
|
||||
|
||||
type SignupDecision string
|
||||
type UserState string
|
||||
|
||||
const (
|
||||
SignupAccepted = "accepted"
|
||||
SignupRejected = "rejected"
|
||||
StateActive = "active"
|
||||
StateTempBan = "temp_banned"
|
||||
StateBan = "banned"
|
||||
)
|
||||
|
||||
type TownSignup struct {
|
||||
ID int64
|
||||
Created time.Time
|
||||
Email string
|
||||
How string
|
||||
Why string
|
||||
Links string
|
||||
Notes []AdminNote
|
||||
Decision SignupDecision
|
||||
}
|
||||
|
||||
type TownAccount struct {
|
||||
ID int
|
||||
Emails []string
|
||||
Username string
|
||||
Signup int
|
||||
Notes []AdminNote
|
||||
State UserState
|
||||
Admin bool
|
||||
}
|
||||
|
||||
type DB struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewDB() (*DB, error) {
|
||||
db, err := sql.Open("sqlite3", "/town/var/signups/signups.db?mode=rw")
|
||||
func ConnectDB() (*sql.DB, error) {
|
||||
db, err := sql.Open("sqlite3", dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &DB{
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *DB) InsertSignup(su *TownSignup) error {
|
||||
stmt, err := d.db.Prepare(`
|
||||
INSERT INTO signups (created, email, how, why, links) VALUES(
|
||||
?, ?, ?, ?, ?
|
||||
) RETURNING id
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, err := stmt.Exec(su.Created.Unix(), su.Email, su.How, su.Why, su.Links)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer stmt.Close()
|
||||
|
||||
liid, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
su.ID = liid
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DB) Close() error {
|
||||
return d.db.Close()
|
||||
return db, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
CREATE TABLE IF NOT EXISTS signups (
|
||||
id INTEGER PRIMARY KEY,
|
||||
|
||||
-- from user
|
||||
created TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M', 'now', 'localtime')),
|
||||
email TEXT,
|
||||
how TEXT,
|
||||
why TEXT,
|
||||
links TEXT,
|
||||
|
||||
-- admin provided
|
||||
decision_time TEXT,
|
||||
decision TEXT
|
||||
decided_by TEXT,
|
||||
clean_email TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notes (
|
||||
id INTEGER PRIMARY KEY,
|
||||
signupid INTEGER,
|
||||
author TEXT,
|
||||
content TEXT,
|
||||
created TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M', 'now', 'localtime')),
|
||||
|
||||
FOREIGN KEY (signupid) REFERENCES signups(signupid)
|
||||
);
|
|
@ -1,9 +0,0 @@
|
|||
CREATE TABLE IF NOT EXISTS signups (
|
||||
id INTEGER PRIMARY KEY,
|
||||
created TEXT DEFAULT (strftime('%Y-%m-%dT%H:%M', 'now', 'localtime')),
|
||||
email TEXT,
|
||||
how TEXT,
|
||||
why TEXT,
|
||||
links TEXT,
|
||||
decision TEXT
|
||||
);
|
Loading…
Reference in New Issue