forked from tildetown/town
retooling review for sql
parent
717c1b93f1
commit
df41bb4df2
|
@ -1,27 +1,23 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
"path"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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"
|
tuser "git.tilde.town/tildetown/town/user"
|
||||||
"github.com/gdamore/tcell/v2"
|
"github.com/gdamore/tcell/v2"
|
||||||
"github.com/rivo/tview"
|
"github.com/rivo/tview"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
signupDir = "/town/signups"
|
|
||||||
acceptedDir = "/town/signups/accepted"
|
|
||||||
rejectedDir = "/town/signups/rejected"
|
|
||||||
)
|
|
||||||
|
|
||||||
func getTitle() string {
|
func getTitle() string {
|
||||||
titles := []string{
|
titles := []string{
|
||||||
"yo bum rush the show",
|
"yo bum rush the show",
|
||||||
|
@ -34,37 +30,36 @@ func getTitle() string {
|
||||||
return titles[rand.Intn(len(titles))]
|
return titles[rand.Intn(len(titles))]
|
||||||
}
|
}
|
||||||
|
|
||||||
type townSignup struct {
|
// TODO affordance for cleaning up email response
|
||||||
When time.Time
|
|
||||||
DecisionTime time.Time
|
type reviewer struct {
|
||||||
Decision string
|
db *sql.DB
|
||||||
Filename string
|
adminName string
|
||||||
Answers map[string]string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s townSignup) Accept() error {
|
func newReviewer(db *sql.DB, adminName string) *reviewer {
|
||||||
return s.review("accept")
|
return &reviewer{db: db, adminName: adminName}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s townSignup) Reject() error {
|
func (r *reviewer) Review(s *models.TownSignup, decision models.SignupDecision) error {
|
||||||
return s.review("reject")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s townSignup) review(decision string) error {
|
|
||||||
s.DecisionTime = time.Now()
|
s.DecisionTime = time.Now()
|
||||||
s.Decision = "decision"
|
s.Decision = decision
|
||||||
oldpath := path.Join(signupDir, s.Filename)
|
s.DecidedBy = r.adminName
|
||||||
newpath := path.Join(acceptedDir, s.Filename)
|
s.Insert(r.db)
|
||||||
if decision == "reject" {
|
return nil
|
||||||
newpath = path.Join(rejectedDir, s.Filename)
|
|
||||||
}
|
|
||||||
return os.Rename(oldpath, newpath)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s townSignup) Render() string {
|
func renderSignup(s models.TownSignup) string {
|
||||||
out := fmt.Sprintf("[-:-:b]submitted:[-:-:-] %s\n", s.When.Format("2006-01-02 15:04"))
|
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 += fmt.Sprintf("[-:-:b]%s[-:-:-]\n", k)
|
||||||
out += strings.TrimSpace(v)
|
out += strings.TrimSpace(v)
|
||||||
out += "\n\n"
|
out += "\n\n"
|
||||||
|
@ -73,39 +68,20 @@ func (s townSignup) Render() string {
|
||||||
return out
|
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 {
|
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()
|
u, err := user.Current()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("that's my purse. I don't know you! %w", err)
|
return fmt.Errorf("that's my purse. I don't know you! %w", err)
|
||||||
|
@ -118,11 +94,16 @@ func _main() error {
|
||||||
if !isAdmin {
|
if !isAdmin {
|
||||||
return errors.New("this command can only be run by a town admin")
|
return errors.New("this command can only be run by a town admin")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r := newReviewer(signupDB, u.Username)
|
||||||
|
|
||||||
rand.Seed(time.Now().Unix())
|
rand.Seed(time.Now().Unix())
|
||||||
|
|
||||||
signups, err := getSignups()
|
su := models.TownSignup{}
|
||||||
|
|
||||||
|
signups, err := su.All(signupDB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not get signups: %w", err)
|
return fmt.Errorf("could not fetch signups: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
signupIx := 0
|
signupIx := 0
|
||||||
|
@ -138,7 +119,7 @@ func _main() error {
|
||||||
if len(signups) == 0 {
|
if len(signups) == 0 {
|
||||||
appView.SetText("no signups found.")
|
appView.SetText("no signups found.")
|
||||||
} else {
|
} else {
|
||||||
appView.SetText(signups[signupIx].Render())
|
appView.SetText(renderSignup(*signups[signupIx]))
|
||||||
}
|
}
|
||||||
|
|
||||||
legend := tview.NewTextView()
|
legend := tview.NewTextView()
|
||||||
|
@ -176,7 +157,7 @@ func _main() error {
|
||||||
notate := tview.NewForm()
|
notate := tview.NewForm()
|
||||||
notate.AddTextArea("note", "", 80, 10, 1000, func(string) {})
|
notate.AddTextArea("note", "", 80, 10, 1000, func(string) {})
|
||||||
notate.AddButton("submit", func() {
|
notate.AddButton("submit", func() {
|
||||||
// TODO add note; this will require re-serializing and saving the file
|
// add note and update
|
||||||
pages.SwitchToPage("main")
|
pages.SwitchToPage("main")
|
||||||
})
|
})
|
||||||
notate.AddButton("cancel", func() {
|
notate.AddButton("cancel", func() {
|
||||||
|
@ -200,11 +181,11 @@ func _main() error {
|
||||||
if signupIx == len(signups) {
|
if signupIx == len(signups) {
|
||||||
signupIx = 0
|
signupIx = 0
|
||||||
}
|
}
|
||||||
appView.SetText(signups[signupIx].Render())
|
appView.SetText(renderSignup(*signups[signupIx]))
|
||||||
}
|
}
|
||||||
|
|
||||||
removeSignup := func(signup townSignup) {
|
removeSignup := func(signup *models.TownSignup) {
|
||||||
newSignups := []townSignup{}
|
newSignups := []*models.TownSignup{}
|
||||||
for ix, s := range signups {
|
for ix, s := range signups {
|
||||||
if ix != signupIx {
|
if ix != signupIx {
|
||||||
newSignups = append(newSignups, s)
|
newSignups = append(newSignups, s)
|
||||||
|
@ -216,7 +197,7 @@ func _main() error {
|
||||||
signupIx = 0
|
signupIx = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
appView.SetText(signups[signupIx].Render())
|
appView.SetText(renderSignup(*signups[signupIx]))
|
||||||
}
|
}
|
||||||
|
|
||||||
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
|
@ -226,29 +207,32 @@ func _main() error {
|
||||||
case 'r':
|
case 'r':
|
||||||
if len(signups) > 0 {
|
if len(signups) > 0 {
|
||||||
signupIx = rand.Intn(len(signups))
|
signupIx = rand.Intn(len(signups))
|
||||||
appView.SetText(signups[signupIx].Render())
|
appView.SetText(renderSignup(*signups[signupIx]))
|
||||||
}
|
}
|
||||||
case 'A':
|
case 'A':
|
||||||
if len(signups) == 0 {
|
if len(signups) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
// TODO modal for collecting clean email
|
||||||
signup := signups[signupIx]
|
signup := signups[signupIx]
|
||||||
err := signup.Accept()
|
err := r.Review(signup, models.SignupAccepted)
|
||||||
if err != nil {
|
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")
|
pages.SwitchToPage("error")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
removeSignup(signup)
|
removeSignup(signup)
|
||||||
updateCount()
|
updateCount()
|
||||||
|
// TODO generate invite token
|
||||||
|
// TODO send invite email
|
||||||
case 'R':
|
case 'R':
|
||||||
if len(signups) == 0 {
|
if len(signups) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
signup := signups[signupIx]
|
signup := signups[signupIx]
|
||||||
err = signup.Reject()
|
err = r.Review(signup, models.SignupRejected)
|
||||||
if err != nil {
|
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")
|
pages.SwitchToPage("error")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
@ -10,9 +11,11 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.tilde.town/tildetown/town/models"
|
||||||
"git.tilde.town/tildetown/town/signup"
|
"git.tilde.town/tildetown/town/signup"
|
||||||
"github.com/MakeNowJust/heredoc/v2"
|
"github.com/MakeNowJust/heredoc/v2"
|
||||||
"github.com/gdamore/tcell/v2"
|
"github.com/gdamore/tcell/v2"
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
"github.com/rivo/tview"
|
"github.com/rivo/tview"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -115,7 +118,7 @@ func main() {
|
||||||
|
|
||||||
logger := log.New(logF, "", log.Ldate|log.Ltime)
|
logger := log.New(logF, "", log.Ldate|log.Ltime)
|
||||||
|
|
||||||
db, err := signup.NewDB()
|
db, err := signup.ConnectDB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
os.Exit(2)
|
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")
|
l.Println("starting a session")
|
||||||
pages := tview.NewPages()
|
pages := tview.NewPages()
|
||||||
mainFlex := tview.NewFlex()
|
mainFlex := tview.NewFlex()
|
||||||
|
@ -163,11 +166,11 @@ func _main(l *log.Logger, db *signup.DB) error {
|
||||||
|
|
||||||
player := newCharacter("you", "TODO")
|
player := newCharacter("you", "TODO")
|
||||||
|
|
||||||
su := &signup.TownSignup{ID: -1}
|
su := &models.TownSignup{ID: -1}
|
||||||
|
|
||||||
save := func() {
|
save := func() {
|
||||||
su.Created = time.Now()
|
su.Created = time.Now()
|
||||||
err := db.InsertSignup(su)
|
err := su.Insert(db)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Printf("failed to write to db: %s", err.Error())
|
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 (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"time"
|
|
||||||
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AdminNote struct {
|
const dsn = "/town/var/signups/signups.db?mode=rw"
|
||||||
ID int64
|
|
||||||
Admin string
|
|
||||||
Note string
|
|
||||||
When time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type SignupDecision string
|
func ConnectDB() (*sql.DB, error) {
|
||||||
type UserState string
|
db, err := sql.Open("sqlite3", dsn)
|
||||||
|
|
||||||
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")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &DB{
|
return db, nil
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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