summaryrefslogtreecommitdiff
path: root/boardvoting.go
diff options
context:
space:
mode:
Diffstat (limited to 'boardvoting.go')
-rw-r--r--boardvoting.go473
1 files changed, 118 insertions, 355 deletions
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 (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)
+func getTemplateFilenames(tmpl []string) (result []string) {
+ result = make([]string, len(tmpl))
+ for i := range tmpl {
+ result[i] = fmt.Sprintf("templates/%s", tmpl[i])
}
- t.Execute(os.Stdout, context)
+ return result
}
-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 = &params
+ var err error
- motion_stmt, err := db.Preparex(sqlGetDecisions)
- if err != nil {
- logger.Fatal(err)
- }
- 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 err != nil {
- rows.Close()
- logger.Fatal(err)
- }
-
- voteRows, err := votes_stmt.Queryx(d.Id)
- if err != nil {
- rows.Close()
- 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
}
-
- 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)
+ } else {
+ if context.Decisions, err = FindDecisionsForDisplayOnPage(params.Page); err != nil {
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ return
}
- 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 len(context.Decisions) > 0 {
+ olderExists, err := context.Decisions[len(context.Decisions)-1].OlderExists()
+ if err != nil {
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ return
}
- 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 = &params
- context.Decisions = append(context.Decisions, *decision)
- renderTemplate(w, "motions", context)
+ if params.ShowVotes {
+ if err := decision.LoadVotes(); err != nil {
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ return
+ }
+ }
+ 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 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)
+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 {
- logger.Fatal(err)
- }
- defer motion_stmt.Close()
- var d *Decision = &Decision{}
- err = motion_stmt.Get(d, tag)
- if err != nil {
- logger.Fatal(err)
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ return
}
- voteRows, err := votes_stmt.Queryx(d.Id)
- if err != nil {
- logger.Fatal(err)
+ if decision == nil {
+ http.NotFound(w, r)
+ return
}
+ handler(w, r, v, decision)
+}
- for voteRows.Next() {
- var vote, count int
- err = voteRows.Scan(&vote, &count)
- if err != nil {
- voteRows.Close()
- logger.Fatal(err)
+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)
}
- d.parseVote(vote, count)
}
- voteRows.Close()
+}
+
+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
+}
- handler(w, r, v, d)
+func (editMotionAction) NeedsAuth() bool {
+ return true
}
-func motionsHandler(w http.ResponseWriter, r *http.Request) {
+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()
}