Implement proxy voting
This commit is contained in:
parent
b6ad5d8ad3
commit
2cac50ee86
8 changed files with 486 additions and 236 deletions
188
boardvoting.go
188
boardvoting.go
|
@ -1,10 +1,12 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"github.com/Masterminds/sprig"
|
||||
"github.com/gorilla/sessions"
|
||||
|
@ -50,6 +52,7 @@ const (
|
|||
ctxNeedsAuth contextKey = iota
|
||||
ctxVoter
|
||||
ctxDecision
|
||||
ctxVote
|
||||
)
|
||||
|
||||
func authenticateRequest(w http.ResponseWriter, r *http.Request, handler func(http.ResponseWriter, *http.Request)) {
|
||||
|
@ -227,6 +230,11 @@ func getDecisionFromRequest(r *http.Request) (decision *DecisionForDisplay, ok b
|
|||
return
|
||||
}
|
||||
|
||||
func getVoteFromRequest(r *http.Request) (vote VoteChoice, ok bool) {
|
||||
vote, ok = r.Context().Value(ctxVote).(VoteChoice)
|
||||
return
|
||||
}
|
||||
|
||||
type FlashMessageAction struct{}
|
||||
|
||||
func (a *FlashMessageAction) AddFlash(w http.ResponseWriter, r *http.Request, message interface{}, tags ...string) (err error) {
|
||||
|
@ -277,7 +285,7 @@ func (a *withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
notifyMail <- &NotificationWithDrawMotion{decision: decision.Decision, voter: *voter}
|
||||
NotifyMailChannel <- NewNotificationWithDrawMotion(&(decision.Decision), voter)
|
||||
|
||||
if err := a.AddFlash(w, r, fmt.Sprintf("Motion %s has been withdrawn!", decision.Tag)); err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
@ -330,7 +338,7 @@ func (h *newMotionHandler) Handle(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
notifyMail <- &NotificationCreateMotion{decision: *data, voter: *voter}
|
||||
NotifyMailChannel <- &NotificationCreateMotion{decision: *data, voter: *voter}
|
||||
|
||||
if err := h.AddFlash(w, r, "The motion has been proposed!"); err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
@ -395,7 +403,7 @@ func (a editMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
notifyMail <- &NotificationUpdateMotion{decision: *data, voter: *voter}
|
||||
NotifyMailChannel <- NewNotificationUpdateMotion(*data, *voter)
|
||||
|
||||
if err := a.AddFlash(w, r, "The motion has been modified!"); err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
@ -467,17 +475,164 @@ func (h motionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
type voteHandler struct {
|
||||
FlashMessageAction
|
||||
authenticationRequiredHandler
|
||||
}
|
||||
|
||||
func (h *voteHandler) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
decision, ok := getDecisionFromRequest(r)
|
||||
if !ok {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
voter, ok := getVoterFromRequest(r)
|
||||
if !ok {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
vote, ok := getVoteFromRequest(r)
|
||||
if !ok {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(w, "to be implemented")
|
||||
fmt.Fprintln(w, "Decision:", decision)
|
||||
fmt.Fprintln(w, "Voter:", voter)
|
||||
fmt.Fprintln(w, "Vote:", vote)
|
||||
}
|
||||
|
||||
type proxyVoteHandler struct {
|
||||
FlashMessageAction
|
||||
authenticationRequiredHandler
|
||||
}
|
||||
|
||||
func getPEMClientCert(r *http.Request) string {
|
||||
clientCertPEM := bytes.NewBufferString("")
|
||||
pem.Encode(clientCertPEM, &pem.Block{Type: "CERTIFICATE", Bytes: r.TLS.PeerCertificates[0].Raw})
|
||||
return clientCertPEM.String()
|
||||
}
|
||||
|
||||
func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
decision, ok := getDecisionFromRequest(r)
|
||||
if !ok {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
proxy, ok := getVoterFromRequest(r)
|
||||
if !ok {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
templates := []string{"proxy_vote_form.html", "header.html", "footer.html", "motion_fragments.html"}
|
||||
var templateContext struct {
|
||||
Form ProxyVoteForm
|
||||
Decision *DecisionForDisplay
|
||||
Voters *[]Voter
|
||||
PageTitle string
|
||||
Flashes interface{}
|
||||
}
|
||||
switch r.Method {
|
||||
case http.MethodPost:
|
||||
form := ProxyVoteForm{
|
||||
Voter: r.FormValue("Voter"),
|
||||
Vote: r.FormValue("Vote"),
|
||||
Justification: r.FormValue("Justification"),
|
||||
}
|
||||
|
||||
if valid, voter, data, justification := form.Validate(); !valid {
|
||||
templateContext.Form = form
|
||||
templateContext.Decision = decision
|
||||
if voters, err := GetVotersForProxy(proxy, &decision.Decision); err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
} else {
|
||||
templateContext.Voters = voters
|
||||
}
|
||||
renderTemplate(w, templates, templateContext)
|
||||
} else {
|
||||
data.DecisionId = decision.Id
|
||||
data.Voted = time.Now().UTC()
|
||||
data.Notes = fmt.Sprintf(
|
||||
"Proxy-Vote by %s\n\n%s\n\n%s",
|
||||
proxy.Name, justification, getPEMClientCert(r))
|
||||
|
||||
if err := data.Save(); err != nil {
|
||||
logger.Println("Error saving vote:", err)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
NotifyMailChannel <- NewNotificationProxyVote(&decision.Decision, proxy, voter, data, justification)
|
||||
|
||||
if err := h.AddFlash(w, r, "The vote has been registered."); err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
|
||||
}
|
||||
return
|
||||
default:
|
||||
templateContext.Form = ProxyVoteForm{}
|
||||
templateContext.Decision = decision
|
||||
if voters, err := GetVotersForProxy(proxy, &decision.Decision); err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
} else {
|
||||
templateContext.Voters = voters
|
||||
}
|
||||
renderTemplate(w, templates, templateContext)
|
||||
}
|
||||
}
|
||||
|
||||
type decisionVoteHandler struct{}
|
||||
|
||||
func (h *decisionVoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if err := db.Ping(); err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(r.URL.Path, "/proxy/"):
|
||||
motionTag := r.URL.Path[len("/proxy/"):]
|
||||
handler := &proxyVoteHandler{}
|
||||
authenticateRequest(
|
||||
w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)),
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
singleDecisionHandler(w, r, motionTag, handler.Handle)
|
||||
})
|
||||
case strings.HasPrefix(r.URL.Path, "/vote/"):
|
||||
parts := strings.Split(r.URL.Path[len("/vote/"):], "/")
|
||||
motionTag := parts[0]
|
||||
voteValue, ok := VoteValues[parts[1]]
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
handler := &voteHandler{}
|
||||
authenticateRequest(
|
||||
w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)),
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
singleDecisionHandler(
|
||||
w, r.WithContext(context.WithValue(r.Context(), ctxVote, voteValue)),
|
||||
motionTag, handler.Handle)
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
BoardMailAddress string `yaml:"board_mail_address"`
|
||||
NoticeSenderAddress string `yaml:"notice_sender_address"`
|
||||
ReminderSenderAddress string `yaml:"reminder_sender_address"`
|
||||
DatabaseFile string `yaml:"database_file"`
|
||||
ClientCACertificates string `yaml:"client_ca_certificates"`
|
||||
ServerCert string `yaml:"server_certificate"`
|
||||
ServerKey string `yaml:"server_key"`
|
||||
CookieSecret string `yaml:"cookie_secret"`
|
||||
BaseURL string `yaml:"base_url"`
|
||||
MailServer struct {
|
||||
BoardMailAddress string `yaml:"board_mail_address"`
|
||||
VoteNoticeAddress string `yaml:"notice_sender_address"`
|
||||
NotificationSenderAddress string `yaml:"reminder_sender_address"`
|
||||
DatabaseFile string `yaml:"database_file"`
|
||||
ClientCACertificates string `yaml:"client_ca_certificates"`
|
||||
ServerCert string `yaml:"server_certificate"`
|
||||
ServerKey string `yaml:"server_key"`
|
||||
CookieSecret string `yaml:"cookie_secret"`
|
||||
BaseURL string `yaml:"base_url"`
|
||||
MailServer struct {
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
} `yaml:"mail_server"`
|
||||
|
@ -516,8 +671,9 @@ func main() {
|
|||
|
||||
defer db.Close()
|
||||
|
||||
go MailNotifier()
|
||||
defer CloseMailNotifier()
|
||||
quitMailChannel := make(chan int)
|
||||
go MailNotifier(quitMailChannel)
|
||||
defer func() { quitMailChannel <- 1 }()
|
||||
|
||||
quitChannel := make(chan int)
|
||||
go JobScheduler(quitChannel)
|
||||
|
@ -525,6 +681,8 @@ func main() {
|
|||
|
||||
http.Handle("/motions/", http.StripPrefix("/motions/", motionsHandler{}))
|
||||
http.Handle("/newmotion/", motionsHandler{})
|
||||
http.Handle("/proxy/", &decisionVoteHandler{})
|
||||
http.Handle("/vote/", &decisionVoteHandler{})
|
||||
http.Handle("/static/", http.FileServer(http.Dir(".")))
|
||||
http.Handle("/", http.RedirectHandler("/motions/", http.StatusMovedPermanently))
|
||||
|
||||
|
|
37
forms.go
37
forms.go
|
@ -92,3 +92,40 @@ func (f *EditDecisionForm) Validate() (bool, *Decision) {
|
|||
|
||||
return len(f.Errors) == 0, data
|
||||
}
|
||||
|
||||
type ProxyVoteForm struct {
|
||||
Voter string
|
||||
Vote string
|
||||
Justification string
|
||||
Errors map[string]string
|
||||
}
|
||||
|
||||
func (f *ProxyVoteForm) Validate() (bool, *Voter, *Vote, string) {
|
||||
f.Errors = make(map[string]string)
|
||||
|
||||
data := &Vote{}
|
||||
|
||||
var voter *Voter
|
||||
if voterId, err := strconv.ParseInt(f.Voter, 10, 64); err != nil {
|
||||
f.Errors["Voter"] = fmt.Sprint("Please choose a valid voter", err)
|
||||
} else if voter, err = GetVoterById(voterId); err != nil {
|
||||
f.Errors["Voter"] = fmt.Sprint("Please choose a valid voter", err)
|
||||
} else {
|
||||
data.VoterId = voter.Id
|
||||
}
|
||||
|
||||
if vote, err := strconv.ParseInt(f.Vote, 10, 8); err != nil {
|
||||
f.Errors["Vote"] = fmt.Sprint("Please choose a valid vote", err)
|
||||
} else if voteChoice, ok := VoteChoices[vote]; !ok {
|
||||
f.Errors["Vote"] = fmt.Sprint("Please choose a valid vote")
|
||||
} else {
|
||||
data.Vote = voteChoice
|
||||
}
|
||||
|
||||
justification := strings.TrimSpace(f.Justification)
|
||||
if len(justification) < 3 {
|
||||
f.Errors["Justification"] = "Please enter at least 3 characters."
|
||||
}
|
||||
|
||||
return len(f.Errors) == 0, voter, data, justification
|
||||
}
|
||||
|
|
2
jobs.go
2
jobs.go
|
@ -142,7 +142,7 @@ func (j *RemindVotersJob) Run() {
|
|||
return
|
||||
}
|
||||
if len(*decisions) > 0 {
|
||||
voterMail <- &RemindVoterNotification{voter: voter, decisions: *decisions}
|
||||
NotifyMailChannel <- &RemindVoterNotification{voter: voter, decisions: *decisions}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
124
models.go
124
models.go
|
@ -26,6 +26,10 @@ const (
|
|||
sqlGetNextPendingDecisionDue
|
||||
sqlGetReminderVoters
|
||||
sqlFindUnvotedDecisionsForVoter
|
||||
sqlGetEnabledVoterById
|
||||
sqlCreateVote
|
||||
sqlLoadVote
|
||||
sqlGetVotersForProxy
|
||||
)
|
||||
|
||||
var sqlStatements = map[sqlKey]string{
|
||||
|
@ -67,6 +71,10 @@ 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`,
|
||||
sqlGetEnabledVoterById: `
|
||||
SELECT id, name, enabled, reminder
|
||||
FROM voters
|
||||
WHERE enabled=1 AND id=$1`,
|
||||
sqlCountOlderThanDecision: `
|
||||
SELECT COUNT(*) > 0 FROM decisions WHERE proposed < $1`,
|
||||
sqlCountOlderThanUnvotedDecision: `
|
||||
|
@ -95,14 +103,23 @@ FROM decisions
|
|||
WHERE decisions.status=0 AND :now > due`,
|
||||
sqlGetNextPendingDecisionDue: `
|
||||
SELECT due FROM decisions WHERE status=0 ORDER BY due LIMIT 1`,
|
||||
sqlGetVotersForProxy: `
|
||||
SELECT id, name, reminder
|
||||
FROM voters WHERE enabled=1 AND id != $1 AND id NOT IN (SELECT voter FROM votes WHERE decision=$2)`,
|
||||
sqlGetReminderVoters: `
|
||||
SELECT id, name, reminder FROM voters WHERE enabled=1 AND reminder!='' AND reminder IS NOT NULL`,
|
||||
sqlFindUnvotedDecisionsForVoter: `
|
||||
SELECT tag, title, votetype, due
|
||||
FROM decisions
|
||||
WHERE status = 0 AND id NOT IN (SELECT decision FROM votes WHERE voter = $1)
|
||||
ORDER BY due ASC
|
||||
`,
|
||||
ORDER BY due ASC`,
|
||||
sqlCreateVote: `
|
||||
INSERT INTO votes (decision, voter, vote, voted, notes)
|
||||
VALUES (:decision, :voter, :vote, :voted, :notes)`,
|
||||
sqlLoadVote: `
|
||||
SELECT decision, voter, vote, voted, notes
|
||||
FROM votes
|
||||
WHERE decision=$1 AND voter=$2`,
|
||||
}
|
||||
|
||||
var db *sqlx.DB
|
||||
|
@ -122,9 +139,9 @@ type VoteType uint8
|
|||
type VoteStatus int8
|
||||
|
||||
type Decision struct {
|
||||
Id int
|
||||
Id int64
|
||||
Proposed time.Time
|
||||
ProponentId int `db:"proponent"`
|
||||
ProponentId int64 `db:"proponent"`
|
||||
Title string
|
||||
Content string
|
||||
Quorum int
|
||||
|
@ -137,12 +154,12 @@ type Decision struct {
|
|||
}
|
||||
|
||||
type Email struct {
|
||||
VoterId int `db:"voter"`
|
||||
VoterId int64 `db:"voter"`
|
||||
Address string
|
||||
}
|
||||
|
||||
type Voter struct {
|
||||
Id int
|
||||
Id int64
|
||||
Name string
|
||||
Enabled bool
|
||||
Reminder string // reminder email address
|
||||
|
@ -150,14 +167,6 @@ type Voter struct {
|
|||
|
||||
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
|
||||
|
@ -202,6 +211,18 @@ func (v VoteChoice) String() string {
|
|||
}
|
||||
}
|
||||
|
||||
var VoteValues = map[string]VoteChoice{
|
||||
"aye": voteAye,
|
||||
"naye": voteNaye,
|
||||
"abstain": voteAbstain,
|
||||
}
|
||||
|
||||
var VoteChoices = map[int64]VoteChoice{
|
||||
1: voteAye,
|
||||
0: voteAbstain,
|
||||
-1: voteNaye,
|
||||
}
|
||||
|
||||
const (
|
||||
voteStatusDeclined = -1
|
||||
voteStatusPending = 0
|
||||
|
@ -224,6 +245,43 @@ func (v VoteStatus) String() string {
|
|||
}
|
||||
}
|
||||
|
||||
type Vote struct {
|
||||
DecisionId int64 `db:"decision"`
|
||||
VoterId int64 `db:"voter"`
|
||||
Vote VoteChoice
|
||||
Voted time.Time
|
||||
Notes string
|
||||
}
|
||||
|
||||
func (v *Vote) Save() (err error) {
|
||||
insertVoteStmt, err := db.PrepareNamed(sqlStatements[sqlCreateVote])
|
||||
if err != nil {
|
||||
logger.Println("Error preparing statement:", err)
|
||||
return
|
||||
}
|
||||
defer insertVoteStmt.Close()
|
||||
|
||||
_, err = insertVoteStmt.Exec(v)
|
||||
if err != nil {
|
||||
logger.Println("Error saving vote:", err)
|
||||
return
|
||||
}
|
||||
|
||||
getVoteStmt, err := db.Preparex(sqlStatements[sqlLoadVote])
|
||||
if err != nil {
|
||||
logger.Println("Error preparing statement:", err)
|
||||
return
|
||||
}
|
||||
defer getVoteStmt.Close()
|
||||
|
||||
err = getVoteStmt.Get(v, v.DecisionId, v.VoterId)
|
||||
if err != nil {
|
||||
logger.Println("Error getting inserted vote:", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type VoteSums struct {
|
||||
Ayes int
|
||||
Nayes int
|
||||
|
@ -569,7 +627,7 @@ func (d *Decision) Close() (err error) {
|
|||
logger.Printf("WARNING wrong number of affected rows: %d (1 expected)\n", affectedRows)
|
||||
}
|
||||
|
||||
notifyMail <- &NotificationClosedDecision{decision: *d, voteSums: *voteSums}
|
||||
NotifyMailChannel <- NewNotificationClosedDecision(d, voteSums)
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -671,3 +729,39 @@ func FindUnvotedDecisionsForVoter(voter *Voter) (decisions *[]Decision, err erro
|
|||
|
||||
return
|
||||
}
|
||||
|
||||
func GetVoterById(id int64) (voter *Voter, err error) {
|
||||
getVoterByIdStmt, err := db.Preparex(sqlStatements[sqlGetEnabledVoterById])
|
||||
if err != nil {
|
||||
logger.Println("Error preparing statement:", err)
|
||||
return
|
||||
}
|
||||
defer getVoterByIdStmt.Close()
|
||||
|
||||
voter = &Voter{}
|
||||
if err = getVoterByIdStmt.Get(voter, id); err != nil {
|
||||
logger.Println("Error getting voter:", err)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func GetVotersForProxy(proxy *Voter, decision *Decision) (voters *[]Voter, err error) {
|
||||
getVotersForProxyStmt, err := db.Preparex(sqlStatements[sqlGetVotersForProxy])
|
||||
if err != nil {
|
||||
logger.Println("Error preparing statement:", err)
|
||||
return
|
||||
}
|
||||
defer getVotersForProxyStmt.Close()
|
||||
|
||||
votersSlice := make([]Voter, 0)
|
||||
|
||||
if err = getVotersForProxyStmt.Select(&votersSlice, proxy.Id, decision.Id); err != nil {
|
||||
logger.Println("Error getting voters for proxy:", err)
|
||||
return
|
||||
}
|
||||
voters = &votersSlice
|
||||
|
||||
return
|
||||
}
|
||||
|
|
143
notifications.go
143
notifications.go
|
@ -13,28 +13,16 @@ type NotificationMail interface {
|
|||
GetTemplate() string
|
||||
GetSubject() string
|
||||
GetHeaders() map[string]string
|
||||
}
|
||||
|
||||
type VoterMail interface {
|
||||
GetData() interface{}
|
||||
GetTemplate() string
|
||||
GetSubject() string
|
||||
GetRecipient() (string, string)
|
||||
}
|
||||
|
||||
var notifyMail = make(chan NotificationMail, 1)
|
||||
var voterMail = make(chan VoterMail, 1)
|
||||
var quitMailNotifier = make(chan int)
|
||||
var NotifyMailChannel = make(chan NotificationMail, 1)
|
||||
|
||||
func CloseMailNotifier() {
|
||||
quitMailNotifier <- 1
|
||||
}
|
||||
|
||||
func MailNotifier() {
|
||||
func MailNotifier(quitMailNotifier chan int) {
|
||||
logger.Println("Launched mail notifier")
|
||||
for {
|
||||
select {
|
||||
case notification := <-notifyMail:
|
||||
case notification := <-NotifyMailChannel:
|
||||
mailText, err := buildMail(notification.GetTemplate(), notification.GetData())
|
||||
if err != nil {
|
||||
logger.Println("ERROR building mail:", err)
|
||||
|
@ -42,32 +30,15 @@ func MailNotifier() {
|
|||
}
|
||||
|
||||
m := gomail.NewMessage()
|
||||
m.SetHeader("From", config.NoticeSenderAddress)
|
||||
m.SetHeader("To", config.BoardMailAddress)
|
||||
m.SetHeader("From", config.NotificationSenderAddress)
|
||||
address, name := notification.GetRecipient()
|
||||
m.SetAddressHeader("To", address, name)
|
||||
m.SetHeader("Subject", notification.GetSubject())
|
||||
for header, value := range notification.GetHeaders() {
|
||||
m.SetHeader(header, value)
|
||||
}
|
||||
m.SetBody("text/plain", mailText.String())
|
||||
|
||||
d := gomail.NewDialer(config.MailServer.Host, config.MailServer.Port, "", "")
|
||||
if err := d.DialAndSend(m); err != nil {
|
||||
logger.Println("ERROR sending mail:", err)
|
||||
}
|
||||
case notification := <-voterMail:
|
||||
mailText, err := buildMail(notification.GetTemplate(), notification.GetData())
|
||||
if err != nil {
|
||||
logger.Println("ERROR building mail:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
m := gomail.NewMessage()
|
||||
m.SetHeader("From", config.ReminderSenderAddress)
|
||||
address, name := notification.GetRecipient()
|
||||
m.SetAddressHeader("To", address, name)
|
||||
m.SetHeader("Subject", notification.GetSubject())
|
||||
m.SetBody("text/plain", mailText.String())
|
||||
|
||||
d := gomail.NewDialer(config.MailServer.Host, config.MailServer.Port, "", "")
|
||||
if err := d.DialAndSend(m); err != nil {
|
||||
logger.Println("ERROR sending mail:", err)
|
||||
|
@ -92,11 +63,36 @@ func buildMail(templateName string, context interface{}) (mailText *bytes.Buffer
|
|||
return
|
||||
}
|
||||
|
||||
type NotificationClosedDecision struct {
|
||||
type notificationBase struct{}
|
||||
|
||||
func (n *notificationBase) GetRecipient() (address string, name string) {
|
||||
address, name = config.BoardMailAddress, "CAcert board mailing list"
|
||||
return
|
||||
}
|
||||
|
||||
type decisionReplyBase struct {
|
||||
decision Decision
|
||||
}
|
||||
|
||||
func (n *decisionReplyBase) GetHeaders() map[string]string {
|
||||
return map[string]string{
|
||||
"References": fmt.Sprintf("<%s>", n.decision.Tag),
|
||||
"In-Reply-To": fmt.Sprintf("<%s>", n.decision.Tag),
|
||||
}
|
||||
}
|
||||
|
||||
type NotificationClosedDecision struct {
|
||||
notificationBase
|
||||
decisionReplyBase
|
||||
voteSums VoteSums
|
||||
}
|
||||
|
||||
func NewNotificationClosedDecision(decision *Decision, voteSums *VoteSums) *NotificationClosedDecision {
|
||||
notification := &NotificationClosedDecision{voteSums: *voteSums}
|
||||
notification.decision = *decision
|
||||
return notification
|
||||
}
|
||||
|
||||
func (n *NotificationClosedDecision) GetData() interface{} {
|
||||
return struct {
|
||||
*Decision
|
||||
|
@ -110,11 +106,8 @@ func (n *NotificationClosedDecision) GetSubject() string {
|
|||
return fmt.Sprintf("Re: %s - %s - finalised", n.decision.Tag, n.decision.Title)
|
||||
}
|
||||
|
||||
func (n *NotificationClosedDecision) GetHeaders() map[string]string {
|
||||
return map[string]string{"References": fmt.Sprintf("<%s>", n.decision.Tag)}
|
||||
}
|
||||
|
||||
type NotificationCreateMotion struct {
|
||||
notificationBase
|
||||
decision Decision
|
||||
voter Voter
|
||||
}
|
||||
|
@ -141,8 +134,15 @@ func (n *NotificationCreateMotion) GetHeaders() map[string]string {
|
|||
}
|
||||
|
||||
type NotificationUpdateMotion struct {
|
||||
decision Decision
|
||||
voter Voter
|
||||
notificationBase
|
||||
decisionReplyBase
|
||||
voter Voter
|
||||
}
|
||||
|
||||
func NewNotificationUpdateMotion(decision Decision, voter Voter) *NotificationUpdateMotion {
|
||||
notification := NotificationUpdateMotion{voter: voter}
|
||||
notification.decision = decision
|
||||
return ¬ification
|
||||
}
|
||||
|
||||
func (n *NotificationUpdateMotion) GetData() interface{} {
|
||||
|
@ -162,13 +162,16 @@ func (n *NotificationUpdateMotion) GetSubject() string {
|
|||
return fmt.Sprintf("Re: %s - %s", n.decision.Tag, n.decision.Title)
|
||||
}
|
||||
|
||||
func (n *NotificationUpdateMotion) GetHeaders() map[string]string {
|
||||
return map[string]string{"References": fmt.Sprintf("<%s>", n.decision.Tag)}
|
||||
type NotificationWithDrawMotion struct {
|
||||
notificationBase
|
||||
decisionReplyBase
|
||||
voter Voter
|
||||
}
|
||||
|
||||
type NotificationWithDrawMotion struct {
|
||||
decision Decision
|
||||
voter Voter
|
||||
func NewNotificationWithDrawMotion(decision *Decision, voter *Voter) *NotificationWithDrawMotion {
|
||||
notification := &NotificationWithDrawMotion{voter: *voter}
|
||||
notification.decision = *decision
|
||||
return notification
|
||||
}
|
||||
|
||||
func (n *NotificationWithDrawMotion) GetData() interface{} {
|
||||
|
@ -184,10 +187,6 @@ func (n *NotificationWithDrawMotion) GetSubject() string {
|
|||
return fmt.Sprintf("Re: %s - %s - withdrawn", n.decision.Tag, n.decision.Title)
|
||||
}
|
||||
|
||||
func (n *NotificationWithDrawMotion) GetHeaders() map[string]string {
|
||||
return map[string]string{"References": fmt.Sprintf("<%s>", n.decision.Tag)}
|
||||
}
|
||||
|
||||
type RemindVoterNotification struct {
|
||||
voter Voter
|
||||
decisions []Decision
|
||||
|
@ -205,7 +204,49 @@ func (n *RemindVoterNotification) GetTemplate() string { return "remind_voter_ma
|
|||
|
||||
func (n *RemindVoterNotification) GetSubject() string { return "Outstanding CAcert board votes" }
|
||||
|
||||
func (n *RemindVoterNotification) GetHeaders() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (n *RemindVoterNotification) GetRecipient() (address string, name string) {
|
||||
address, name = n.voter.Reminder, n.voter.Name
|
||||
return
|
||||
}
|
||||
|
||||
type voteNotificationBase struct{}
|
||||
|
||||
func (n *voteNotificationBase) GetRecipient() (address string, name string) {
|
||||
address, name = config.VoteNoticeAddress, "CAcert board votes mailing list"
|
||||
return
|
||||
}
|
||||
|
||||
type NotificationProxyVote struct {
|
||||
voteNotificationBase
|
||||
decisionReplyBase
|
||||
proxy Voter
|
||||
voter Voter
|
||||
vote Vote
|
||||
justification string
|
||||
}
|
||||
|
||||
func NewNotificationProxyVote(decision *Decision, proxy *Voter, voter *Voter, vote *Vote, justification string) *NotificationProxyVote {
|
||||
notification := &NotificationProxyVote{proxy: *proxy, voter: *voter, vote: *vote, justification: justification}
|
||||
notification.decision = *decision
|
||||
return notification
|
||||
}
|
||||
|
||||
func (n *NotificationProxyVote) GetData() interface{} {
|
||||
return struct {
|
||||
Proxy string
|
||||
Vote VoteChoice
|
||||
Voter string
|
||||
Decision *Decision
|
||||
Justification string
|
||||
}{n.proxy.Name, n.vote.Vote, n.voter.Name, &n.decision, n.justification}
|
||||
}
|
||||
|
||||
func (n *NotificationProxyVote) GetTemplate() string { return "proxy_vote_mail.txt" }
|
||||
|
||||
func (n *NotificationProxyVote) GetSubject() string {
|
||||
return fmt.Sprintf("Re: %s - %s", n.decision.Tag, n.decision.Title)
|
||||
}
|
||||
|
|
154
proxy.php
154
proxy.php
|
@ -1,154 +0,0 @@
|
|||
<?php
|
||||
if ($_SERVER['HTTPS'] != 'on') {
|
||||
header("HTTP/1.0 302 Redirect");
|
||||
header("Location: https://".$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']);
|
||||
exit();
|
||||
}
|
||||
require_once("database.php");
|
||||
$db = new DB();
|
||||
if (!($user = $db->auth())) {
|
||||
header("HTTP/1.0 302 Redirect");
|
||||
header("Location: denied.php");
|
||||
exit();
|
||||
}
|
||||
?>
|
||||
<html>
|
||||
<head>
|
||||
<title>CAcert Board Decisions</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset='UTF-8'" />
|
||||
<link rel="stylesheet" type="text/css" href="styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<?php
|
||||
if (!is_numeric($_REQUEST['motion'])) {
|
||||
?>
|
||||
<b>This is not a valid motion!</b><br/>
|
||||
<a href="motions.php">Back to motions</a><br/>
|
||||
<?php
|
||||
} else {
|
||||
$stmt = $db->getStatement("get decision");
|
||||
$stmt->bindParam(":decision",$_REQUEST['motion']);
|
||||
if ($stmt->execute() && ($decision=$stmt->fetch()) && ($decision['status'] == 0)) {
|
||||
if (is_numeric($_POST['voter']) && is_numeric($_POST['vote']) && is_numeric($_REQUEST['motion']) && ($_POST['justification'] != "")) {
|
||||
$stmt = $db->getStatement("del vote");
|
||||
$stmt->bindParam(":voter",$_REQUEST['voter']);
|
||||
$stmt->bindParam(":decision",$_REQUEST['motion']);
|
||||
if ($stmt->execute()) {
|
||||
$stmt = $db->getStatement("do vote");
|
||||
$stmt->bindParam(":voter",$_REQUEST['voter']);
|
||||
$stmt->bindParam(":decision",$_REQUEST['motion']);
|
||||
$stmt->bindParam(":vote",$_REQUEST['vote']);
|
||||
$notes = "Proxy-Vote by ".$user['name']."\n\n".$_REQUEST['justification']."\n\n".$_SERVER['SSL_CLIENT_CERT'];
|
||||
$stmt->bindParam(":notes",$notes);
|
||||
if ($stmt->execute()) {
|
||||
?>
|
||||
<b>The vote has been registered.</b><br/>
|
||||
<a href="motions.php">Back to motions</a>
|
||||
<?php
|
||||
$stmt = $db->getStatement("get voter by id");
|
||||
$stmt->bindParam(":id",$_REQUEST['voter']);
|
||||
if ($stmt->execute() && ($voter=$stmt->fetch())) {
|
||||
$voter = $voter['name'];
|
||||
} else {
|
||||
$voter = "Voter: ".$_REQUEST['voter'];
|
||||
}
|
||||
$name = $user['name'];
|
||||
$justification = $_REQUEST['justification'];
|
||||
$vote = '';
|
||||
switch($_REQUEST['vote']) {
|
||||
case 1 : $vote='Aye'; break;
|
||||
case -1: $vote='Naye'; break;
|
||||
default: $vote='Abstain'; break;
|
||||
}
|
||||
$tag = $decision['tag'];
|
||||
$title = $decision['title'];
|
||||
$content = $decision['content'];
|
||||
$due = $decision['due']." UTC";
|
||||
$body = <<<BODY
|
||||
Dear Board,
|
||||
|
||||
$name has just registered a proxy vote of $vote for $voter on motion $tag.
|
||||
|
||||
The justification for this was:
|
||||
$justification
|
||||
|
||||
Motion:
|
||||
$title
|
||||
$content
|
||||
|
||||
Kind regards,
|
||||
the vote system
|
||||
|
||||
BODY;
|
||||
$db->vote_notify("Re: $tag - $title",$body,$tag);
|
||||
} else {
|
||||
?>
|
||||
<b>The vote has NOT been registered.</b><br/>
|
||||
<a href="motions.php">Back to motions</a>
|
||||
<i><?php echo join("<br/>\n",$stmt->errorInfo()); ?></i>
|
||||
<?php
|
||||
}
|
||||
} else {
|
||||
?>
|
||||
<b>The vote has NOT been registered.</b><br/>
|
||||
<a href="motions.php">Back to motions</a>
|
||||
<i><?php echo join("<br/>\n",$stmt->errorInfo()); ?></i>
|
||||
<?php
|
||||
}
|
||||
} else {
|
||||
$stmt = $db->getStatement("get voters");
|
||||
if ($stmt->execute() && ($voters = $stmt->fetchAll())) {
|
||||
?>
|
||||
<form method="POST" action="?motion=<?php echo($_REQUEST['motion']); ?>">
|
||||
<table>
|
||||
<tr>
|
||||
<th>Voter</th><th>Vote</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><select name="voter"><?php
|
||||
foreach ($voters as $voter) {
|
||||
?>
|
||||
<option value="<?php echo($voter['id']); ?>"<?php if ($voter['id'] == $_POST['voter']) { echo(" selected=\"selected\""); } ?>><?php echo($voter['name']); ?></option>
|
||||
<?php
|
||||
}
|
||||
?></select></td>
|
||||
<td><select name="vote">
|
||||
<option value="1"<?php if (1 == $_POST['voter']) { echo(" selected=\"selected\""); } ?>>Aye</option>
|
||||
<option value="0"<?php if (0 == $_POST['voter']) { echo(" selected=\"selected\""); } ?>>Abstain</option>
|
||||
<option value="-1"<?php if (-1 == $_POST['voter']) { echo(" selected=\"selected\""); } ?>>Naye</option>
|
||||
</select></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="2">Justification:</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><textarea name="justification"><?php echo($_POST['justification']); ?></textarea></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><input type="submit" value="Proxy Vote" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
<?php
|
||||
} else {
|
||||
?>
|
||||
<b>Could not retrieve voters!</b><br/>
|
||||
<a href="motions.php">Back to motions</a><br/>
|
||||
<i><?php echo join("<br/>\n",$stmt->errorInfo()); ?></i>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<?php
|
||||
} else {
|
||||
?>
|
||||
<b>This is not a valid motion!</b><br/>
|
||||
<a href="motions.php">Back to motions</a><br/>
|
||||
<i><?php echo join("<br/>\n",$stmt->errorInfo()); ?></i>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
?>
|
||||
</body>
|
||||
</html>
|
61
templates/proxy_vote_form.html
Normal file
61
templates/proxy_vote_form.html
Normal file
|
@ -0,0 +1,61 @@
|
|||
{{ template "header" . }}
|
||||
{{ $form := .Form }}
|
||||
<table class="list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th>Motion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
{{ with .Decision }}
|
||||
{{ template "motion_fragment" .}}
|
||||
{{ end }}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<form action="/proxy/{{ .Decision.Tag }}" method="post">
|
||||
<table>
|
||||
<tr>
|
||||
<th>Voter</th>
|
||||
<th>Vote</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<select name="Voter">
|
||||
{{ range .Voters }}
|
||||
<option value="{{ .Id }}"
|
||||
{{ if eq (.Id | print) $form.Voter }}
|
||||
selected{{ end }}>{{ .Name }}</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<select name="Vote">
|
||||
<option value="1"{{ if eq $form.Vote "1" }}
|
||||
selected{{ end }}>Aye
|
||||
</option>
|
||||
<option value="0"{{ if eq $form.Vote "0" }}
|
||||
selected{{ end }}>Abstain
|
||||
</option>
|
||||
<option value="-1"{{ if eq $form.Vote "-1" }}
|
||||
selected{{ end }}>Naye
|
||||
</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="2">Justification:</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><textarea
|
||||
name="Justification">{{ $form.Justification }}</textarea>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2"><input type="submit" value="Proxy Vote"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
{{ template "footer" . }}
|
13
templates/proxy_vote_mail.txt
Normal file
13
templates/proxy_vote_mail.txt
Normal file
|
@ -0,0 +1,13 @@
|
|||
Dear Board,
|
||||
|
||||
{{ .Proxy }} has just registered a proxy vote of {{ .Vote }} for {{ .Voter }} on motion {{ .Decision.Tag }}.
|
||||
|
||||
The justification for this was:
|
||||
{{ .Justification }}
|
||||
|
||||
Motion:
|
||||
{{ .Decision.Title }}
|
||||
{{ .Decision.Content }}
|
||||
|
||||
Kind regards,
|
||||
the vote system
|
Loading…
Reference in a new issue