Implement proxy voting

This commit is contained in:
Jan Dittberner 2017-04-21 02:25:49 +02:00
parent b6ad5d8ad3
commit 2cac50ee86
8 changed files with 486 additions and 236 deletions

View file

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

View file

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

View file

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

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

View file

@ -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 &notification
}
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
View file

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

View 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" . }}

View 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