diff --git a/actions.go b/actions.go
index 190314c..0619587 100644
--- a/actions.go
+++ b/actions.go
@@ -1,6 +1,7 @@
package main
import (
+ "fmt"
"github.com/Masterminds/sprig"
"os"
"text/template"
@@ -14,7 +15,29 @@ func CreateMotion(decision *Decision, voter *Voter) (err error) {
return
}
- // TODO: implement fetching new decision, implement mail
+ type mailContext struct {
+ Decision
+ Name string
+ Sender string
+ Recipient string
+ VoteURL string
+ UnvotedURL string
+ }
+ voteURL := fmt.Sprintf("%s/vote", config.BaseURL)
+ unvotedURL := fmt.Sprintf("%s/motions/?unvoted=1", config.BaseURL)
+ context := mailContext{
+ *decision, voter.Name, config.NoticeSenderAddress,
+ config.BoardMailAddress, voteURL,
+ unvotedURL}
+
+ t, err := template.New("create_motion_mail.txt").Funcs(
+ sprig.GenericFuncMap()).ParseFiles("templates/create_motion_mail.txt")
+ if err != nil {
+ logger.Fatal(err)
+ }
+ t.Execute(os.Stdout, context)
+
+ // TODO: implement mail sending
return
}
diff --git a/boardvoting.go b/boardvoting.go
index 8ad5438..367179f 100644
--- a/boardvoting.go
+++ b/boardvoting.go
@@ -83,7 +83,7 @@ type motionParameters struct {
}
type motionListParameters struct {
- Page int64
+ Page int64
Flags struct {
Confirmed, Withdraw, Unvoted bool
}
@@ -136,21 +136,13 @@ func motionListHandler(w http.ResponseWriter, r *http.Request) {
session.Save(r, w)
templateContext.Params = ¶ms
- if params.Flags.Unvoted && templateContext.Voter != nil {
- if templateContext.Decisions, err = FindVotersUnvotedDecisionsForDisplayOnPage(
- params.Page, templateContext.Voter); err != nil {
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- return
- }
- } else {
- if templateContext.Decisions, err = FindDecisionsForDisplayOnPage(params.Page); err != nil {
- http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
- return
- }
+ if templateContext.Decisions, err = FindDecisionsForDisplayOnPage(params.Page, params.Flags.Unvoted, templateContext.Voter); err != nil {
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ return
}
if len(templateContext.Decisions) > 0 {
- olderExists, err := templateContext.Decisions[len(templateContext.Decisions)-1].OlderExists()
+ olderExists, err := templateContext.Decisions[len(templateContext.Decisions)-1].OlderExists(params.Flags.Unvoted, templateContext.Voter)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
@@ -363,7 +355,7 @@ func newMotionHandler(w http.ResponseWriter, r *http.Request) {
session.AddFlash("The motion has been proposed!")
session.Save(r, w)
- http.Redirect(w, r, "/motions/", http.StatusTemporaryRedirect)
+ http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
}
return
@@ -384,6 +376,7 @@ type Config struct {
ServerCert string `yaml:"server_certificate"`
ServerKey string `yaml:"server_key"`
CookieSecret string `yaml:"cookie_secret"`
+ BaseURL string `yaml:"base_url"`
}
func init() {
@@ -406,16 +399,17 @@ func init() {
}
store = sessions.NewCookieStore(cookieSecret)
logger.Println("read configuration")
+
+ db, err = sqlx.Open("sqlite3", config.DatabaseFile)
+ if err != nil {
+ logger.Fatal(err)
+ }
+ logger.Println("opened database connection")
}
func main() {
logger.Printf("CAcert Board Voting version %s, build %s\n", version, build)
- var err error
- db, err = sqlx.Open("sqlite3", config.DatabaseFile)
- if err != nil {
- logger.Fatal(err)
- }
defer db.Close()
http.Handle("/motions/", http.StripPrefix("/motions/", motionsHandler{}))
diff --git a/config.yaml.example b/config.yaml.example
index fac503d..7a9390c 100644
--- a/config.yaml.example
+++ b/config.yaml.example
@@ -5,4 +5,5 @@ database_file: database.sqlite
client_ca_certificates: cacert_class3.pem
server_certificate: server.crt
server_key: server.key
-cookie_secret: base64encoded_random_byte_value_of_at_least_32_bytes
\ No newline at end of file
+cookie_secret: base64encoded_random_byte_value_of_at_least_32_bytes
+base_url: https://motions.cacert.org
\ No newline at end of file
diff --git a/forms.go b/forms.go
index d5359ff..f3cd942 100644
--- a/forms.go
+++ b/forms.go
@@ -47,8 +47,10 @@ func (f *NewDecisionForm) Validate() (bool, *Decision) {
f.Errors["Due"] = "Please choose a valid due date"
} else {
year, month, day := time.Now().UTC().Add(dueDuration).Date()
- data.Due = time.Date(year, month, day, 23,59,59,0, time.UTC)
+ data.Due = time.Date(year, month, day, 23, 59, 59, 0, time.UTC)
}
+ data.Proposed = time.Now().UTC()
+
return len(f.Errors) == 0, data
}
diff --git a/models.go b/models.go
index 410ed3e..a8d24ad 100644
--- a/models.go
+++ b/models.go
@@ -6,8 +6,23 @@ import (
"time"
)
+type sqlKey int
+
const (
- sqlGetDecisions = `
+ sqlLoadDecisions sqlKey = iota
+ sqlLoadUnvotedDecisions
+ sqlLoadDecisionByTag
+ sqlLoadDecisionById
+ sqlLoadVoteCountsForDecision
+ sqlLoadVotesForDecision
+ sqlLoadEnabledVoterByEmail
+ sqlCountOlderThanDecision
+ sqlCountOlderThanUnvotedDecision
+ sqlCreateDecision
+)
+
+var sqlStatements = map[sqlKey]string{
+ sqlLoadDecisions: `
SELECT decisions.id, decisions.tag, decisions.proponent,
voters.name AS proposer, decisions.proposed, decisions.title,
decisions.content, decisions.votetype, decisions.status, decisions.due,
@@ -15,60 +30,83 @@ 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`
- sqlGetDecision = `
+LIMIT 10 OFFSET 10 * $1`,
+ sqlLoadUnvotedDecisions: `
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, voters.enabled, voters.reminder
-FROM voters
-JOIN emails ON voters.id=emails.voter
-WHERE emails.address=$1 AND voters.enabled=1`
- sqlVoteCount = `
+WHERE decisions.status = 0 AND decisions.id NOT IN (
+ SELECT votes.decision
+ FROM votes
+ WHERE votes.voter = $1)
+ORDER BY proposed DESC
+LIMIT 10 OFFSET 10 * $2;`,
+ sqlLoadDecisionByTag: `
+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;`,
+ sqlLoadDecisionById: `
+SELECT decisions.id, decisions.tag, decisions.proponent, decisions.proposed,
+ decisions.title, decisions.content, decisions.votetype, decisions.status,
+ decisions.due, decisions.modified
+FROM decisions
+WHERE decisions.id=$1;`,
+ sqlLoadVoteCountsForDecision: `
SELECT vote, COUNT(vote)
FROM votes
-WHERE decision=$1 GROUP BY vote`
- sqlCountOlderThanDecision = `
-SELECT COUNT(*) > 0 FROM decisions WHERE proposed < $1`
- sqlGetVotesForDecision = `
+WHERE decision=$1 GROUP BY vote`,
+ sqlLoadVotesForDecision: `
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`
- sqlCreateDecision = `
+WHERE decision=$1`,
+ sqlLoadEnabledVoterByEmail: `
+SELECT voters.id, voters.name, voters.enabled, voters.reminder
+FROM voters
+JOIN emails ON voters.id=emails.voter
+WHERE emails.address=$1 AND voters.enabled=1`,
+ sqlCountOlderThanDecision: `
+SELECT COUNT(*) > 0 FROM decisions WHERE proposed < $1`,
+ sqlCountOlderThanUnvotedDecision: `
+SELECT COUNT(*) > 0 FROM decisions WHERE proposed < $1
+ AND status=0
+ AND id NOT IN (SELECT decision FROM votes WHERE votes.voter=$2)`,
+ sqlCreateDecision: `
INSERT INTO decisions (
proposed, proponent, title, content, votetype, status, due, modified,tag
) VALUES (
- datetime('now','utc'), :proponent, :title, :content, :votetype, 0,
+ :proposed, :proponent, :title, :content, :votetype, 0,
:due,
- datetime('now','utc'),
- 'm' || strftime('%Y%m%d','now') || '.' || (
- SELECT COUNT(*)+1 AS num
- FROM decisions
- WHERE proposed BETWEEN date('now') AND date('now','1 day')
+ :proposed,
+ 'm' || strftime('%Y%m%d', :proposed) || '.' || (
+ SELECT COUNT(*)+1 AS num
+ FROM decisions
+ WHERE proposed
+ BETWEEN date(:proposed) AND date(:proposed, '1 day')
)
-)
-`
-)
+)`,
+}
var db *sqlx.DB
+func init() {
+ for _, sqlStatement := range sqlStatements {
+ var stmt *sqlx.Stmt
+ stmt, err := db.Preparex(sqlStatement)
+ if err != nil {
+ logger.Fatalf("ERROR parsing statement %s: %s", sqlStatement, err)
+ }
+ stmt.Close()
+ }
+}
+
type VoteType uint8
type VoteStatus int8
@@ -198,7 +236,7 @@ type DecisionForDisplay struct {
}
func FindDecisionForDisplayByTag(tag string) (decision *DecisionForDisplay, err error) {
- decisionStmt, err := db.Preparex(sqlGetDecision)
+ decisionStmt, err := db.Preparex(sqlStatements[sqlLoadDecisionByTag])
if err != nil {
logger.Println("Error preparing statement:", err)
return
@@ -223,45 +261,25 @@ func FindDecisionForDisplayByTag(tag string) (decision *DecisionForDisplay, err
// 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)
+func FindDecisionsForDisplayOnPage(page int64, unvoted bool, voter *Voter) (decisions []*DecisionForDisplay, err error) {
+ var decisionsStmt *sqlx.Stmt
+ if unvoted && voter != nil {
+ decisionsStmt, err = db.Preparex(sqlStatements[sqlLoadUnvotedDecisions])
+ } else {
+ decisionsStmt, err = db.Preparex(sqlStatements[sqlLoadDecisions])
+ }
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
+ var rows *sqlx.Rows
+ if unvoted && voter != nil {
+ rows, err = decisionsStmt.Queryx(voter.Id, page-1)
+ } else {
+ rows, err = decisionsStmt.Queryx(page - 1)
}
- 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
@@ -284,7 +302,7 @@ func FindVotersUnvotedDecisionsForDisplayOnPage(page int64, voter *Voter) (decis
}
func (d *Decision) VoteSums() (sums *VoteSums, err error) {
- votesStmt, err := db.Preparex(sqlVoteCount)
+ votesStmt, err := db.Preparex(sqlStatements[sqlLoadVoteCountsForDecision])
if err != nil {
logger.Println("Error preparing statement:", err)
return
@@ -319,7 +337,7 @@ func (d *Decision) VoteSums() (sums *VoteSums, err error) {
}
func (d *DecisionForDisplay) LoadVotes() (err error) {
- votesStmt, err := db.Preparex(sqlGetVotesForDecision)
+ votesStmt, err := db.Preparex(sqlStatements[sqlLoadVotesForDecision])
if err != nil {
logger.Println("Error preparing statement:", err)
return
@@ -332,22 +350,34 @@ func (d *DecisionForDisplay) LoadVotes() (err error) {
return
}
-func (d *Decision) OlderExists() (result bool, err error) {
- olderStmt, err := db.Preparex(sqlCountOlderThanDecision)
+func (d *Decision) OlderExists(unvoted bool, voter *Voter) (result bool, err error) {
+ var olderStmt *sqlx.Stmt
+ if unvoted && voter != nil {
+ olderStmt, err = db.Preparex(sqlStatements[sqlCountOlderThanUnvotedDecision])
+ } else {
+ olderStmt, err = db.Preparex(sqlStatements[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)
+ if unvoted && voter != nil {
+ if err = olderStmt.Get(&result, d.Proposed, voter.Id); err != nil {
+ logger.Printf("Error finding older motions than %s: %v\n", d.Tag, err)
+ }
+ } else {
+ if err = olderStmt.Get(&result, d.Proposed); err != nil {
+ logger.Printf("Error finding older motions than %s: %v\n", d.Tag, err)
+ }
}
+
return
}
func (d *Decision) Save() (err error) {
- insertDecisionStmt, err := db.PrepareNamed(sqlCreateDecision)
+ insertDecisionStmt, err := db.PrepareNamed(sqlStatements[sqlCreateDecision])
if err != nil {
logger.Println("Error preparing statement:", err)
return
@@ -359,14 +389,30 @@ func (d *Decision) Save() (err error) {
logger.Println("Error creating motion:", err)
return
}
- logger.Println(result)
- // TODO: implement fetch last id from result
- // TODO: load decision from DB
+
+ lastInsertId, err := result.LastInsertId()
+ if err != nil {
+ logger.Println("Error getting id of inserted motion:", err)
+ }
+ logger.Println("DEBUG new motion has id", lastInsertId)
+
+ getDecisionStmt, err := db.Preparex(sqlStatements[sqlLoadDecisionById])
+ if err != nil {
+ logger.Println("Error preparing statement:", err)
+ return
+ }
+ defer getDecisionStmt.Close()
+
+ err = getDecisionStmt.Get(d, lastInsertId)
+ if err != nil {
+ logger.Println("Error getting inserted motion:", err)
+ }
+
return
}
func FindVoterByAddress(emailAddress string) (voter *Voter, err error) {
- findVoterStmt, err := db.Preparex(sqlGetVoter)
+ findVoterStmt, err := db.Preparex(sqlStatements[sqlLoadEnabledVoterByEmail])
if err != nil {
logger.Println("Error preparing statement:", err)
return
diff --git a/templates/create_motion_mail.txt b/templates/create_motion_mail.txt
new file mode 100644
index 0000000..f26577e
--- /dev/null
+++ b/templates/create_motion_mail.txt
@@ -0,0 +1,25 @@
+From: {{ .Sender }}
+To: {{ .Recipient }}
+Subject: {{ .Tag }} - {{ .Title }}
+
+Dear Board,
+
+{{ .Name }} has made the following motion:
+
+{{ .Title }}
+{{ .Content }}
+
+Vote type: {{ .VoteType }}
+
+Voting will close {{ .Due }}
+
+To vote please choose:
+
+Aye: {{ .VoteURL }}/aye
+Naye: {{ .VoteURL }}/naye
+Abstain: {{ .VoteURL }}/abstain
+
+To see all your pending votes: {{ .UnvotedURL }}
+
+Kind regards,
+the voting system
\ No newline at end of file
diff --git a/templates/motions.html b/templates/motions.html
index 414c399..e5279bd 100644
--- a/templates/motions.html
+++ b/templates/motions.html
@@ -3,7 +3,7 @@
{{ if .Params.Flags.Unvoted }}
Show all votes
{{ else if $voter }}
-Show my outstanding votes
+Show my pending votes
{{ end }}
{{ if .Decisions }}
- {{ if .PrevPage }}<{{ end }} - {{ if .NextPage }}>{{ end }} + {{ if .PrevPage }}<{{ end }} + {{ if .NextPage }}>{{ end }} | {{ if $voter }}