retooling review for sql
This commit is contained in:
		
							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()) | ||||
|  | ||||
							
								
								
									
										119
									
								
								models/models.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								models/models.go
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||
| } | ||||
|  | ||||
							
								
								
									
										26
									
								
								sql/create_signups_db.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								sql/create_signups_db.sql
									
									
									
									
									
										Normal file
									
								
							| @ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user