summaryrefslogtreecommitdiff
path: root/boardvoting.go
diff options
context:
space:
mode:
Diffstat (limited to 'boardvoting.go')
-rw-r--r--boardvoting.go389
1 files changed, 285 insertions, 104 deletions
diff --git a/boardvoting.go b/boardvoting.go
index 68210d9..a49b34e 100644
--- a/boardvoting.go
+++ b/boardvoting.go
@@ -1,25 +1,26 @@
package main
import (
+ "crypto/tls"
+ "crypto/x509"
+ "database/sql"
"fmt"
- "log"
- "strings"
- "net/http"
- "io/ioutil"
- "time"
+ "github.com/Masterminds/sprig"
+ "github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
"gopkg.in/yaml.v2"
- "github.com/jmoiron/sqlx"
- "github.com/Masterminds/sprig"
- "os"
- "crypto/x509"
- "crypto/tls"
- "database/sql"
"html/template"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "os"
+ "strconv"
+ "strings"
+ "time"
)
const (
- list_decisions_sql = `
+ sqlGetDecisions = `
SELECT decisions.id, decisions.tag, decisions.proponent,
voters.name AS proposer, decisions.proposed, decisions.title,
decisions.content, decisions.votetype, decisions.status, decisions.due,
@@ -27,54 +28,63 @@ SELECT decisions.id, decisions.tag, decisions.proponent,
FROM decisions
JOIN voters ON decisions.proponent=voters.id
ORDER BY proposed DESC
-LIMIT 10 OFFSET 10 * ($1 - 1)`
- get_decision_sql = `
+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.id=$1;`
- get_voter = `
+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`
- vote_count_sql = `
+ sqlVoteCount = `
SELECT vote, COUNT(vote)
FROM votes
-WHERE decision=$1`
+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
const (
- voteAye = 1
- voteNaye = -1
+ voteAye = 1
+ voteNaye = -1
voteAbstain = 0
)
const (
voteTypeMotion = 0
- voteTypeVeto = 1
+ voteTypeVeto = 1
)
type VoteType int
func (v VoteType) String() string {
switch v {
- case voteTypeMotion: return "motion"
- case voteTypeVeto: return "veto"
- default: return "unknown"
+ 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
+ case voteTypeMotion:
+ return 3, 50
+ default:
+ return 1, 99
}
}
@@ -92,11 +102,16 @@ 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"
+ case -1:
+ return "declined"
+ case 0:
+ return "pending"
+ case 1:
+ return "approved"
+ case -2:
+ return "withdrawn"
+ default:
+ return "unknown"
}
}
@@ -104,10 +119,14 @@ 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"
+ case voteAye:
+ return "Aye"
+ case voteNaye:
+ return "Naye"
+ case voteAbstain:
+ return "Abstain"
+ default:
+ return "unknown"
}
}
@@ -131,7 +150,7 @@ type Decision struct {
Due time.Time
Modified time.Time
VoteSums
- Votes []Vote
+ Votes []Vote
}
func (d *Decision) parseVote(vote int, count int) {
@@ -150,134 +169,298 @@ type Voter struct {
Name string
}
-func authenticateVoter(emailAddress string, voter *Voter) bool {
+func withDrawMotion(tag string, voter *Voter, config *Config) {
err := db.Ping()
if err != nil {
logger.Fatal(err)
}
- auth_stmt, err := db.Preparex(get_voter)
+ decision_stmt, err := db.Preparex(sqlGetDecision)
if err != nil {
logger.Fatal(err)
}
- defer auth_stmt.Close()
- var found = false
- err = auth_stmt.Get(voter, emailAddress)
+ defer decision_stmt.Close()
+
+ var d Decision
+ err = decision_stmt.Get(&d, tag)
if err == nil {
- found = true
- } else {
+ 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.Fatal(err)
+ logger.Println("Problem getting voter", err)
}
+ return nil
}
- return found
+ return voter
}
func redirectToMotionsHandler(w http.ResponseWriter, _ *http.Request) {
- w.Header().Add("Location", "/motions")
+ w.Header().Set("Location", "/motions/")
w.WriteHeader(http.StatusMovedPermanently)
}
func renderTemplate(w http.ResponseWriter, tmpl string, context interface{}) {
- t := template.New("motions.html")
- t.Funcs(sprig.FuncMap())
- t, err := t.ParseFiles(fmt.Sprintf("templates/%s.html", tmpl))
+ 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)
}
- err = t.Execute(w, context)
- if err != nil {
+ if err := t.Execute(w, context); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
-func authenticateRequest(
-w http.ResponseWriter, r *http.Request,
-handler func(http.ResponseWriter, *http.Request, *Voter)) {
- var voter Voter
- var found = false
- authLoop: for _, cert := range r.TLS.PeerCertificates {
- var isClientCert = false
+func authenticateRequest(w http.ResponseWriter, r *http.Request, authRequired bool, handler func(http.ResponseWriter, *http.Request, *Voter)) {
+ for _, cert := range r.TLS.PeerCertificates {
for _, extKeyUsage := range cert.ExtKeyUsage {
if extKeyUsage == x509.ExtKeyUsageClientAuth {
- isClientCert = true
- break
- }
- }
- if !isClientCert {
- continue
- }
-
- for _, emailAddress := range cert.EmailAddresses {
- if authenticateVoter(emailAddress, &voter) {
- found = true
- break authLoop
+ for _, emailAddress := range cert.EmailAddresses {
+ if voter := authenticateVoter(emailAddress); voter != nil {
+ handler(w, r, voter)
+ return
+ }
+ }
}
}
}
- if !found {
+ if authRequired {
w.WriteHeader(http.StatusForbidden)
renderTemplate(w, "denied", nil)
return
}
- handler(w, r, &voter)
+ handler(w, r, nil)
}
-func motionsHandler(w http.ResponseWriter, _ *http.Request, voter *Voter) {
- err := db.Ping()
- if err != nil {
- logger.Fatal(err)
+type motionParameters struct {
+ ShowVotes bool
+}
+
+type motionListParameters struct {
+ Page int64
+ Flags struct {
+ Confirmed, Withdraw, Unvoted bool
}
+}
- // $page = is_numeric($_REQUEST['page'])?$_REQUEST['page']:1;
+func parseMotionParameters(r *http.Request) motionParameters {
+ var m = motionParameters{}
+ m.ShowVotes, _ = strconv.ParseBool(r.URL.Query().Get("showvotes"))
+ logger.Printf("parsed parameters: %+v\n", m)
+ return m
+}
- motion_stmt, err := db.Preparex(list_decisions_sql)
- votes_stmt, err := db.Preparex(vote_count_sql)
+func parseMotionListParameters(r *http.Request) motionListParameters {
+ var m = motionListParameters{}
+ if page, err := strconv.ParseInt(r.URL.Query().Get("page"), 10, 0); err != nil {
+ m.Page = 1
+ } else {
+ m.Page = page
+ }
+ m.Flags.Withdraw, _ = strconv.ParseBool(r.URL.Query().Get("withdraw"))
+ m.Flags.Unvoted, _ = strconv.ParseBool(r.URL.Query().Get("unvoted"))
+
+ if r.Method == http.MethodPost {
+ m.Flags.Confirmed, _ = strconv.ParseBool(r.PostFormValue("confirm"))
+ }
+ logger.Printf("parsed parameters: %+v\n", m)
+ return m
+}
+
+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 motion_stmt.Close()
defer votes_stmt.Close()
-
- rows, err := motion_stmt.Queryx(1)
+ beforeAfterStmt, err := db.Preparex(sqlCountNewerOlderThanMotion)
if err != nil {
logger.Fatal(err)
}
- defer rows.Close()
+ defer beforeAfterStmt.Close()
- var page struct {
- Decisions []Decision
- Voter *Voter
+ var context struct {
+ Decisions []Decision
+ Voter *Voter
+ Params *motionListParameters
+ PrevPage, NextPage int64
}
- page.Voter = voter
+ context.Voter = voter
+ context.Params = &params
+ 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)
}
for voteRows.Next() {
- var vote, count int
- err = voteRows.Scan(&vote, &count)
- if err != nil {
+ var vote int
+ var count int
+ if err := voteRows.Scan(&vote, &count); err != nil {
voteRows.Close()
- logger.Fatal(err)
+ logger.Fatalf("Error fetching counts for motion %s: %s", d.Tag, err)
}
d.parseVote(vote, count)
}
- page.Decisions = append(page.Decisions, d)
+ 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 {
+ context.NextPage = params.Page + 1
+ }
+ }
+ if params.Page > 1 {
+ context.PrevPage = params.Page - 1
+ }
- renderTemplate(w, "motions", page)
+ renderTemplate(w, "motions", context)
+}
+
+func motionHandler(w http.ResponseWriter, r *http.Request, voter *Voter, decision *Decision) {
+ params := parseMotionParameters(r)
+
+ var context struct {
+ Decisions []Decision
+ Voter *Voter
+ Params *motionParameters
+ PrevPage, NextPage int64
+ }
+ context.Voter = voter
+ context.Params = &params
+ 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)
+ }
+ d.parseVote(vote, count)
+ }
+ voteRows.Close()
+
+ handler(w, r, v, d)
+}
+
+func motionsHandler(w http.ResponseWriter, r *http.Request) {
+ if err := db.Ping(); err != nil {
+ logger.Fatal(err)
+ }
+
+ subURL := r.URL.Path[len("/motions/"):]
+
+ switch {
+ case subURL == "":
+ authenticateRequest(w, r, false, motionListHandler)
+ return
+ 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]
+ logger.Printf("motion: %s, action: %s\n", motionTag, action)
+ return
+ case strings.Count(subURL, "/") == 0:
+ authenticateRequest(w, r, false, func(w http.ResponseWriter, r *http.Request, v *Voter) {
+ singleDecisionHandler(w, r, v, subURL, motionHandler)
+ })
+ return
+ default:
+ fmt.Fprintf(w, "No handler for '%s'", subURL)
+ return
+ }
}
func votersHandler(w http.ResponseWriter, _ *http.Request, voter *Voter) {
@@ -325,7 +508,7 @@ type Config struct {
}
func main() {
- logger = log.New(os.Stderr, "boardvoting: ", log.LstdFlags | log.LUTC)
+ logger = log.New(os.Stderr, "boardvoting: ", log.LstdFlags|log.LUTC|log.Lshortfile)
var filename = "config.yaml"
if len(os.Args) == 2 {
@@ -352,13 +535,11 @@ func main() {
logger.Fatal(err)
}
- http.HandleFunc("/motions", func(w http.ResponseWriter, r *http.Request) {
- authenticateRequest(w, r, motionsHandler)
- })
- http.HandleFunc("/voters", func(w http.ResponseWriter, r *http.Request) {
- authenticateRequest(w, r, votersHandler)
+ http.HandleFunc("/motions/", motionsHandler)
+ http.HandleFunc("/voters/", func(w http.ResponseWriter, r *http.Request) {
+ authenticateRequest(w, r, true, votersHandler)
})
- http.HandleFunc("/static/", http.FileServer(http.Dir(".")).ServeHTTP)
+ http.Handle("/static/", http.FileServer(http.Dir(".")))
http.HandleFunc("/", redirectToMotionsHandler)
// load CA certificates for client authentication
@@ -373,14 +554,14 @@ func main() {
// setup HTTPS server
tlsConfig := &tls.Config{
- ClientCAs:caCertPool,
- ClientAuth:tls.RequireAndVerifyClientCert,
+ ClientCAs: caCertPool,
+ ClientAuth: tls.RequireAndVerifyClientCert,
}
tlsConfig.BuildNameToCertificate()
server := &http.Server{
- Addr: ":8443",
- TLSConfig:tlsConfig,
+ Addr: ":8443",
+ TLSConfig: tlsConfig,
}
err = server.ListenAndServeTLS(config.ServerCert, config.ServerKey)