Implement proper model, actions and template structure

debian
Jan Dittberner 8 years ago committed by Jan Dittberner
parent f4360b98c8
commit 6fe515ea52

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

@ -3,7 +3,6 @@ package main
import ( import (
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"database/sql"
"fmt" "fmt"
"github.com/Masterminds/sprig" "github.com/Masterminds/sprig"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
@ -16,227 +15,21 @@ import (
"os" "os"
"strconv" "strconv"
"strings" "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 logger *log.Logger
var config *Config
const ( func getTemplateFilenames(tmpl []string) (result []string) {
voteAye = 1 result = make([]string, len(tmpl))
voteNaye = -1 for i := range tmpl {
voteAbstain = 0 result[i] = fmt.Sprintf("templates/%s", tmpl[i])
)
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)
} }
t.Execute(os.Stdout, context) return result
} }
func authenticateVoter(emailAddress string) *Voter { func renderTemplate(w http.ResponseWriter, tmpl []string, context interface{}) {
if err := db.Ping(); err != nil { t := template.Must(template.New(tmpl[0]).Funcs(sprig.FuncMap()).ParseFiles(getTemplateFilenames(tmpl)...))
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)
}
if err := t.Execute(w, context); err != nil { if err := t.Execute(w, context); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) 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 { for _, extKeyUsage := range cert.ExtKeyUsage {
if extKeyUsage == x509.ExtKeyUsageClientAuth { if extKeyUsage == x509.ExtKeyUsageClientAuth {
for _, emailAddress := range cert.EmailAddresses { 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) handler(w, r, voter)
return return
} }
@ -257,7 +55,7 @@ func authenticateRequest(w http.ResponseWriter, r *http.Request, authRequired bo
} }
if authRequired { if authRequired {
w.WriteHeader(http.StatusForbidden) w.WriteHeader(http.StatusForbidden)
renderTemplate(w, "denied", nil) renderTemplate(w, []string{"denied.html"}, nil)
return return
} }
handler(w, r, nil) handler(w, r, nil)
@ -268,7 +66,7 @@ type motionParameters struct {
} }
type motionListParameters struct { type motionListParameters struct {
Page int64 Page int64
Flags struct { Flags struct {
Confirmed, Withdraw, Unvoted bool Confirmed, Withdraw, Unvoted bool
} }
@ -301,144 +99,133 @@ func parseMotionListParameters(r *http.Request) motionListParameters {
func motionListHandler(w http.ResponseWriter, r *http.Request, voter *Voter) { func motionListHandler(w http.ResponseWriter, r *http.Request, voter *Voter) {
params := parseMotionListParameters(r) 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 { var context struct {
Decisions []Decision Decisions []*DecisionForDisplay
Voter *Voter Voter *Voter
Params *motionListParameters Params *motionListParameters
PrevPage, NextPage int64 PrevPage, NextPage int64
PageTitle string
} }
context.Voter = voter context.Voter = voter
context.Params = &params context.Params = &params
var err error
motion_stmt, err := db.Preparex(sqlGetDecisions) if params.Flags.Unvoted {
if err != nil { if context.Decisions, err = FindVotersUnvotedDecisionsForDisplayOnPage(params.Page, voter); err != nil {
logger.Fatal(err) 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 err != nil {
rows.Close()
logger.Fatal(err)
}
voteRows, err := votes_stmt.Queryx(d.Id)
if err != nil {
rows.Close()
logger.Fatal(err)
} }
} else {
for voteRows.Next() { if context.Decisions, err = FindDecisionsForDisplayOnPage(params.Page); err != nil {
var vote int http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
var count int return
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( if len(context.Decisions) > 0 {
context.Decisions[0].Proposed, olderExists, err := context.Decisions[len(context.Decisions)-1].OlderExists()
context.Decisions[len(context.Decisions)-1].Proposed) if err != nil {
if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
logger.Fatal(err) return
}
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 context.NextPage = params.Page + 1
} }
} }
if params.Page > 1 { if params.Page > 1 {
context.PrevPage = 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) params := parseMotionParameters(r)
var context struct { var context struct {
Decisions []Decision Decision *DecisionForDisplay
Voter *Voter Voter *Voter
Params *motionParameters Params *motionParameters
PrevPage, NextPage int64 PrevPage, NextPage int64
PageTitle string
} }
context.Voter = voter context.Voter = voter
context.Params = &params context.Params = &params
context.Decisions = append(context.Decisions, *decision) if params.ShowVotes {
renderTemplate(w, "motions", context) 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)) { func singleDecisionHandler(w http.ResponseWriter, r *http.Request, v *Voter, tag string, handler func(http.ResponseWriter, *http.Request, *Voter, *DecisionForDisplay)) {
votes_stmt, err := db.Preparex(sqlVoteCount) decision, err := FindDecisionForDisplayByTag(tag)
if err != nil {
logger.Fatal(err)
}
defer votes_stmt.Close()
motion_stmt, err := db.Preparex(sqlGetDecision)
if err != nil { if err != nil {
logger.Fatal(err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
} return
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 decision == nil {
if err != nil { http.NotFound(w, r)
logger.Fatal(err) return
} }
handler(w, r, v, decision)
}
for voteRows.Next() { type motionsHandler struct{}
var vote, count int
err = voteRows.Scan(&vote, &count) type motionActionHandler interface {
if err != nil { Handle(w http.ResponseWriter, r *http.Request, voter *Voter, decision *DecisionForDisplay)
voteRows.Close() NeedsAuth() bool
logger.Fatal(err) }
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 { if err := db.Ping(); err != nil {
logger.Fatal(err) logger.Fatal(err)
} }
subURL := r.URL.Path[len("/motions/"):] subURL := r.URL.Path
var motionActionMap = map[string]motionActionHandler{
"withdraw": withDrawMotionAction{},
"edit": editMotionAction{},
}
switch { switch {
case subURL == "": case subURL == "":
@ -447,9 +234,16 @@ func motionsHandler(w http.ResponseWriter, r *http.Request) {
case strings.Count(subURL, "/") == 1: case strings.Count(subURL, "/") == 1:
parts := strings.Split(subURL, "/") parts := strings.Split(subURL, "/")
logger.Printf("handle %v\n", parts) logger.Printf("handle %v\n", parts)
fmt.Fprintf(w, "No handler for '%s'", subURL)
motionTag := parts[0] 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) logger.Printf("motion: %s, action: %s\n", motionTag, action)
return return
case strings.Count(subURL, "/") == 0: 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) { func newMotionHandler(w http.ResponseWriter, _ *http.Request, _ *Voter) {
err := db.Ping() fmt.Fprintln(w,"New motion")
if err != nil { // TODO: implement
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)
}
} }
type Config struct { type Config struct {
@ -517,7 +281,6 @@ func main() {
var err error var err error
var config Config
var source []byte var source []byte
source, err = ioutil.ReadFile(filename) source, err = ioutil.ReadFile(filename)
@ -534,13 +297,14 @@ func main() {
if err != nil { if err != nil {
logger.Fatal(err) logger.Fatal(err)
} }
defer db.Close()
http.HandleFunc("/motions/", motionsHandler) http.Handle("/motions/", http.StripPrefix("/motions/", motionsHandler{}))
http.HandleFunc("/voters/", func(w http.ResponseWriter, r *http.Request) { http.HandleFunc("/newmotion/", func(w http.ResponseWriter, r *http.Request) {
authenticateRequest(w, r, true, votersHandler) authenticateRequest(w, r, true, newMotionHandler)
}) })
http.Handle("/static/", http.FileServer(http.Dir("."))) http.Handle("/static/", http.FileServer(http.Dir(".")))
http.HandleFunc("/", redirectToMotionsHandler) http.Handle("/", http.RedirectHandler("/motions/", http.StatusMovedPermanently))
// load CA certificates for client authentication // load CA certificates for client authentication
caCert, err := ioutil.ReadFile(config.ClientCACertificates) caCert, err := ioutil.ReadFile(config.ClientCACertificates)
@ -564,10 +328,9 @@ func main() {
TLSConfig: tlsConfig, TLSConfig: tlsConfig,
} }
err = server.ListenAndServeTLS(config.ServerCert, config.ServerKey) logger.Printf("Launching application on https://localhost%s/\n", server.Addr)
if err != nil {
if err = server.ListenAndServeTLS(config.ServerCert, config.ServerKey); err != nil {
logger.Fatal("ListenAndServerTLS: ", err) logger.Fatal("ListenAndServerTLS: ", err)
} }
defer db.Close()
} }

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

@ -0,0 +1,4 @@
{{ define "footer" }}
</body>
</html>
{{ end }}

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

@ -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" . }}

@ -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> {{ template "header" . }}
<html xmlns="http://www.w3.org/1999/html"> {{ if .Params.Flags.Unvoted }}
<head> <a href="/motions/">Show all votes</a>
<title>CAcert Board Decisions</title> {{ else }}
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<link rel="stylesheet" type="text/css" href="/static/styles.css"/>
</head>
<body>
<a href="/motions/?unvoted=1">Show my outstanding votes</a><br/> <a href="/motions/?unvoted=1">Show my outstanding votes</a><br/>
{{ end }}
{{ $voter := .Voter }} {{ $voter := .Voter }}
{{ if .Decisions }} {{ if .Decisions }}
<table class="list"> <table class="list">
@ -20,57 +17,30 @@
<tbody> <tbody>
{{range .Decisions }} {{range .Decisions }}
<tr> <tr>
<td class="{{.Status}}"> {{ template "motion_fragment" . }}
{{ if eq .Status 0 }}Pending {{ .Due}} {{ if $voter }}{{ template "motion_actions" . }}{{ end }}
{{ 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 }}
</tr> </tr>
{{end}} {{end}}
<tr> <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">&lt;</a>{{ end }} {{ if .PrevPage }}<a href="?page={{ .PrevPage }}" title="previous page">&lt;</a>{{ end }}
{{ if .NextPage }}<a href="?page={{ .NextPage }}" title="next page">&gt;</a>{{ end }} {{ if .NextPage }}<a href="?page={{ .NextPage }}" title="next page">&gt;</a>{{ end }}
</td> </td>
{{ if $voter }}
<td class="actions">
<ul>
<li><a href="/newmotion/">New Motion</a></li>
</ul>
</td>
{{ end }}
</tr> </tr>
</tbody> </tbody>
</table> </table>
{{else}} {{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> <p>There are no motions in the system yet.</p>
{{ end }}
{{end}} {{end}}
</body> {{ template "footer" . }}
</html>
Loading…
Cancel
Save