Implement motion creation mail template
authorJan Dittberner <jan@dittberner.info>
Tue, 18 Apr 2017 18:30:08 +0000 (20:30 +0200)
committerJan Dittberner <jan@dittberner.info>
Fri, 21 Apr 2017 22:12:38 +0000 (00:12 +0200)
actions.go
boardvoting.go
config.yaml.example
forms.go
models.go
templates/create_motion_mail.txt [new file with mode: 0644]
templates/motions.html

index 190314c..0619587 100644 (file)
@@ -1,6 +1,7 @@
 package main
 
 import (
 package main
 
 import (
+       "fmt"
        "github.com/Masterminds/sprig"
        "os"
        "text/template"
        "github.com/Masterminds/sprig"
        "os"
        "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
 }
 
index 8ad5438..367179f 100644 (file)
@@ -83,7 +83,7 @@ type motionParameters struct {
 }
 
 type motionListParameters struct {
 }
 
 type motionListParameters struct {
-       Page int64
+       Page  int64
        Flags struct {
                Confirmed, Withdraw, Unvoted bool
        }
        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
 
        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 {
        }
 
        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
                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)
 
                        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
                }
 
                return
@@ -384,6 +376,7 @@ type Config struct {
        ServerCert           string `yaml:"server_certificate"`
        ServerKey            string `yaml:"server_key"`
        CookieSecret         string `yaml:"cookie_secret"`
        ServerCert           string `yaml:"server_certificate"`
        ServerKey            string `yaml:"server_key"`
        CookieSecret         string `yaml:"cookie_secret"`
+       BaseURL              string `yaml:"base_url"`
 }
 
 func init() {
 }
 
 func init() {
@@ -406,16 +399,17 @@ func init() {
        }
        store = sessions.NewCookieStore(cookieSecret)
        logger.Println("read configuration")
        }
        store = sessions.NewCookieStore(cookieSecret)
        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)
        if err != nil {
                logger.Fatal(err)
        }
        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)
+
        defer db.Close()
 
        http.Handle("/motions/", http.StripPrefix("/motions/", motionsHandler{}))
        defer db.Close()
 
        http.Handle("/motions/", http.StripPrefix("/motions/", motionsHandler{}))
index fac503d..7a9390c 100644 (file)
@@ -5,4 +5,5 @@ database_file: database.sqlite
 client_ca_certificates: cacert_class3.pem
 server_certificate: server.crt
 server_key: server.key
 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
index d5359ff..f3cd942 100644 (file)
--- 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()
                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
 }
        return len(f.Errors) == 0, data
 }
index 410ed3e..a8d24ad 100644 (file)
--- a/models.go
+++ b/models.go
@@ -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,
        voters.name AS proposer, decisions.proposed, decisions.title,
        decisions.content, decisions.votetype, decisions.status, decisions.due,
 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
 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
 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
 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
 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 (
 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,
     :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
 
 
 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
 
 type VoteType uint8
 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 {
                logger.Println("Error preparing statement:", err)
                return
        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
 // 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)
+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])
        }
        }
-       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()
 
        if err != nil {
                logger.Println("Error preparing statement:", err)
                return
        }
        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 {
                logger.Printf("Error loading motions for page %d: %v\n", page, err)
                return
        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) {
 }
 
 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
        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) {
 }
 
 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
        if err != nil {
                logger.Println("Error preparing statement:", err)
                return
@@ -332,22 +350,34 @@ func (d *DecisionForDisplay) LoadVotes() (err error) {
        return
 }
 
        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 != 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) {
        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
        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("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) {
        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
        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 (file)
index 0000000..f26577e
--- /dev/null
@@ -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
index 414c399..e5279bd 100644 (file)
@@ -3,7 +3,7 @@
 {{ if .Params.Flags.Unvoted }}
 <a href="/motions/">Show all votes</a>
 {{ else if $voter }}
 {{ 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">
 {{ end }}
 {{ if .Decisions }}
 <table class="list">
@@ -23,8 +23,8 @@
     {{end}}
     <tr>
         <td colspan="2" class="navigation">
     {{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">
         </td>
         {{ if $voter }}
         <td class="actions">