From 6fe515ea52493ea79a07efe9e1fc652dea272e32 Mon Sep 17 00:00:00 2001 From: Jan Dittberner Date: Mon, 17 Apr 2017 16:24:37 +0200 Subject: [PATCH] Implement proper model, actions and template structure --- actions.go | 30 ++ boardvoting.go | 489 ++++++++------------------------ models.go | 346 ++++++++++++++++++++++ templates/footer.html | 4 + templates/header.html | 15 + templates/motion.html | 21 ++ templates/motion_fragments.html | 43 +++ templates/motions.html | 70 ++--- 8 files changed, 605 insertions(+), 413 deletions(-) create mode 100644 actions.go create mode 100644 models.go create mode 100644 templates/footer.html create mode 100644 templates/header.html create mode 100644 templates/motion.html create mode 100644 templates/motion_fragments.html diff --git a/actions.go b/actions.go new file mode 100644 index 0000000..fde767c --- /dev/null +++ b/actions.go @@ -0,0 +1,30 @@ +package main + +import ( + "github.com/Masterminds/sprig" + "os" + "text/template" +) + +func WithdrawMotion(decision *Decision, voter *Voter) (err error) { + // load template, fill name, tag, title, content + type mailContext struct { + *Decision + Name string + Sender string + Recipient string + } + context := mailContext{decision, voter.Name, config.NoticeSenderAddress, config.BoardMailAddress} + + // fill withdraw_mail.txt + t, err := template.New("withdraw_mail.txt").Funcs( + sprig.GenericFuncMap()).ParseFiles("templates/withdraw_mail.txt") + if err != nil { + logger.Fatal(err) + } + // TODO: send mail + t.Execute(os.Stdout, context) + + // TODO: implement call decision.Close() + return +} diff --git a/boardvoting.go b/boardvoting.go index a49b34e..77499a2 100644 --- a/boardvoting.go +++ b/boardvoting.go @@ -3,7 +3,6 @@ package main import ( "crypto/tls" "crypto/x509" - "database/sql" "fmt" "github.com/Masterminds/sprig" "github.com/jmoiron/sqlx" @@ -16,227 +15,21 @@ import ( "os" "strconv" "strings" - "time" ) -const ( - sqlGetDecisions = ` -SELECT decisions.id, decisions.tag, decisions.proponent, - voters.name AS proposer, decisions.proposed, decisions.title, - decisions.content, decisions.votetype, decisions.status, decisions.due, - decisions.modified -FROM decisions -JOIN voters ON decisions.proponent=voters.id -ORDER BY proposed DESC -LIMIT 10 OFFSET 10 * $1` - sqlGetDecision = ` -SELECT decisions.id, decisions.tag, decisions.proponent, - voters.name AS proposer, decisions.proposed, decisions.title, - decisions.content, decisions.votetype, decisions.status, decisions.due, - decisions.modified -FROM decisions -JOIN voters ON decisions.proponent=voters.id -WHERE decisions.tag=$1;` - sqlGetVoter = ` -SELECT voters.id, voters.name -FROM voters -JOIN emails ON voters.id=emails.voter -WHERE emails.address=$1 AND voters.enabled=1` - sqlVoteCount = ` -SELECT vote, COUNT(vote) -FROM votes -WHERE decision=$1 GROUP BY vote` - sqlCountNewerOlderThanMotion = ` -SELECT "newer" AS label, COUNT(*) AS value FROM decisions WHERE proposed > $1 -UNION -SELECT "older", COUNT(*) FROM decisions WHERE proposed < $2` -) - -var db *sqlx.DB var logger *log.Logger +var config *Config -const ( - voteAye = 1 - voteNaye = -1 - voteAbstain = 0 -) - -const ( - voteTypeMotion = 0 - voteTypeVeto = 1 -) - -type VoteType int - -func (v VoteType) String() string { - switch v { - case voteTypeMotion: - return "motion" - case voteTypeVeto: - return "veto" - default: - return "unknown" +func getTemplateFilenames(tmpl []string) (result []string) { + result = make([]string, len(tmpl)) + for i := range tmpl { + result[i] = fmt.Sprintf("templates/%s", tmpl[i]) } + return result } -func (v VoteType) QuorumAndMajority() (int, int) { - switch v { - case voteTypeMotion: - return 3, 50 - default: - return 1, 99 - } -} - -type VoteSums struct { - Ayes int - Nayes int - Abstains int -} - -func (v *VoteSums) voteCount() int { - return v.Ayes + v.Nayes + v.Abstains -} - -type VoteStatus int - -func (v VoteStatus) String() string { - switch v { - case -1: - return "declined" - case 0: - return "pending" - case 1: - return "approved" - case -2: - return "withdrawn" - default: - return "unknown" - } -} - -type VoteKind int - -func (v VoteKind) String() string { - switch v { - case voteAye: - return "Aye" - case voteNaye: - return "Naye" - case voteAbstain: - return "Abstain" - default: - return "unknown" - } -} - -type Vote struct { - Name string - Vote VoteKind -} - -type Decision struct { - Id int - Tag string - Proponent int - Proposer string - Proposed time.Time - Title string - Content string - Majority int - Quorum int - VoteType VoteType - Status VoteStatus - Due time.Time - Modified time.Time - VoteSums - Votes []Vote -} - -func (d *Decision) parseVote(vote int, count int) { - switch vote { - case voteAye: - d.Ayes = count - case voteAbstain: - d.Abstains = count - case voteNaye: - d.Nayes = count - } -} - -type Voter struct { - Id int - Name string -} - -func withDrawMotion(tag string, voter *Voter, config *Config) { - err := db.Ping() - if err != nil { - logger.Fatal(err) - } - - decision_stmt, err := db.Preparex(sqlGetDecision) - if err != nil { - logger.Fatal(err) - } - defer decision_stmt.Close() - - var d Decision - err = decision_stmt.Get(&d, tag) - if err == nil { - logger.Println(d) - } - - type MailContext struct { - Decision - Name string - Sender string - Recipient string - } - - context := MailContext{d, voter.Name, config.NoticeSenderAddress, config.BoardMailAddress} - - // TODO: implement - // fill withdraw_mail.txt - t, err := template.New("withdraw_mail.txt").Funcs(sprig.FuncMap()).ParseFiles("templates/withdraw_mail.txt") - if err != nil { - logger.Fatal(err) - } - t.Execute(os.Stdout, context) -} - -func authenticateVoter(emailAddress string) *Voter { - if err := db.Ping(); err != nil { - logger.Fatal(err) - } - - auth_stmt, err := db.Preparex(sqlGetVoter) - if err != nil { - logger.Println("Problem getting voter", err) - return nil - } - defer auth_stmt.Close() - - var voter = &Voter{} - if err = auth_stmt.Get(voter, emailAddress); err != nil { - if err != sql.ErrNoRows { - logger.Println("Problem getting voter", err) - } - return nil - } - return voter -} - -func redirectToMotionsHandler(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Location", "/motions/") - w.WriteHeader(http.StatusMovedPermanently) -} - -func renderTemplate(w http.ResponseWriter, tmpl string, context interface{}) { - t, err := template.New(fmt.Sprintf("%s.html", tmpl)).Funcs(sprig.FuncMap()).ParseFiles(fmt.Sprintf("templates/%s.html", tmpl)) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } +func renderTemplate(w http.ResponseWriter, tmpl []string, context interface{}) { + t := template.Must(template.New(tmpl[0]).Funcs(sprig.FuncMap()).ParseFiles(getTemplateFilenames(tmpl)...)) if err := t.Execute(w, context); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } @@ -247,7 +40,12 @@ func authenticateRequest(w http.ResponseWriter, r *http.Request, authRequired bo for _, extKeyUsage := range cert.ExtKeyUsage { if extKeyUsage == x509.ExtKeyUsageClientAuth { for _, emailAddress := range cert.EmailAddresses { - if voter := authenticateVoter(emailAddress); voter != nil { + voter, err := FindVoterByAddress(emailAddress) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + if voter != nil { handler(w, r, voter) return } @@ -257,7 +55,7 @@ func authenticateRequest(w http.ResponseWriter, r *http.Request, authRequired bo } if authRequired { w.WriteHeader(http.StatusForbidden) - renderTemplate(w, "denied", nil) + renderTemplate(w, []string{"denied.html"}, nil) return } handler(w, r, nil) @@ -268,7 +66,7 @@ type motionParameters struct { } type motionListParameters struct { - Page int64 + Page int64 Flags struct { Confirmed, Withdraw, Unvoted bool } @@ -301,144 +99,133 @@ func parseMotionListParameters(r *http.Request) motionListParameters { func motionListHandler(w http.ResponseWriter, r *http.Request, voter *Voter) { params := parseMotionListParameters(r) - votes_stmt, err := db.Preparex(sqlVoteCount) - if err != nil { - logger.Fatal(err) - } - defer votes_stmt.Close() - beforeAfterStmt, err := db.Preparex(sqlCountNewerOlderThanMotion) - if err != nil { - logger.Fatal(err) - } - defer beforeAfterStmt.Close() - var context struct { - Decisions []Decision + Decisions []*DecisionForDisplay Voter *Voter Params *motionListParameters PrevPage, NextPage int64 + PageTitle string } context.Voter = voter context.Params = ¶ms + var err error - motion_stmt, err := db.Preparex(sqlGetDecisions) - if err != nil { - logger.Fatal(err) + if params.Flags.Unvoted { + if context.Decisions, err = FindVotersUnvotedDecisionsForDisplayOnPage(params.Page, voter); err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + } else { + if context.Decisions, err = FindDecisionsForDisplayOnPage(params.Page); err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } } - defer motion_stmt.Close() - rows, err := motion_stmt.Queryx(params.Page - 1) - if err != nil { - logger.Fatal(err) - } - for rows.Next() { - var d Decision - err := rows.StructScan(&d) + + if len(context.Decisions) > 0 { + olderExists, err := context.Decisions[len(context.Decisions)-1].OlderExists() if err != nil { - rows.Close() - logger.Fatal(err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return } - - voteRows, err := votes_stmt.Queryx(d.Id) - if err != nil { - rows.Close() - logger.Fatal(err) - } - - for voteRows.Next() { - var vote int - var count int - if err := voteRows.Scan(&vote, &count); err != nil { - voteRows.Close() - logger.Fatalf("Error fetching counts for motion %s: %s", d.Tag, err) - } - d.parseVote(vote, count) - } - context.Decisions = append(context.Decisions, d) - - voteRows.Close() - } - rows.Close() - rows, err = beforeAfterStmt.Queryx( - context.Decisions[0].Proposed, - context.Decisions[len(context.Decisions)-1].Proposed) - if err != nil { - logger.Fatal(err) - } - defer rows.Close() - for rows.Next() { - var key string - var value int - if err := rows.Scan(&key, &value); err != nil { - rows.Close() - logger.Fatal(err) - } - if key == "older" && value > 0 { + if olderExists { context.NextPage = params.Page + 1 } } + if params.Page > 1 { context.PrevPage = params.Page - 1 } - renderTemplate(w, "motions", context) + renderTemplate(w, []string{"motions.html", "motion_fragments.html", "header.html", "footer.html"}, context) } -func motionHandler(w http.ResponseWriter, r *http.Request, voter *Voter, decision *Decision) { +func motionHandler(w http.ResponseWriter, r *http.Request, voter *Voter, decision *DecisionForDisplay) { params := parseMotionParameters(r) var context struct { - Decisions []Decision + Decision *DecisionForDisplay Voter *Voter Params *motionParameters PrevPage, NextPage int64 + PageTitle string } context.Voter = voter context.Params = ¶ms - context.Decisions = append(context.Decisions, *decision) - renderTemplate(w, "motions", context) -} - -func singleDecisionHandler(w http.ResponseWriter, r *http.Request, v *Voter, tag string, handler func(http.ResponseWriter, *http.Request, *Voter, *Decision)) { - votes_stmt, err := db.Preparex(sqlVoteCount) - if err != nil { - logger.Fatal(err) - } - defer votes_stmt.Close() - motion_stmt, err := db.Preparex(sqlGetDecision) - if err != nil { - logger.Fatal(err) - } - defer motion_stmt.Close() - var d *Decision = &Decision{} - err = motion_stmt.Get(d, tag) - if err != nil { - logger.Fatal(err) - } - voteRows, err := votes_stmt.Queryx(d.Id) - if err != nil { - logger.Fatal(err) - } - - for voteRows.Next() { - var vote, count int - err = voteRows.Scan(&vote, &count) - if err != nil { - voteRows.Close() - logger.Fatal(err) + if params.ShowVotes { + if err := decision.LoadVotes(); err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return } - d.parseVote(vote, count) } - voteRows.Close() - - handler(w, r, v, d) + context.Decision = decision + context.PageTitle = fmt.Sprintf("Motion %s: %s", decision.Tag, decision.Title) + renderTemplate(w, []string{"motion.html", "motion_fragments.html", "header.html", "footer.html"}, context) } -func motionsHandler(w http.ResponseWriter, r *http.Request) { +func singleDecisionHandler(w http.ResponseWriter, r *http.Request, v *Voter, tag string, handler func(http.ResponseWriter, *http.Request, *Voter, *DecisionForDisplay)) { + decision, err := FindDecisionForDisplayByTag(tag) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + if decision == nil { + http.NotFound(w, r) + return + } + handler(w, r, v, decision) +} + +type motionsHandler struct{} + +type motionActionHandler interface { + Handle(w http.ResponseWriter, r *http.Request, voter *Voter, decision *DecisionForDisplay) + NeedsAuth() bool +} + +type withDrawMotionAction struct{} + +func (withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request, voter *Voter, decision *DecisionForDisplay) { + fmt.Fprintln(w, "Withdraw motion", decision.Tag) + // TODO: implement + if r.Method == http.MethodPost { + if confirm, err := strconv.ParseBool(r.PostFormValue("confirm")); err != nil { + log.Println("could not parse confirm parameter:", err) + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + } else if confirm { + WithdrawMotion(&decision.Decision, voter) + } else { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + } + } +} + +func (withDrawMotionAction) NeedsAuth() bool { + return true +} + +type editMotionAction struct{} + +func (editMotionAction) Handle(w http.ResponseWriter, r *http.Request, voter *Voter, decision *DecisionForDisplay) { + fmt.Fprintln(w, "Edit motion", decision.Tag) + // TODO: implement +} + +func (editMotionAction) NeedsAuth() bool { + return true +} + +func (h motionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if err := db.Ping(); err != nil { logger.Fatal(err) } - subURL := r.URL.Path[len("/motions/"):] + subURL := r.URL.Path + + var motionActionMap = map[string]motionActionHandler{ + "withdraw": withDrawMotionAction{}, + "edit": editMotionAction{}, + } switch { case subURL == "": @@ -447,9 +234,16 @@ func motionsHandler(w http.ResponseWriter, r *http.Request) { case strings.Count(subURL, "/") == 1: parts := strings.Split(subURL, "/") logger.Printf("handle %v\n", parts) - fmt.Fprintf(w, "No handler for '%s'", subURL) motionTag := parts[0] - action := parts[1] + action, ok := motionActionMap[parts[1]] + if !ok { + http.NotFound(w, r) + return + } + authenticateRequest(w, r, action.NeedsAuth(), func(w http.ResponseWriter, r *http.Request, v *Voter) { + singleDecisionHandler(w, r, v, motionTag, action.Handle) + }) + logger.Printf("motion: %s, action: %s\n", motionTag, action) return case strings.Count(subURL, "/") == 0: @@ -463,39 +257,9 @@ func motionsHandler(w http.ResponseWriter, r *http.Request) { } } -func votersHandler(w http.ResponseWriter, _ *http.Request, voter *Voter) { - err := db.Ping() - if err != nil { - logger.Fatal(err) - } - - fmt.Fprintln(w, "Hello", voter.Name) - - sqlStmt := "SELECT name, reminder FROM voters WHERE enabled=1" - - rows, err := db.Query(sqlStmt) - if err != nil { - logger.Fatal(err) - } - defer rows.Close() - - fmt.Print("Enabled voters\n\n") - fmt.Printf("%-30s %-30s\n", "Name", "Reminder E-Mail address") - fmt.Printf("%s %s\n", strings.Repeat("-", 30), strings.Repeat("-", 30)) - for rows.Next() { - var name string - var reminder string - - err = rows.Scan(&name, &reminder) - if err != nil { - logger.Fatal(err) - } - fmt.Printf("%-30s %s\n", name, reminder) - } - err = rows.Err() - if err != nil { - logger.Fatal(err) - } +func newMotionHandler(w http.ResponseWriter, _ *http.Request, _ *Voter) { + fmt.Fprintln(w,"New motion") + // TODO: implement } type Config struct { @@ -517,7 +281,6 @@ func main() { var err error - var config Config var source []byte source, err = ioutil.ReadFile(filename) @@ -534,13 +297,14 @@ func main() { if err != nil { logger.Fatal(err) } + defer db.Close() - http.HandleFunc("/motions/", motionsHandler) - http.HandleFunc("/voters/", func(w http.ResponseWriter, r *http.Request) { - authenticateRequest(w, r, true, votersHandler) + http.Handle("/motions/", http.StripPrefix("/motions/", motionsHandler{})) + http.HandleFunc("/newmotion/", func(w http.ResponseWriter, r *http.Request) { + authenticateRequest(w, r, true, newMotionHandler) }) http.Handle("/static/", http.FileServer(http.Dir("."))) - http.HandleFunc("/", redirectToMotionsHandler) + http.Handle("/", http.RedirectHandler("/motions/", http.StatusMovedPermanently)) // load CA certificates for client authentication caCert, err := ioutil.ReadFile(config.ClientCACertificates) @@ -564,10 +328,9 @@ func main() { TLSConfig: tlsConfig, } - err = server.ListenAndServeTLS(config.ServerCert, config.ServerKey) - if err != nil { + logger.Printf("Launching application on https://localhost%s/\n", server.Addr) + + if err = server.ListenAndServeTLS(config.ServerCert, config.ServerKey); err != nil { logger.Fatal("ListenAndServerTLS: ", err) } - - defer db.Close() } diff --git a/models.go b/models.go new file mode 100644 index 0000000..fe6cd23 --- /dev/null +++ b/models.go @@ -0,0 +1,346 @@ +package main + +import ( + "database/sql" + "github.com/jmoiron/sqlx" + "time" +) + +const ( + sqlGetDecisions = ` +SELECT decisions.id, decisions.tag, decisions.proponent, + voters.name AS proposer, decisions.proposed, decisions.title, + decisions.content, decisions.votetype, decisions.status, decisions.due, + decisions.modified +FROM decisions +JOIN voters ON decisions.proponent=voters.id +ORDER BY proposed DESC +LIMIT 10 OFFSET 10 * $1` + sqlGetDecision = ` +SELECT decisions.id, decisions.tag, decisions.proponent, + voters.name AS proposer, decisions.proposed, decisions.title, + decisions.content, decisions.votetype, decisions.status, decisions.due, + decisions.modified +FROM decisions +JOIN voters ON decisions.proponent=voters.id +WHERE decisions.tag=$1;` + sqlGetVoter = ` +SELECT voters.id, voters.name +FROM voters +JOIN emails ON voters.id=emails.voter +WHERE emails.address=$1 AND voters.enabled=1` + sqlVoteCount = ` +SELECT vote, COUNT(vote) +FROM votes +WHERE decision=$1 GROUP BY vote` + sqlCountOlderThanDecision = ` +SELECT COUNT(*) > 0 FROM decisions WHERE proposed < $1` + sqlGetVotesForDecision = ` +SELECT votes.decision, votes.voter, voters.name, votes.vote, votes.voted, votes.notes +FROM votes +JOIN voters ON votes.voter=voters.id +WHERE decision=$1` + sqlListUnvotedDecisions = ` +SELECT decisions.id, decisions.tag, decisions.proponent, + voters.name AS proposer, decisions.proposed, decisions.title, + decisions.content AS content, decisions.votetype, decisions.status, decisions.due, + decisions.modified +FROM decisions +JOIN voters ON decisions.proponent=voters.id +WHERE decisions.status=0 AND decisions.id NOT IN ( + SELECT decision FROM votes WHERE votes.voter=$2) +ORDER BY proposed DESC +LIMIT 10 OFFSET 10 * $1` +) + +var db *sqlx.DB + +type VoteType int +type VoteStatus int + +type Decision struct { + Id int + Proposed time.Time + ProponentId int `db:"proponent"` + Title string + Content string + Quorum int + Majority int + Status VoteStatus + Due time.Time + Modified time.Time + Tag string + VoteType VoteType +} + +type Email struct { + VoterId int `db:"voter"` + Address string +} + +type Voter struct { + Id int + Name string + Enabled bool + Reminder string // reminder email address +} + +type VoteChoice int + +type Vote struct { + DecisionId int `db:"decision"` + VoterId int `db:"voter"` + Vote VoteChoice + Voted time.Time + Notes string +} + +const ( + voteAye = 1 + voteNaye = -1 + voteAbstain = 0 +) + +const ( + voteTypeMotion = 0 + voteTypeVeto = 1 +) + +func (v VoteType) String() string { + switch v { + case voteTypeMotion: + return "motion" + case voteTypeVeto: + return "veto" + default: + return "unknown" + } +} + +func (v VoteType) QuorumAndMajority() (int, int) { + switch v { + case voteTypeMotion: + return 3, 50 + default: + return 1, 99 + } +} + +func (v VoteChoice) String() string { + switch v { + case voteAye: + return "aye" + case voteNaye: + return "naye" + case voteAbstain: + return "abstain" + default: + return "unknown" + } +} + +func (v VoteStatus) String() string { + switch v { + case -1: + return "declined" + case 0: + return "pending" + case 1: + return "approved" + case -2: + return "withdrawn" + default: + return "unknown" + } +} + +type VoteSums struct { + Ayes int + Nayes int + Abstains int +} + +func (v *VoteSums) VoteCount() int { + return v.Ayes + v.Nayes + v.Abstains +} + +type VoteForDisplay struct { + Vote + Name string +} + +type DecisionForDisplay struct { + Decision + Proposer string `db:"proposer"` + *VoteSums + Votes []VoteForDisplay +} + +func FindDecisionForDisplayByTag(tag string) (decision *DecisionForDisplay, err error) { + decisionStmt, err := db.Preparex(sqlGetDecision) + if err != nil { + logger.Println("Error preparing statement:", err) + return + } + defer decisionStmt.Close() + + decision = &DecisionForDisplay{} + if err = decisionStmt.Get(decision, tag); err != nil { + if err == sql.ErrNoRows { + decision = nil + err = nil + } else { + logger.Printf("Error getting motion %s: %v\n", tag, err) + } + } + decision.VoteSums, err = decision.Decision.VoteSums() + return +} + +// FindDecisionsForDisplayOnPage loads a set of decisions from the database. +// +// This function uses OFFSET for pagination which is not a good idea for larger data sets. +// +// TODO: migrate to timestamp base pagination +func FindDecisionsForDisplayOnPage(page int64) (decisions []*DecisionForDisplay, err error) { + decisionsStmt, err := db.Preparex(sqlGetDecisions) + if err != nil { + logger.Println("Error preparing statement:", err) + return + } + defer decisionsStmt.Close() + + rows, err := decisionsStmt.Queryx(page - 1) + if err != nil { + logger.Printf("Error loading motions for page %d: %v\n", page, err) + return + } + defer rows.Close() + + for rows.Next() { + var d DecisionForDisplay + if err = rows.StructScan(&d); err != nil { + logger.Printf("Error loading motions for page %d: %v\n", page, err) + return + } + d.VoteSums, err = d.Decision.VoteSums() + if err != nil { + return + } + decisions = append(decisions, &d) + } + return +} + +func FindVotersUnvotedDecisionsForDisplayOnPage(page int64, voter *Voter) (decisions []*DecisionForDisplay, err error) { + decisionsStmt, err := db.Preparex(sqlListUnvotedDecisions) + if err != nil { + logger.Println("Error preparing statement:", err) + return + } + defer decisionsStmt.Close() + + rows, err := decisionsStmt.Queryx(page - 1, voter.Id) + if err != nil { + logger.Printf("Error loading motions for page %d: %v\n", page, err) + return + } + defer rows.Close() + + for rows.Next() { + var d DecisionForDisplay + if err = rows.StructScan(&d); err != nil { + logger.Printf("Error loading motions for page %d: %v\n", page, err) + return + } + d.VoteSums, err = d.Decision.VoteSums() + if err != nil { + return + } + decisions = append(decisions, &d) + } + return +} + +func (d *Decision) VoteSums() (sums *VoteSums, err error) { + votesStmt, err := db.Preparex(sqlVoteCount) + if err != nil { + logger.Println("Error preparing statement:", err) + return + } + defer votesStmt.Close() + + voteRows, err := votesStmt.Queryx(d.Id) + if err != nil { + logger.Printf("Error fetching vote sums for motion %s: %v\n", d.Tag, err) + return + } + defer voteRows.Close() + + sums = &VoteSums{} + for voteRows.Next() { + var vote VoteChoice + var count int + if err = voteRows.Scan(&vote, &count); err != nil { + logger.Printf("Error fetching vote sums for motion %s: %v\n", d.Tag, err) + return + } + switch vote { + case voteAye: + sums.Ayes = count + case voteNaye: + sums.Nayes = count + case voteAbstain: + sums.Abstains = count + } + } + return +} + +func (d *DecisionForDisplay) LoadVotes() (err error) { + votesStmt, err := db.Preparex(sqlGetVotesForDecision) + if err != nil { + logger.Println("Error preparing statement:", err) + return + } + defer votesStmt.Close() + err = votesStmt.Select(&d.Votes, d.Id) + if err != nil { + logger.Printf("Error selecting votes for motion %s: %v\n", d.Tag, err) + } + return +} + +func (d *Decision) OlderExists() (result bool, err error) { + olderStmt, err := db.Preparex(sqlCountOlderThanDecision) + if err != nil { + logger.Println("Error preparing statement:", err) + return + } + defer olderStmt.Close() + + if err := olderStmt.Get(&result, d.Proposed); err != nil { + logger.Printf("Error finding older motions than %s: %v\n", d.Tag, err) + } + return +} + +func FindVoterByAddress(emailAddress string) (voter *Voter, err error) { + findVoterStmt, err := db.Preparex(sqlGetVoter) + if err != nil { + logger.Println("Error preparing statement:", err) + return + } + defer findVoterStmt.Close() + + voter = &Voter{} + if err = findVoterStmt.Get(voter, emailAddress); err != nil { + if err != sql.ErrNoRows { + logger.Printf("Error getting voter for address %s: %v\n", emailAddress, err) + } else { + err = nil + voter = nil + } + } + return +} diff --git a/templates/footer.html b/templates/footer.html new file mode 100644 index 0000000..a7086ad --- /dev/null +++ b/templates/footer.html @@ -0,0 +1,4 @@ +{{ define "footer" }} + + +{{ end }} \ No newline at end of file diff --git a/templates/header.html b/templates/header.html new file mode 100644 index 0000000..5f777bb --- /dev/null +++ b/templates/header.html @@ -0,0 +1,15 @@ +{{ define "pagetitle" }} +CAcert Board Decisions{{ if .PageTitle }} - {{ .PageTitle }}{{ end}} +{{ end }} + +{{ define "header" -}} + + + + {{ template "pagetitle" . }} + + + + +

{{ template "pagetitle" . }}

+{{ end }} \ No newline at end of file diff --git a/templates/motion.html b/templates/motion.html new file mode 100644 index 0000000..66e4ce0 --- /dev/null +++ b/templates/motion.html @@ -0,0 +1,21 @@ +{{ template "header" . }} +Show all votes +{{ $voter := .Voter }} + + + + + {{ if $voter}} + + {{ end }} + + + + {{ with .Decision }} + {{ template "motion_fragment" .}} + {{ if $voter }}{{ template "motion_actions" . }}{{ end }} + {{ end}} + + +
StatusMotionActions
+{{ template "footer" . }} \ No newline at end of file diff --git a/templates/motion_fragments.html b/templates/motion_fragments.html new file mode 100644 index 0000000..38c7a01 --- /dev/null +++ b/templates/motion_fragments.html @@ -0,0 +1,43 @@ +{{ define "motion_fragment" }} + + {{ if eq .Status 0 }}Pending {{ .Due}} + {{ else if eq .Status 1}}Approved {{ .Modified}} + {{ else if eq .Status -1}}Declined {{ .Modified}} + {{ else if eq .Status -2}}Withdrawn {{ .Modified}} + {{ else }}Unknown + {{ end }} + + + {{ .Tag}}
+ {{ .Title}}
+
{{ wrap 76 .Content }}
+
+ Due: {{.Due}}
+ Proposed: {{.Proposer}} ({{.Proposed}})
+ Vote type: {{.VoteType}}
+ Aye|Naye|Abstain: {{.Ayes}}|{{.Nayes}}|{{.Abstains}}
+ {{ if .Votes }} + Votes:
+ {{ range .Votes}} + {{ .Name }}: {{ .Vote.Vote }}
+ {{ end }} + Hide Votes + {{ else}} + Show Votes + {{ end }} + +{{ end }} +{{ define "motion_actions" }} + + {{ if eq .Status 0 }} + + {{ end }} + +{{ end }} diff --git a/templates/motions.html b/templates/motions.html index f717d78..ef84757 100644 --- a/templates/motions.html +++ b/templates/motions.html @@ -1,12 +1,9 @@ - - - - CAcert Board Decisions - - - - +{{ template "header" . }} +{{ if .Params.Flags.Unvoted }} +Show all votes +{{ else }} Show my outstanding votes
+{{ end }} {{ $voter := .Voter }} {{ if .Decisions }} @@ -20,57 +17,30 @@ {{range .Decisions }} - - - {{ if $voter }} - {{ end }} + {{ template "motion_fragment" . }} + {{ if $voter }}{{ template "motion_actions" . }}{{ end }} {{end}} - + {{ if $voter }} + + {{ end }}
- {{ if eq .Status 0 }}Pending {{ .Due}} - {{ else if eq .Status 1}}Approved {{ .Modified}} - {{ else if eq .Status -1}}Declined {{ .Modified}} - {{ else if eq .Status -2}}Withdrawn {{ .Modified}} - {{ else }}Unknown - {{ end }} - - {{ .Tag}}
- {{ .Title}}
-
{{ wrap 76 .Content }}
-
- Due: {{.Due}}
- Proposed: {{.Proposer}} ({{.Proposed}})
- Vote type: {{.VoteType}}
- Aye|Naye|Abstain: {{.Ayes}}|{{.Nayes}}|{{.Abstains}}
- {{ if .Votes }} - Votes:
- {{ range .Votes}} - {{ .Name }}: {{ .Vote}}
- {{ end }} - {{ else}} - Show Votes - {{ end }} -
- {{ if eq .Status 0 }} - - {{ end }} -
+ +
{{else}} +{{ if .Params.Flags.Unvoted }} +

There are no motions requiring a vote from you.

+{{ else }}

There are no motions in the system yet.

+{{ end }} {{end}} - - +{{ template "footer" . }} \ No newline at end of file