Implement proper model, actions and template structure
This commit is contained in:
parent
f4360b98c8
commit
6fe515ea52
8 changed files with 605 additions and 413 deletions
30
actions.go
Normal file
30
actions.go
Normal file
|
@ -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
|
||||
}
|
489
boardvoting.go
489
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()
|
||||
}
|
||||
|
|
346
models.go
Normal file
346
models.go
Normal file
|
@ -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
|
||||
}
|
4
templates/footer.html
Normal file
4
templates/footer.html
Normal file
|
@ -0,0 +1,4 @@
|
|||
{{ define "footer" }}
|
||||
</body>
|
||||
</html>
|
||||
{{ end }}
|
15
templates/header.html
Normal file
15
templates/header.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
{{ define "pagetitle" }}
|
||||
CAcert Board Decisions{{ if .PageTitle }} - {{ .PageTitle }}{{ end}}
|
||||
{{ end }}
|
||||
|
||||
{{ define "header" -}}
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/html">
|
||||
<head>
|
||||
<title>{{ template "pagetitle" . }}</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
<link rel="stylesheet" type="text/css" href="/static/styles.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{ template "pagetitle" . }}</h1>
|
||||
{{ end }}
|
21
templates/motion.html
Normal file
21
templates/motion.html
Normal file
|
@ -0,0 +1,21 @@
|
|||
{{ template "header" . }}
|
||||
<a href="/motions/">Show all votes</a>
|
||||
{{ $voter := .Voter }}
|
||||
<table class="list">
|
||||
<thead>
|
||||
<th>Status</th>
|
||||
<th>Motion</th>
|
||||
{{ if $voter}}
|
||||
<th>Actions</th>
|
||||
{{ end }}
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
{{ with .Decision }}
|
||||
{{ template "motion_fragment" .}}
|
||||
{{ if $voter }}{{ template "motion_actions" . }}{{ end }}
|
||||
{{ end}}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{ template "footer" . }}
|
43
templates/motion_fragments.html
Normal file
43
templates/motion_fragments.html
Normal file
|
@ -0,0 +1,43 @@
|
|||
{{ define "motion_fragment" }}
|
||||
<td class="{{.Status}}">
|
||||
{{ 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 }}
|
||||
</td>
|
||||
<td>
|
||||
<i><a href="/motions/{{ .Tag}}">{{ .Tag}}</a></i><br/>
|
||||
<b>{{ .Title}}</b><br/>
|
||||
<pre>{{ wrap 76 .Content }}</pre>
|
||||
<br/>
|
||||
<i>Due: {{.Due}}</i><br/>
|
||||
<i>Proposed: {{.Proposer}} ({{.Proposed}})</i><br/>
|
||||
<i>Vote type: {{.VoteType}}</i><br/>
|
||||
<i>Aye|Naye|Abstain: {{.Ayes}}|{{.Nayes}}|{{.Abstains}}</i><br/>
|
||||
{{ if .Votes }}
|
||||
<i>Votes:</i><br/>
|
||||
{{ range .Votes}}
|
||||
<i>{{ .Name }}: {{ .Vote.Vote }}</i><br/>
|
||||
{{ end }}
|
||||
<i><a href="/motions/{{.Tag}}">Hide Votes</a></i>
|
||||
{{ else}}
|
||||
<i><a href="/motions/{{.Tag}}?showvotes=1">Show Votes</a></i>
|
||||
{{ end }}
|
||||
</td>
|
||||
{{ end }}
|
||||
{{ define "motion_actions" }}
|
||||
<td>
|
||||
{{ if eq .Status 0 }}
|
||||
<ul>
|
||||
<li><a href="/vote/{{ .Tag }}/aye">Aye</a></li>
|
||||
<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="/motions/{{ .Tag }}/edit">Modify</a></li>
|
||||
<li><a href="/motions/{{ .Tag }}/withdraw">Withdraw</a></li>
|
||||
</ul>
|
||||
{{ end }}
|
||||
</td>
|
||||
{{ end }}
|
|
@ -1,12 +1,9 @@
|
|||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/html">
|
||||
<head>
|
||||
<title>CAcert Board Decisions</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
<link rel="stylesheet" type="text/css" href="/static/styles.css"/>
|
||||
</head>
|
||||
<body>
|
||||
{{ template "header" . }}
|
||||
{{ if .Params.Flags.Unvoted }}
|
||||
<a href="/motions/">Show all votes</a>
|
||||
{{ else }}
|
||||
<a href="/motions/?unvoted=1">Show my outstanding votes</a><br/>
|
||||
{{ end }}
|
||||
{{ $voter := .Voter }}
|
||||
{{ if .Decisions }}
|
||||
<table class="list">
|
||||
|
@ -20,57 +17,30 @@
|
|||
<tbody>
|
||||
{{range .Decisions }}
|
||||
<tr>
|
||||
<td class="{{.Status}}">
|
||||
{{ 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 }}
|
||||
</td>
|
||||
<td>
|
||||
<i><a href="/motions/{{ .Tag}}">{{ .Tag}}</a></i><br />
|
||||
<b>{{ .Title}}</b><br />
|
||||
<pre>{{ wrap 76 .Content }}</pre>
|
||||
<br />
|
||||
<i>Due: {{.Due}}</i><br/>
|
||||
<i>Proposed: {{.Proposer}} ({{.Proposed}})</i><br/>
|
||||
<i>Vote type: {{.VoteType}}</i><br/>
|
||||
<i>Aye|Naye|Abstain: {{.Ayes}}|{{.Nayes}}|{{.Abstains}}</i><br />
|
||||
{{ if .Votes }}
|
||||
<i>Votes:</i><br/>
|
||||
{{ range .Votes}}
|
||||
<i>{{ .Name }}: {{ .Vote}}</i><br />
|
||||
{{ end }}
|
||||
{{ else}}
|
||||
<i><a href="/motions/{{.Tag}}?showvotes=1">Show Votes</a></i>
|
||||
{{ end }}
|
||||
</td>
|
||||
{{ if $voter }}
|
||||
<td>
|
||||
{{ if eq .Status 0 }}
|
||||
<ul>
|
||||
<li><a href="/vote/{{ .Tag }}/aye">Aye</a></li>
|
||||
<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="/motions/{{ .Tag }}/edit">Modify</a></li>
|
||||
<li><a href="/motions/{{ .Tag }}/withdraw">Withdraw</a></li>
|
||||
</ul>
|
||||
{{ end }}
|
||||
</td>{{ end }}
|
||||
{{ template "motion_fragment" . }}
|
||||
{{ if $voter }}{{ template "motion_actions" . }}{{ end }}
|
||||
</tr>
|
||||
{{end}}
|
||||
<tr>
|
||||
<td colspan="{{ if $voter }}3{{ else }}2{{ end }}" class="navigation">
|
||||
<td colspan="2" class="navigation">
|
||||
{{ if .PrevPage }}<a href="?page={{ .PrevPage }}" title="previous page"><</a>{{ end }}
|
||||
{{ if .NextPage }}<a href="?page={{ .NextPage }}" title="next page">></a>{{ end }}
|
||||
</td>
|
||||
{{ if $voter }}
|
||||
<td class="actions">
|
||||
<ul>
|
||||
<li><a href="/newmotion/">New Motion</a></li>
|
||||
</ul>
|
||||
</td>
|
||||
{{ end }}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
{{ if .Params.Flags.Unvoted }}
|
||||
<p>There are no motions requiring a vote from you.</p>
|
||||
{{ else }}
|
||||
<p>There are no motions in the system yet.</p>
|
||||
{{ end }}
|
||||
{{end}}
|
||||
</body>
|
||||
</html>
|
||||
{{ template "footer" . }}
|
Loading…
Reference in a new issue