Implement motion creation mail template

This commit is contained in:
Jan Dittberner 2017-04-18 20:30:08 +02:00
parent bcfbad42b6
commit cc0f5c0b7b
7 changed files with 196 additions and 105 deletions

View file

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

View file

@ -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 = &params
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{}))

View file

@ -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
cookie_secret: base64encoded_random_byte_value_of_at_least_32_bytes
base_url: https://motions.cacert.org

View file

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

206
models.go
View file

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

View file

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

View file

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