Implement motion creation mail template

debian
Jan Dittberner 7 years ago
parent bcfbad42b6
commit cc0f5c0b7b

@ -1,6 +1,7 @@
package main package main
import ( import (
"fmt"
"github.com/Masterminds/sprig" "github.com/Masterminds/sprig"
"os" "os"
"text/template" "text/template"
@ -14,7 +15,29 @@ func CreateMotion(decision *Decision, voter *Voter) (err error) {
return 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 return
} }

@ -83,7 +83,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
} }
@ -136,21 +136,13 @@ func motionListHandler(w http.ResponseWriter, r *http.Request) {
session.Save(r, w) session.Save(r, w)
templateContext.Params = &params templateContext.Params = &params
if params.Flags.Unvoted && templateContext.Voter != nil { if templateContext.Decisions, err = FindDecisionsForDisplayOnPage(params.Page, params.Flags.Unvoted, templateContext.Voter); err != nil {
if templateContext.Decisions, err = FindVotersUnvotedDecisionsForDisplayOnPage( http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
params.Page, templateContext.Voter); err != nil { return
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 len(templateContext.Decisions) > 0 { 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 { if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return return
@ -363,7 +355,7 @@ func newMotionHandler(w http.ResponseWriter, r *http.Request) {
session.AddFlash("The motion has been proposed!") session.AddFlash("The motion has been proposed!")
session.Save(r, w) session.Save(r, w)
http.Redirect(w, r, "/motions/", http.StatusTemporaryRedirect) http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
} }
return return
@ -384,6 +376,7 @@ type Config struct {
ServerCert string `yaml:"server_certificate"` ServerCert string `yaml:"server_certificate"`
ServerKey string `yaml:"server_key"` ServerKey string `yaml:"server_key"`
CookieSecret string `yaml:"cookie_secret"` CookieSecret string `yaml:"cookie_secret"`
BaseURL string `yaml:"base_url"`
} }
func init() { func init() {
@ -406,16 +399,17 @@ func init() {
} }
store = sessions.NewCookieStore(cookieSecret) store = sessions.NewCookieStore(cookieSecret)
logger.Println("read configuration") logger.Println("read configuration")
}
func main() {
logger.Printf("CAcert Board Voting version %s, build %s\n", version, build)
var err error
db, err = sqlx.Open("sqlite3", config.DatabaseFile) db, err = sqlx.Open("sqlite3", config.DatabaseFile)
if err != nil { if err != nil {
logger.Fatal(err) logger.Fatal(err)
} }
logger.Println("opened database connection")
}
func main() {
logger.Printf("CAcert Board Voting version %s, build %s\n", version, build)
defer db.Close() defer db.Close()
http.Handle("/motions/", http.StripPrefix("/motions/", motionsHandler{})) http.Handle("/motions/", http.StripPrefix("/motions/", motionsHandler{}))

@ -5,4 +5,5 @@ database_file: database.sqlite
client_ca_certificates: cacert_class3.pem client_ca_certificates: cacert_class3.pem
server_certificate: server.crt server_certificate: server.crt
server_key: server.key server_key: server.key
cookie_secret: base64encoded_random_byte_value_of_at_least_32_bytes cookie_secret: base64encoded_random_byte_value_of_at_least_32_bytes
base_url: https://motions.cacert.org

@ -47,8 +47,10 @@ func (f *NewDecisionForm) Validate() (bool, *Decision) {
f.Errors["Due"] = "Please choose a valid due date" f.Errors["Due"] = "Please choose a valid due date"
} else { } else {
year, month, day := time.Now().UTC().Add(dueDuration).Date() 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 return len(f.Errors) == 0, data
} }

@ -6,8 +6,23 @@ import (
"time" "time"
) )
type sqlKey int
const ( 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, SELECT decisions.id, decisions.tag, decisions.proponent,
voters.name AS proposer, decisions.proposed, decisions.title, voters.name AS proposer, decisions.proposed, decisions.title,
decisions.content, decisions.votetype, decisions.status, decisions.due, decisions.content, decisions.votetype, decisions.status, decisions.due,
@ -15,60 +30,83 @@ SELECT decisions.id, decisions.tag, decisions.proponent,
FROM decisions FROM decisions
JOIN voters ON decisions.proponent=voters.id JOIN voters ON decisions.proponent=voters.id
ORDER BY proposed DESC ORDER BY proposed DESC
LIMIT 10 OFFSET 10 * $1` LIMIT 10 OFFSET 10 * $1`,
sqlGetDecision = ` sqlLoadUnvotedDecisions: `
SELECT decisions.id, decisions.tag, decisions.proponent, SELECT decisions.id, decisions.tag, decisions.proponent,
voters.name AS proposer, decisions.proposed, decisions.title, voters.name AS proposer, decisions.proposed, decisions.title,
decisions.content, decisions.votetype, decisions.status, decisions.due, decisions.content, decisions.votetype, decisions.status, decisions.due,
decisions.modified decisions.modified
FROM decisions FROM decisions
JOIN voters ON decisions.proponent=voters.id JOIN voters ON decisions.proponent=voters.id
WHERE decisions.tag=$1;` WHERE decisions.status = 0 AND decisions.id NOT IN (
sqlGetVoter = ` SELECT votes.decision
SELECT voters.id, voters.name, voters.enabled, voters.reminder FROM votes
FROM voters WHERE votes.voter = $1)
JOIN emails ON voters.id=emails.voter ORDER BY proposed DESC
WHERE emails.address=$1 AND voters.enabled=1` LIMIT 10 OFFSET 10 * $2;`,
sqlVoteCount = ` 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) SELECT vote, COUNT(vote)
FROM votes FROM votes
WHERE decision=$1 GROUP BY vote` WHERE decision=$1 GROUP BY vote`,
sqlCountOlderThanDecision = ` sqlLoadVotesForDecision: `
SELECT COUNT(*) > 0 FROM decisions WHERE proposed < $1`
sqlGetVotesForDecision = `
SELECT votes.decision, votes.voter, voters.name, votes.vote, votes.voted, votes.notes SELECT votes.decision, votes.voter, voters.name, votes.vote, votes.voted, votes.notes
FROM votes FROM votes
JOIN voters ON votes.voter=voters.id JOIN voters ON votes.voter=voters.id
WHERE decision=$1` WHERE decision=$1`,
sqlListUnvotedDecisions = ` sqlLoadEnabledVoterByEmail: `
SELECT decisions.id, decisions.tag, decisions.proponent, SELECT voters.id, voters.name, voters.enabled, voters.reminder
voters.name AS proposer, decisions.proposed, decisions.title, FROM voters
decisions.content AS content, decisions.votetype, decisions.status, decisions.due, JOIN emails ON voters.id=emails.voter
decisions.modified WHERE emails.address=$1 AND voters.enabled=1`,
FROM decisions sqlCountOlderThanDecision: `
JOIN voters ON decisions.proponent=voters.id SELECT COUNT(*) > 0 FROM decisions WHERE proposed < $1`,
WHERE decisions.status=0 AND decisions.id NOT IN ( sqlCountOlderThanUnvotedDecision: `
SELECT decision FROM votes WHERE votes.voter=$2) SELECT COUNT(*) > 0 FROM decisions WHERE proposed < $1
ORDER BY proposed DESC AND status=0
LIMIT 10 OFFSET 10 * $1` AND id NOT IN (SELECT decision FROM votes WHERE votes.voter=$2)`,
sqlCreateDecision = ` sqlCreateDecision: `
INSERT INTO decisions ( INSERT INTO decisions (
proposed, proponent, title, content, votetype, status, due, modified,tag proposed, proponent, title, content, votetype, status, due, modified,tag
) VALUES ( ) VALUES (
datetime('now','utc'), :proponent, :title, :content, :votetype, 0, :proposed, :proponent, :title, :content, :votetype, 0,
:due, :due,
datetime('now','utc'), :proposed,
'm' || strftime('%Y%m%d','now') || '.' || ( 'm' || strftime('%Y%m%d', :proposed) || '.' || (
SELECT COUNT(*)+1 AS num SELECT COUNT(*)+1 AS num
FROM decisions FROM decisions
WHERE proposed BETWEEN date('now') AND date('now','1 day') WHERE proposed
BETWEEN date(:proposed) AND date(:proposed, '1 day')
) )
) )`,
` }
)
var db *sqlx.DB 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 VoteType uint8
type VoteStatus int8 type VoteStatus int8
@ -198,7 +236,7 @@ type DecisionForDisplay struct {
} }
func FindDecisionForDisplayByTag(tag string) (decision *DecisionForDisplay, err error) { func FindDecisionForDisplayByTag(tag string) (decision *DecisionForDisplay, err error) {
decisionStmt, err := db.Preparex(sqlGetDecision) decisionStmt, err := db.Preparex(sqlStatements[sqlLoadDecisionByTag])
if err != nil { if err != nil {
logger.Println("Error preparing statement:", err) logger.Println("Error preparing statement:", err)
return 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. // This function uses OFFSET for pagination which is not a good idea for larger data sets.
// //
// TODO: migrate to timestamp base pagination // TODO: migrate to timestamp base pagination
func FindDecisionsForDisplayOnPage(page int64) (decisions []*DecisionForDisplay, err error) { func FindDecisionsForDisplayOnPage(page int64, unvoted bool, voter *Voter) (decisions []*DecisionForDisplay, err error) {
decisionsStmt, err := db.Preparex(sqlGetDecisions) var decisionsStmt *sqlx.Stmt
if err != nil { if unvoted && voter != nil {
logger.Println("Error preparing statement:", err) decisionsStmt, err = db.Preparex(sqlStatements[sqlLoadUnvotedDecisions])
return } else {
} decisionsStmt, err = db.Preparex(sqlStatements[sqlLoadDecisions])
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 { if err != nil {
logger.Println("Error preparing statement:", err) logger.Println("Error preparing statement:", err)
return return
} }
defer decisionsStmt.Close() defer decisionsStmt.Close()
rows, err := decisionsStmt.Queryx(page-1, voter.Id) var rows *sqlx.Rows
if unvoted && voter != nil {
rows, err = decisionsStmt.Queryx(voter.Id, page-1)
} else {
rows, err = decisionsStmt.Queryx(page - 1)
}
if err != nil { if err != nil {
logger.Printf("Error loading motions for page %d: %v\n", page, err) logger.Printf("Error loading motions for page %d: %v\n", page, err)
return return
@ -284,7 +302,7 @@ func FindVotersUnvotedDecisionsForDisplayOnPage(page int64, voter *Voter) (decis
} }
func (d *Decision) VoteSums() (sums *VoteSums, err error) { func (d *Decision) VoteSums() (sums *VoteSums, err error) {
votesStmt, err := db.Preparex(sqlVoteCount) votesStmt, err := db.Preparex(sqlStatements[sqlLoadVoteCountsForDecision])
if err != nil { if err != nil {
logger.Println("Error preparing statement:", err) logger.Println("Error preparing statement:", err)
return return
@ -319,7 +337,7 @@ func (d *Decision) VoteSums() (sums *VoteSums, err error) {
} }
func (d *DecisionForDisplay) LoadVotes() (err error) { func (d *DecisionForDisplay) LoadVotes() (err error) {
votesStmt, err := db.Preparex(sqlGetVotesForDecision) votesStmt, err := db.Preparex(sqlStatements[sqlLoadVotesForDecision])
if err != nil { if err != nil {
logger.Println("Error preparing statement:", err) logger.Println("Error preparing statement:", err)
return return
@ -332,22 +350,34 @@ func (d *DecisionForDisplay) LoadVotes() (err error) {
return return
} }
func (d *Decision) OlderExists() (result bool, err error) { func (d *Decision) OlderExists(unvoted bool, voter *Voter) (result bool, err error) {
olderStmt, err := db.Preparex(sqlCountOlderThanDecision) var olderStmt *sqlx.Stmt
if unvoted && voter != nil {
olderStmt, err = db.Preparex(sqlStatements[sqlCountOlderThanUnvotedDecision])
} else {
olderStmt, err = db.Preparex(sqlStatements[sqlCountOlderThanDecision])
}
if err != nil { if err != nil {
logger.Println("Error preparing statement:", err) logger.Println("Error preparing statement:", err)
return return
} }
defer olderStmt.Close() defer olderStmt.Close()
if err := olderStmt.Get(&result, d.Proposed); err != nil { if unvoted && voter != nil {
logger.Printf("Error finding older motions than %s: %v\n", d.Tag, err) 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 return
} }
func (d *Decision) Save() (err error) { func (d *Decision) Save() (err error) {
insertDecisionStmt, err := db.PrepareNamed(sqlCreateDecision) insertDecisionStmt, err := db.PrepareNamed(sqlStatements[sqlCreateDecision])
if err != nil { if err != nil {
logger.Println("Error preparing statement:", err) logger.Println("Error preparing statement:", err)
return return
@ -359,14 +389,30 @@ func (d *Decision) Save() (err error) {
logger.Println("Error creating motion:", err) logger.Println("Error creating motion:", err)
return return
} }
logger.Println(result)
// TODO: implement fetch last id from result lastInsertId, err := result.LastInsertId()
// TODO: load decision from DB 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 return
} }
func FindVoterByAddress(emailAddress string) (voter *Voter, err error) { func FindVoterByAddress(emailAddress string) (voter *Voter, err error) {
findVoterStmt, err := db.Preparex(sqlGetVoter) findVoterStmt, err := db.Preparex(sqlStatements[sqlLoadEnabledVoterByEmail])
if err != nil { if err != nil {
logger.Println("Error preparing statement:", err) logger.Println("Error preparing statement:", err)
return return

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

@ -3,7 +3,7 @@
{{ if .Params.Flags.Unvoted }} {{ if .Params.Flags.Unvoted }}
<a href="/motions/">Show all votes</a> <a href="/motions/">Show all votes</a>
{{ else if $voter }} {{ else if $voter }}
<a href="/motions/?unvoted=1">Show my outstanding votes</a><br/> <a href="/motions/?unvoted=1">Show my pending votes</a><br/>
{{ end }} {{ end }}
{{ if .Decisions }} {{ if .Decisions }}
<table class="list"> <table class="list">
@ -23,8 +23,8 @@
{{end}} {{end}}
<tr> <tr>
<td colspan="2" 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 }}{{ if .Params.Flags.Unvoted }}&unvoted=1{{ end }}" title="previous page">&lt;</a>{{ end }}
{{ if .NextPage }}<a href="?page={{ .NextPage }}" title="next page">&gt;</a>{{ end }} {{ if .NextPage }}<a href="?page={{ .NextPage }}{{ if .Params.Flags.Unvoted }}&unvoted=1{{ end }}" title="next page">&gt;</a>{{ end }}
</td> </td>
{{ if $voter }} {{ if $voter }}
<td class="actions"> <td class="actions">

Loading…
Cancel
Save