Implement more RESTful URLs for motions

This commit implements URLs /motions/ and /motions/{:tag} handlers.
debian
Jan Dittberner 7 years ago
parent 74987ce184
commit f4360b98c8

@ -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)

@ -6,14 +6,15 @@
<link rel="stylesheet" type="text/css" href="/static/styles.css"/>
</head>
<body>
<a href="?unvoted=1">Show my outstanding votes</a><br/>
<a href="/motions/?unvoted=1">Show my outstanding votes</a><br/>
{{ $voter := .Voter }}
{{ if .Decisions }}
<table class="list">
<thead>
<tr>
<th>Status</th>
<th>Motion</th>
<th>Actions</th>
{{ if $voter }}<th>Actions</th>{{ end }}
</tr>
</thead>
<tbody>
@ -28,7 +29,7 @@
{{ end }}
</td>
<td>
<i><a href="/motions?motion={{ .Tag}}">{{ .Tag}}</a></i><br />
<i><a href="/motions/{{ .Tag}}">{{ .Tag}}</a></i><br />
<b>{{ .Title}}</b><br />
<pre>{{ wrap 76 .Content }}</pre>
<br />
@ -42,9 +43,10 @@
<i>{{ .Name }}: {{ .Vote}}</i><br />
{{ end }}
{{ else}}
<i><a href="/motions?motion={{.Tag}}&showvotes=1">Show Votes</a></i>
<i><a href="/motions/{{.Tag}}?showvotes=1">Show Votes</a></i>
{{ end }}
</td>
{{ if $voter }}
<td>
{{ if eq .Status 0 }}
<ul>
@ -52,17 +54,23 @@
<li><a href="/vote/{{ .Tag }}/abstain">Abstain</a></li>
<li><a href="/vote/{{ .Tag }}/naye">Naye</a></li>
<li><a href="/proxy/{{ .Tag }}">Proxy Vote</a></li>
<li><a href="/motion/{{ .Tag }}">Modify</a></li>
<li><a href="/motions?motion={{ .Tag }}&withdraw=1">Withdraw</a></li>
<li><a href="/motions/{{ .Tag }}/edit">Modify</a></li>
<li><a href="/motions/{{ .Tag }}/withdraw">Withdraw</a></li>
</ul>
{{ end }}
</td>
</td>{{ end }}
</tr>
{{end}}
<tr>
<td colspan="{{ if $voter }}3{{ else }}2{{ end }}" class="navigation">
{{ if .PrevPage }}<a href="?page={{ .PrevPage }}" title="previous page">&lt;</a>{{ end }}
{{ if .NextPage }}<a href="?page={{ .NextPage }}" title="next page">&gt;</a>{{ end }}
</td>
</tr>
</tbody>
</table>
{{else}}
<p>There are no motions in the system yet.</p>
{{end}}
</body>
</html>
</html>

@ -0,0 +1,14 @@
From: {{ .Sender }}
To: {{ .Recipient }}
Subject: Re: {{ .Tag }} - {{ .Title }} - withdrawn
Dear Board,
{{ .Name }} has withdrawn the motion {{ .Tag }} that was as follows:
{{ .Title }}
{{ wrap 76 .Content }}
Kind regards,
the voting system
Loading…
Cancel
Save