Partialy add new motion creation

debian
Jan Dittberner 7 years ago
parent 57e3d53245
commit 471daf12ea

@ -6,6 +6,18 @@ import (
"text/template"
)
func CreateMotion(decision *Decision, voter *Voter) (err error) {
decision.ProponentId = voter.Id
err = decision.Save()
if err != nil {
logger.Println("Error saving motion:", err)
return
}
// TODO: implement fetching new decision, implement mail
return
}
func WithdrawMotion(decision *Decision, voter *Voter) (err error) {
// load template, fill name, tag, title, content
type mailContext struct {

@ -4,8 +4,10 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"fmt"
"github.com/Masterminds/sprig"
"github.com/gorilla/sessions"
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
"gopkg.in/yaml.v2"
@ -20,17 +22,20 @@ import (
var logger *log.Logger
var config *Config
var store *sessions.CookieStore
func getTemplateFilenames(tmpl []string) (result []string) {
result = make([]string, len(tmpl))
for i := range tmpl {
result[i] = fmt.Sprintf("templates/%s", tmpl[i])
const sessionCookieName = "votesession"
func getTemplateFilenames(templates []string) (result []string) {
result = make([]string, len(templates))
for i := range templates {
result[i] = fmt.Sprintf("templates/%s", templates[i])
}
return result
}
func renderTemplate(w http.ResponseWriter, tmpl []string, context interface{}) {
t := template.Must(template.New(tmpl[0]).Funcs(sprig.FuncMap()).ParseFiles(getTemplateFilenames(tmpl)...))
func renderTemplate(w http.ResponseWriter, templates []string, context interface{}) {
t := template.Must(template.New(templates[0]).Funcs(sprig.FuncMap()).ParseFiles(getTemplateFilenames(templates)...))
if err := t.Execute(w, context); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
@ -76,7 +81,7 @@ type motionParameters struct {
}
type motionListParameters struct {
Page int64
Page int64
Flags struct {
Confirmed, Withdraw, Unvoted bool
}
@ -85,7 +90,6 @@ type motionListParameters struct {
func parseMotionParameters(r *http.Request) motionParameters {
var m = motionParameters{}
m.ShowVotes, _ = strconv.ParseBool(r.URL.Query().Get("showvotes"))
logger.Printf("parsed parameters: %+v\n", m)
return m
}
@ -102,12 +106,16 @@ func parseMotionListParameters(r *http.Request) motionListParameters {
if r.Method == http.MethodPost {
m.Flags.Confirmed, _ = strconv.ParseBool(r.PostFormValue("confirm"))
}
logger.Printf("parsed parameters: %+v\n", m)
return m
}
func motionListHandler(w http.ResponseWriter, r *http.Request) {
params := parseMotionListParameters(r)
session, err := store.Get(r, sessionCookieName)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
var templateContext struct {
Decisions []*DecisionForDisplay
@ -115,12 +123,16 @@ func motionListHandler(w http.ResponseWriter, r *http.Request) {
Params *motionListParameters
PrevPage, NextPage int64
PageTitle string
Flashes interface{}
}
if voter, ok := r.Context().Value(ctxVoter).(*Voter); ok {
templateContext.Voter = voter
}
if flashes := session.Flashes(); len(flashes) > 0 {
templateContext.Flashes = flashes
}
session.Save(r, w)
templateContext.Params = &params
var err error
if params.Flags.Unvoted && templateContext.Voter != nil {
if templateContext.Decisions, err = FindVotersUnvotedDecisionsForDisplayOnPage(
@ -168,6 +180,7 @@ func motionHandler(w http.ResponseWriter, r *http.Request) {
Params *motionParameters
PrevPage, NextPage int64
PageTitle string
Flashes interface{}
}
voter, ok := getVoterFromRequest(r)
if ok {
@ -304,16 +317,61 @@ func (h motionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
})
return
default:
fmt.Fprintf(w, "No handler for '%s'", subURL)
http.NotFound(w, r)
return
}
}
func newMotionHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "New motion")
voter, _ := getVoterFromRequest(r)
fmt.Fprintf(w, "%+v\n", voter)
// TODO: implement
voter, ok := getVoterFromRequest(r)
if !ok {
http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
}
templates := []string{"newmotion_form.html", "header.html", "footer.html"}
var templateContext struct {
Form NewDecisionForm
PageTitle string
Voter *Voter
Flashes interface{}
}
switch r.Method {
case http.MethodPost:
form := NewDecisionForm{
Title: r.FormValue("Title"),
Content: r.FormValue("Content"),
VoteType: r.FormValue("VoteType"),
Due: r.FormValue("Due"),
}
if valid, data := form.Validate(); !valid {
templateContext.Voter = voter
templateContext.Form = form
renderTemplate(w, templates, templateContext)
} else {
if err := CreateMotion(data, voter); err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
session, err := store.Get(r, sessionCookieName)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
session.AddFlash("The motion has been proposed!")
session.Save(r, w)
http.Redirect(w, r, "/motions/", http.StatusTemporaryRedirect)
}
return
default:
templateContext.Voter = voter
templateContext.Form = NewDecisionForm{
VoteType: strconv.FormatInt(voteTypeMotion, 10),
}
renderTemplate(w, templates, templateContext)
}
}
type Config struct {
@ -323,30 +381,33 @@ type Config struct {
ClientCACertificates string `yaml:"client_ca_certificates"`
ServerCert string `yaml:"server_certificate"`
ServerKey string `yaml:"server_key"`
CookieSecret string `yaml:"cookie_secret"`
}
func main() {
func init() {
logger = log.New(os.Stderr, "boardvoting: ", log.LstdFlags|log.LUTC|log.Lshortfile)
var filename = "config.yaml"
if len(os.Args) == 2 {
filename = os.Args[1]
}
var err error
var source []byte
source, err = ioutil.ReadFile(filename)
source, err := ioutil.ReadFile("config.yaml")
if err != nil {
logger.Fatal(err)
}
err = yaml.Unmarshal(source, &config)
if err := yaml.Unmarshal(source, &config); err != nil {
logger.Fatal(err)
}
cookieSecret, err := base64.StdEncoding.DecodeString(config.CookieSecret)
if err != nil {
logger.Fatal(err)
}
logger.Printf("read configuration %v", config)
if len(cookieSecret) < 32 {
logger.Fatalln("Cookie secret is less than 32 bytes long")
}
store = sessions.NewCookieStore(cookieSecret)
logger.Println("read configuration")
}
func main() {
var err error
db, err = sqlx.Open("sqlite3", config.DatabaseFile)
if err != nil {
logger.Fatal(err)

@ -4,4 +4,5 @@ notice_sender_address: cacert-board-votes@lists.cacert.org
database_file: database.sqlite
client_ca_certificates: cacert_class3.pem
server_certificate: server.crt
server_key: server.key
server_key: server.key
cookie_secret: base64encoded_random_byte_value_of_at_least_32_bytes

@ -0,0 +1,54 @@
package main
import (
"fmt"
"strconv"
"strings"
"time"
)
var validDueDurations = map[string]time.Duration{
"+3 days": time.Hour * 24 * 3,
"+7 days": time.Hour * 24 * 7,
"+14 days": time.Hour * 24 * 14,
"+28 days": time.Hour * 24 * 28,
}
type NewDecisionForm struct {
Title string
Content string
VoteType string
Due string
Errors map[string]string
}
func (f *NewDecisionForm) Validate() (bool, *Decision) {
f.Errors = make(map[string]string)
data := &Decision{}
data.Title = strings.TrimSpace(f.Title)
if len(data.Title) < 3 {
f.Errors["Title"] = "Please enter at least 3 characters."
}
data.Content = strings.TrimSpace(f.Content)
if len(strings.Fields(data.Content)) < 3 {
f.Errors["Content"] = "Please enter at least 3 words."
}
if voteType, err := strconv.ParseUint(f.VoteType, 10, 8); err != nil || (voteType != 0 && voteType != 1) {
f.Errors["VoteType"] = fmt.Sprint("Please choose a valid vote type", err)
} else {
data.VoteType = VoteType(uint8(voteType))
}
if dueDuration, ok := validDueDurations[f.Due]; !ok {
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)
}
return len(f.Errors) == 0, data
}

@ -51,12 +51,26 @@ 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 = `
INSERT INTO decisions (
proposed, proponent, title, content, votetype, status, due, modified,tag
) VALUES (
datetime('now','utc'), :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')
)
)
`
)
var db *sqlx.DB
type VoteType int
type VoteStatus int
type VoteType uint8
type VoteStatus int8
type Decision struct {
Id int
@ -332,6 +346,25 @@ func (d *Decision) OlderExists() (result bool, err error) {
return
}
func (d *Decision) Save() (err error) {
insertDecisionStmt, err := db.PrepareNamed(sqlCreateDecision)
if err != nil {
logger.Println("Error preparing statement:", err)
return
}
defer insertDecisionStmt.Close()
result, err := insertDecisionStmt.Exec(d)
if err != nil {
logger.Println("Error creating motion:", err)
return
}
logger.Println(result)
// TODO: implement fetch last id from result
// TODO: load decision from DB
return
}
func FindVoterByAddress(emailAddress string) (voter *Voter, err error) {
findVoterStmt, err := db.Preparex(sqlGetVoter)
if err != nil {

@ -12,4 +12,11 @@ CAcert Board Decisions{{ if .PageTitle }} - {{ .PageTitle }}{{ end}}
</head>
<body>
<h1>{{ template "pagetitle" . }}</h1>
{{ with .Flashes }}
<ul class="flash-messages">
{{ range . }}
<li>{{ . }}</li>
{{ end }}
</ul>
{{ end }}
{{ end }}

@ -0,0 +1,73 @@
{{ template "header" . }}
<form action="/newmotion/" method="post">
<table>
<tr>
<td>ID:</td>
<td>(generated on submit)</td>
</tr>
<tr>
<td>Proponent:</td>
<td>{{ .Voter.Name }}</td>
</tr>
<tr>
<td>Proposed date/time:</td>
<td>(auto filled to current date/time)</td>
</tr>
<tr>
<td>Title:</td>
<td><input name="Title" value="{{ .Form.Title }}"/>
{{ with .Form.Errors.Title }}
<span class="error">{{ . }}</span>
{{ end }}
</td>
</tr>
<tr>
<td>Text:</td>
<td><textarea name="Content">{{ .Form.Content }}</textarea>
{{ with .Form.Errors.Content }}
<span class="error">{{ . }}</span>
{{ end }}
</td>
</tr>
<tr>
<td>Vote type:</td>
<td>
<select name="VoteType">
<option value="0"
{{ if eq "0" .Form.VoteType }}selected{{ end }}>
Motion
</option>
<option value="1"
{{ if eq "1" .Form.VoteType }}selected{{ end }}>Veto
</option>
</select>
{{ with .Form.Errors.VoteType }}
<span class="error">{{ . }}</span>
{{ end }}
</td>
</tr>
<tr>
<td rowspan="2">Due:</td>
<td>(autofilled from option below)</td>
</tr>
<tr>
<td>
<select name="Due">
<option value="+3 days">In 3 Days</option>
<option value="+7 days">In 1 Week</option>
<option value="+14 days">In 2 Weeks</option>
<option value="+28 days">In 4 Weeks</option>
</select>
{{ with .Form.Errors.Due }}
<span class="error">{{ . }}</span>
{{ end }}
</td>
</tr>
<tr>
<td>&nbsp;</td>
<td><input type="submit" value="Propose"/></td>
</tr>
</table>
</form>
<a href="/motions/">Back to motions</a>
{{ template "footer" . }}
Loading…
Cancel
Save