Implement motion editing
This commit is contained in:
parent
335ce16547
commit
2b98712aa8
7 changed files with 432 additions and 151 deletions
|
@ -25,10 +25,10 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.cacert.org/cacert-boardvoting/internal/forms"
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
|
|
||||||
"git.cacert.org/cacert-boardvoting/internal/models"
|
"git.cacert.org/cacert-boardvoting/internal/models"
|
||||||
"git.cacert.org/cacert-boardvoting/internal/validator"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func checkRole(v *models.User, roles []string) (bool, error) {
|
func checkRole(v *models.User, roles []string) (bool, error) {
|
||||||
|
@ -202,78 +202,19 @@ func (app *application) motionDetails(w http.ResponseWriter, r *http.Request) {
|
||||||
app.render(w, http.StatusOK, "motion.html", data)
|
app.render(w, http.StatusOK, "motion.html", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
type NewMotionForm struct {
|
|
||||||
Title string `form:"title"`
|
|
||||||
Content string `form:"content"`
|
|
||||||
Type *models.VoteType `form:"type"`
|
|
||||||
Due int `form:"due"`
|
|
||||||
User *models.User `form:"-"`
|
|
||||||
validator.Validator `form:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *application) newMotionForm(w http.ResponseWriter, r *http.Request) {
|
func (app *application) newMotionForm(w http.ResponseWriter, r *http.Request) {
|
||||||
data := app.newTemplateData(r, "motions", "all-motions")
|
data := app.newTemplateData(r, topLevelNavMotions, subLevelNavMotionsAll)
|
||||||
data.Form = &NewMotionForm{
|
data.Form = &forms.NewMotionForm{
|
||||||
User: data.User,
|
|
||||||
Type: models.VoteTypeMotion,
|
Type: models.VoteTypeMotion,
|
||||||
}
|
}
|
||||||
|
|
||||||
app.render(w, http.StatusOK, "create_motion.html", data)
|
app.render(w, http.StatusOK, "create_motion.html", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (form *NewMotionForm) Validate() {
|
|
||||||
const (
|
|
||||||
minimumTitleLength = 3
|
|
||||||
maximumTitleLength = 200
|
|
||||||
minimumContentLength = 3
|
|
||||||
maximumContentLength = 8000
|
|
||||||
|
|
||||||
threeDays = 3
|
|
||||||
oneWeek = 7
|
|
||||||
twoWeeks = 14
|
|
||||||
threeWeeks = 28
|
|
||||||
)
|
|
||||||
|
|
||||||
form.CheckField(
|
|
||||||
validator.NotBlank(form.Title),
|
|
||||||
"title",
|
|
||||||
"This field cannot be blank",
|
|
||||||
)
|
|
||||||
form.CheckField(
|
|
||||||
validator.MinChars(form.Title, minimumTitleLength),
|
|
||||||
"title",
|
|
||||||
fmt.Sprintf("This field must be at least %d characters long", minimumTitleLength),
|
|
||||||
)
|
|
||||||
form.CheckField(
|
|
||||||
validator.MaxChars(form.Title, maximumTitleLength),
|
|
||||||
"title",
|
|
||||||
fmt.Sprintf("This field must be at most %d characters long", maximumTitleLength),
|
|
||||||
)
|
|
||||||
form.CheckField(
|
|
||||||
validator.NotBlank(form.Content),
|
|
||||||
"content",
|
|
||||||
"This field cannot be blank",
|
|
||||||
)
|
|
||||||
form.CheckField(
|
|
||||||
validator.MinChars(form.Content, minimumContentLength),
|
|
||||||
"content",
|
|
||||||
fmt.Sprintf("This field must be at least %d characters long", minimumContentLength),
|
|
||||||
)
|
|
||||||
form.CheckField(
|
|
||||||
validator.MaxChars(form.Content, maximumContentLength),
|
|
||||||
"content",
|
|
||||||
fmt.Sprintf("This field must be at most %d characters long", maximumContentLength),
|
|
||||||
)
|
|
||||||
|
|
||||||
form.CheckField(validator.PermittedInt(
|
|
||||||
form.Due, threeDays, oneWeek, twoWeeks, threeWeeks), "due", "invalid duration choice",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *application) newMotionSubmit(w http.ResponseWriter, r *http.Request) {
|
|
||||||
const hoursInDay = 24
|
const hoursInDay = 24
|
||||||
|
|
||||||
var form NewMotionForm
|
func (app *application) newMotionSubmit(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var form forms.NewMotionForm
|
||||||
|
|
||||||
err := app.decodePostForm(r, &form)
|
err := app.decodePostForm(r, &form)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -285,8 +226,6 @@ func (app *application) newMotionSubmit(w http.ResponseWriter, r *http.Request)
|
||||||
form.Validate()
|
form.Validate()
|
||||||
|
|
||||||
if !form.Valid() {
|
if !form.Valid() {
|
||||||
form.User = &models.User{}
|
|
||||||
|
|
||||||
data := app.newTemplateData(r, topLevelNavMotions, subLevelNavMotionsAll)
|
data := app.newTemplateData(r, topLevelNavMotions, subLevelNavMotionsAll)
|
||||||
data.Form = form
|
data.Form = form
|
||||||
|
|
||||||
|
@ -328,20 +267,120 @@ func (app *application) newMotionSubmit(w http.ResponseWriter, r *http.Request)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.mailNotifier.notifyChannel <- &NewDecisionNotification{
|
app.mailNotifier.notifyChannel <- &NewDecisionNotification{
|
||||||
decision: &models.NewMotion{Decision: decision, Proposer: user},
|
Decision: &models.NewMotion{Decision: decision, Proposer: user},
|
||||||
}
|
}
|
||||||
|
|
||||||
app.sessionManager.Put(r.Context(), "flash", fmt.Sprintf("Started new motion %s: %s", decision.Tag, decision.Title))
|
app.sessionManager.Put(r.Context(), "flash", fmt.Sprintf("Started new motion %s: %s", decision.Tag, decision.Title))
|
||||||
|
|
||||||
http.Redirect(w, r, fmt.Sprintf("/motions/%s", decision.Tag), http.StatusSeeOther)
|
http.Redirect(w, r, "/motions/", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *application) editMotionForm(_ http.ResponseWriter, _ *http.Request) {
|
func (app *application) editMotionForm(w http.ResponseWriter, r *http.Request) {
|
||||||
panic("not implemented")
|
params := httprouter.ParamsFromContext(r.Context())
|
||||||
|
|
||||||
|
tag := params.ByName("tag")
|
||||||
|
|
||||||
|
motion, err := app.motions.GetMotionByTag(r.Context(), tag, false)
|
||||||
|
if err != nil {
|
||||||
|
app.serverError(w, err)
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *application) editMotionSubmit(_ http.ResponseWriter, _ *http.Request) {
|
if motion.ID == 0 {
|
||||||
panic("not implemented")
|
app.notFound(w)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data := app.newTemplateData(r, topLevelNavMotions, subLevelNavMotionsAll)
|
||||||
|
|
||||||
|
data.Motion = motion
|
||||||
|
|
||||||
|
data.Form = &forms.EditMotionForm{
|
||||||
|
Title: motion.Title,
|
||||||
|
Content: motion.Content,
|
||||||
|
Type: motion.Type,
|
||||||
|
}
|
||||||
|
|
||||||
|
app.render(w, http.StatusOK, "edit_motion.html", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *application) editMotionSubmit(w http.ResponseWriter, r *http.Request) {
|
||||||
|
params := httprouter.ParamsFromContext(r.Context())
|
||||||
|
|
||||||
|
tag := params.ByName("tag")
|
||||||
|
|
||||||
|
motion, err := app.motions.GetMotionByTag(r.Context(), tag, false)
|
||||||
|
if err != nil {
|
||||||
|
app.serverError(w, err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if motion.ID == 0 {
|
||||||
|
app.notFound(w)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var form forms.EditMotionForm
|
||||||
|
|
||||||
|
err = app.decodePostForm(r, &form)
|
||||||
|
if err != nil {
|
||||||
|
app.clientError(w, http.StatusBadRequest)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
form.Validate()
|
||||||
|
|
||||||
|
if !form.Valid() {
|
||||||
|
data := app.newTemplateData(r, topLevelNavMotions, subLevelNavMotionsAll)
|
||||||
|
data.Form = form
|
||||||
|
|
||||||
|
app.render(w, http.StatusUnprocessableEntity, "edit_motion.html", data)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := app.GetUser(r)
|
||||||
|
if err != nil {
|
||||||
|
app.clientError(w, http.StatusUnauthorized)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().UTC()
|
||||||
|
dueDuration := time.Duration(form.Due) * hoursInDay * time.Hour
|
||||||
|
|
||||||
|
err = app.motions.Update(
|
||||||
|
r.Context(),
|
||||||
|
motion.ID,
|
||||||
|
form.Type,
|
||||||
|
form.Title,
|
||||||
|
form.Content,
|
||||||
|
now.Add(dueDuration),
|
||||||
|
)
|
||||||
|
|
||||||
|
decision, err := app.motions.GetByID(r.Context(), motion.ID)
|
||||||
|
if err != nil {
|
||||||
|
app.serverError(w, err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
app.mailNotifier.notifyChannel <- &UpdateDecisionNotification{
|
||||||
|
Decision: &models.UpdatedMotion{Decision: decision, User: user},
|
||||||
|
}
|
||||||
|
|
||||||
|
app.sessionManager.Put(
|
||||||
|
r.Context(),
|
||||||
|
"flash",
|
||||||
|
fmt.Sprintf("The motion %s has been modified!", decision.Tag),
|
||||||
|
)
|
||||||
|
|
||||||
|
http.Redirect(w, r, "/motions/", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *application) withdrawMotionForm(_ http.ResponseWriter, _ *http.Request) {
|
func (app *application) withdrawMotionForm(_ http.ResponseWriter, _ *http.Request) {
|
||||||
|
|
|
@ -173,11 +173,11 @@ func (c *ClosedDecisionNotification) getHeaders() map[string][]string {
|
||||||
}
|
}
|
||||||
|
|
||||||
type NewDecisionNotification struct {
|
type NewDecisionNotification struct {
|
||||||
decision *models.NewMotion
|
Decision *models.NewMotion
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n NewDecisionNotification) GetNotificationContent(mc *mailConfig) *NotificationContent {
|
func (n NewDecisionNotification) GetNotificationContent(mc *mailConfig) *NotificationContent {
|
||||||
voteURL := fmt.Sprintf("/vote/%s", n.decision.Decision.Tag)
|
voteURL := fmt.Sprintf("/vote/%s", n.Decision.Decision.Tag)
|
||||||
unvotedURL := "/motions/?unvoted=1"
|
unvotedURL := "/motions/?unvoted=1"
|
||||||
|
|
||||||
return &NotificationContent{
|
return &NotificationContent{
|
||||||
|
@ -187,8 +187,8 @@ func (n NewDecisionNotification) GetNotificationContent(mc *mailConfig) *Notific
|
||||||
Name string
|
Name string
|
||||||
VoteURL string
|
VoteURL string
|
||||||
UnvotedURL string
|
UnvotedURL string
|
||||||
}{n.decision.Decision, n.decision.Proposer.Name, voteURL, unvotedURL},
|
}{n.Decision.Decision, n.Decision.Proposer.Name, voteURL, unvotedURL},
|
||||||
subject: fmt.Sprintf("%s - %s", n.decision.Decision.Tag, n.decision.Decision.Title),
|
subject: fmt.Sprintf("%s - %s", n.Decision.Decision.Tag, n.Decision.Decision.Title),
|
||||||
headers: n.getHeaders(),
|
headers: n.getHeaders(),
|
||||||
recipients: []recipientData{defaultRecipient(mc)},
|
recipients: []recipientData{defaultRecipient(mc)},
|
||||||
}
|
}
|
||||||
|
@ -196,6 +196,35 @@ func (n NewDecisionNotification) GetNotificationContent(mc *mailConfig) *Notific
|
||||||
|
|
||||||
func (n NewDecisionNotification) getHeaders() map[string][]string {
|
func (n NewDecisionNotification) getHeaders() map[string][]string {
|
||||||
return map[string][]string{
|
return map[string][]string{
|
||||||
"Message-ID": {fmt.Sprintf("<%s>", n.decision.Decision.Tag)},
|
"Message-ID": {fmt.Sprintf("<%s>", n.Decision.Decision.Tag)},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateDecisionNotification struct {
|
||||||
|
Decision *models.UpdatedMotion
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u UpdateDecisionNotification) GetNotificationContent(mc *mailConfig) *NotificationContent {
|
||||||
|
voteURL := fmt.Sprintf("/vote/%s", u.Decision.Decision.Tag)
|
||||||
|
unvotedURL := "/motions/?unvoted=1"
|
||||||
|
|
||||||
|
return &NotificationContent{
|
||||||
|
template: "update_motion_mail.txt",
|
||||||
|
data: struct {
|
||||||
|
*models.Motion
|
||||||
|
Name string
|
||||||
|
VoteURL string
|
||||||
|
UnvotedURL string
|
||||||
|
}{u.Decision.Decision, u.Decision.User.Name, voteURL, unvotedURL},
|
||||||
|
subject: fmt.Sprintf("%s - %s", u.Decision.Decision.Tag, u.Decision.Decision.Title),
|
||||||
|
headers: u.getHeaders(),
|
||||||
|
recipients: []recipientData{defaultRecipient(mc)},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u UpdateDecisionNotification) getHeaders() map[string][]string {
|
||||||
|
return map[string][]string{
|
||||||
|
"References": {fmt.Sprintf("<%s>", u.Decision.Decision.Tag)},
|
||||||
|
"In-Reply-To": {fmt.Sprintf("<%s>", u.Decision.Decision.Tag)},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
127
internal/forms/forms.go
Normal file
127
internal/forms/forms.go
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 CAcert Inc.
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package forms
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.cacert.org/cacert-boardvoting/internal/models"
|
||||||
|
"git.cacert.org/cacert-boardvoting/internal/validator"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NewMotionForm struct {
|
||||||
|
Title string `form:"title"`
|
||||||
|
Content string `form:"content"`
|
||||||
|
Type *models.VoteType `form:"type"`
|
||||||
|
Due int `form:"due"`
|
||||||
|
validator.Validator `form:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
minimumTitleLength = 3
|
||||||
|
maximumTitleLength = 200
|
||||||
|
minimumContentLength = 3
|
||||||
|
maximumContentLength = 8000
|
||||||
|
|
||||||
|
threeDays = 3
|
||||||
|
oneWeek = 7
|
||||||
|
twoWeeks = 14
|
||||||
|
threeWeeks = 28
|
||||||
|
)
|
||||||
|
|
||||||
|
func (f *NewMotionForm) Validate() {
|
||||||
|
f.CheckField(
|
||||||
|
validator.NotBlank(f.Title),
|
||||||
|
"title",
|
||||||
|
"This field cannot be blank",
|
||||||
|
)
|
||||||
|
f.CheckField(
|
||||||
|
validator.MinChars(f.Title, minimumTitleLength),
|
||||||
|
"title",
|
||||||
|
fmt.Sprintf("This field must be at least %d characters long", minimumTitleLength),
|
||||||
|
)
|
||||||
|
f.CheckField(
|
||||||
|
validator.MaxChars(f.Title, maximumTitleLength),
|
||||||
|
"title",
|
||||||
|
fmt.Sprintf("This field must be at most %d characters long", maximumTitleLength),
|
||||||
|
)
|
||||||
|
f.CheckField(
|
||||||
|
validator.NotBlank(f.Content),
|
||||||
|
"content",
|
||||||
|
"This field cannot be blank",
|
||||||
|
)
|
||||||
|
f.CheckField(
|
||||||
|
validator.MinChars(f.Content, minimumContentLength),
|
||||||
|
"content",
|
||||||
|
fmt.Sprintf("This field must be at least %d characters long", minimumContentLength),
|
||||||
|
)
|
||||||
|
f.CheckField(
|
||||||
|
validator.MaxChars(f.Content, maximumContentLength),
|
||||||
|
"content",
|
||||||
|
fmt.Sprintf("This field must be at most %d characters long", maximumContentLength),
|
||||||
|
)
|
||||||
|
|
||||||
|
f.CheckField(validator.PermittedInt(
|
||||||
|
f.Due, threeDays, oneWeek, twoWeeks, threeWeeks), "due", "invalid duration choice",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type EditMotionForm struct {
|
||||||
|
Title string `form:"title"`
|
||||||
|
Content string `form:"content"`
|
||||||
|
Type *models.VoteType `form:"type"`
|
||||||
|
Due int `form:"due"`
|
||||||
|
validator.Validator `form:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f EditMotionForm) Validate() {
|
||||||
|
f.CheckField(
|
||||||
|
validator.NotBlank(f.Title),
|
||||||
|
"title",
|
||||||
|
"This field cannot be blank",
|
||||||
|
)
|
||||||
|
f.CheckField(
|
||||||
|
validator.MinChars(f.Title, minimumTitleLength),
|
||||||
|
"title",
|
||||||
|
fmt.Sprintf("This field must be at least %d characters long", minimumTitleLength),
|
||||||
|
)
|
||||||
|
f.CheckField(
|
||||||
|
validator.MaxChars(f.Title, maximumTitleLength),
|
||||||
|
"title",
|
||||||
|
fmt.Sprintf("This field must be at most %d characters long", maximumTitleLength),
|
||||||
|
)
|
||||||
|
f.CheckField(
|
||||||
|
validator.NotBlank(f.Content),
|
||||||
|
"content",
|
||||||
|
"This field cannot be blank",
|
||||||
|
)
|
||||||
|
f.CheckField(
|
||||||
|
validator.MinChars(f.Content, minimumContentLength),
|
||||||
|
"content",
|
||||||
|
fmt.Sprintf("This field must be at least %d characters long", minimumContentLength),
|
||||||
|
)
|
||||||
|
f.CheckField(
|
||||||
|
validator.MaxChars(f.Content, maximumContentLength),
|
||||||
|
"content",
|
||||||
|
fmt.Sprintf("This field must be at most %d characters long", maximumContentLength),
|
||||||
|
)
|
||||||
|
|
||||||
|
f.CheckField(validator.PermittedInt(
|
||||||
|
f.Due, threeDays, oneWeek, twoWeeks, threeWeeks), "due", "invalid duration choice",
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,26 +1,26 @@
|
||||||
Dear Board,
|
Dear Board,
|
||||||
|
|
||||||
{{ .Name }} has modified motion {{ .Tag }} to the following:
|
{{ .Data.Name }} has modified motion {{ .Data.Tag }} to the following:
|
||||||
|
|
||||||
{{ .Title }}
|
{{ .Data.Title }}
|
||||||
|
|
||||||
{{ wrap 76 .Content }}
|
{{ wrap 76 .Data.Content }}
|
||||||
|
|
||||||
Vote type: {{ .VoteType }}
|
Vote type: {{ .Data.Type }}
|
||||||
|
|
||||||
Voting will close {{ .Due }}
|
Voting will close {{ .Data.Due }}
|
||||||
|
|
||||||
To vote please choose:
|
To vote please choose:
|
||||||
|
|
||||||
Aye: {{ .VoteURL }}/aye
|
Aye: {{ .BaseURL }}{{ .Data.VoteURL }}/aye
|
||||||
Naye: {{ .VoteURL }}/naye
|
Naye: {{ .BaseURL }}{{ .Data.VoteURL }}/naye
|
||||||
Abstain: {{ .VoteURL }}/abstain
|
Abstain: {{ .BaseURL }}{{ .Data.VoteURL }}/abstain
|
||||||
|
|
||||||
Please be aware, that if you have voted already your vote is still
|
Please be aware, that if you have voted already your vote is still
|
||||||
registered and valid. If this modification has an impact on how you wish to
|
registered and valid. If this modification has an impact on how you wish to
|
||||||
vote, you are responsible for voting again.
|
vote, you are responsible for voting again.
|
||||||
|
|
||||||
To see all your pending votes: {{ .UnvotedURL }}
|
To see all your pending votes: {{ .BaseURL }}{{ .Data.UnvotedURL }}
|
||||||
|
|
||||||
Kind regards,
|
Kind regards,
|
||||||
the voting system
|
the voting system
|
|
@ -195,6 +195,11 @@ type NewMotion struct {
|
||||||
Proposer *User
|
Proposer *User
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UpdatedMotion struct {
|
||||||
|
Decision *Motion
|
||||||
|
User *User
|
||||||
|
}
|
||||||
|
|
||||||
type MotionModel struct {
|
type MotionModel struct {
|
||||||
DB *sqlx.DB
|
DB *sqlx.DB
|
||||||
InfoLog *log.Logger
|
InfoLog *log.Logger
|
||||||
|
@ -750,7 +755,7 @@ WHERE decisions.tag = ?`,
|
||||||
return nil, fmt.Errorf("could not get vote sums: %w", err)
|
return nil, fmt.Errorf("could not get vote sums: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if withVotes {
|
if result.ID != 0 && withVotes {
|
||||||
if err := m.FillVotes(ctx, &result); err != nil {
|
if err := m.FillVotes(ctx, &result); err != nil {
|
||||||
return nil, fmt.Errorf("could not get votes for %s: %w", result.Tag, err)
|
return nil, fmt.Errorf("could not get votes for %s: %w", result.Tag, err)
|
||||||
}
|
}
|
||||||
|
@ -811,3 +816,16 @@ func (m *MotionModel) GetByID(ctx context.Context, id int64) (*Motion, error) {
|
||||||
|
|
||||||
return &motion, nil
|
return &motion, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MotionModel) Update(ctx context.Context, id int64, voteType *VoteType, title string, content string, due time.Time) error {
|
||||||
|
_, err := m.DB.ExecContext(
|
||||||
|
ctx,
|
||||||
|
`UPDATE decisions SET title=?, content=?, votetype=?, due=? WHERE id=?`,
|
||||||
|
title, content, voteType, due.UTC(), id,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not update decision: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
{{ define "title" }}New motion{{ end }}
|
{{ define "title" }}New motion{{ end }}
|
||||||
|
|
||||||
{{ define "main" }}
|
{{ define "main" }}
|
||||||
<div class="ui raised segment">
|
|
||||||
<form action="/newmotion/" method="post">
|
<form action="/newmotion/" method="post">
|
||||||
<input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
|
<input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
|
||||||
<div class="ui form{{ if .Form.FieldErrors }} error{{ end }}">
|
<div class="ui form segment{{ if .Form.FieldErrors }} error{{ end }}">
|
||||||
<div class="three fields">
|
<div class="three fields">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>ID:</label>
|
<label>ID:</label>
|
||||||
|
@ -12,7 +11,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Proponent:</label>
|
<label>Proponent:</label>
|
||||||
{{ .Form.User.Name }}
|
{{ .User.Name }}
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Proposed date/time:</label>
|
<label>Proposed date/time:</label>
|
||||||
|
@ -68,5 +67,4 @@
|
||||||
<button class="ui button" type="submit">Propose</button>
|
<button class="ui button" type="submit">Propose</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
{{ end }}
|
{{ end }}
|
70
ui/html/pages/edit_motion.html
Normal file
70
ui/html/pages/edit_motion.html
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
{{ define "title" }}Edit motion {{ .Motion.Tag }}{{ end }}
|
||||||
|
|
||||||
|
{{ define "main" }}
|
||||||
|
<form action="/motions/{{ .Motion.Tag }}/edit" method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
|
||||||
|
<div class="ui form segment{{ if .Form.FieldErrors }} error{{ end }}">
|
||||||
|
<div class="three fields">
|
||||||
|
<div class="field">
|
||||||
|
<label>ID:</label>
|
||||||
|
{{ .Motion.Tag }}
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Proponent:</label>
|
||||||
|
{{ .User.Name }}
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Proposed date/time:</label>
|
||||||
|
{{ .Motion.Proposed }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="required field{{ if .Form.FieldErrors.title }} error{{ end }}">
|
||||||
|
<label for="title">Title:</label>
|
||||||
|
<input id="title" name="title" type="text" value="{{ .Form.Title }}">
|
||||||
|
{{ if .Form.FieldErrors.title }}
|
||||||
|
<span class="ui small error text">{{ .Form.FieldErrors.title }}</span>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
<div class="required field{{ if .Form.FieldErrors.content }} error{{ end }}">
|
||||||
|
<label for="content">Text:</label>
|
||||||
|
<textarea id="content" name="content">{{ .Form.Content }}</textarea>
|
||||||
|
{{ if .Form.FieldErrors.content }}
|
||||||
|
<span class="ui small error text">{{ .Form.FieldErrors.content }}</span>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
<div class="two fields">
|
||||||
|
<div class="required field{{ if .Form.FieldErrors.type }} error{{ end }}">
|
||||||
|
<label for="type">Vote type:</label>
|
||||||
|
{{ $voteType := toString .Form.Type }}
|
||||||
|
<select id="type" name="type">
|
||||||
|
<option value="motion"
|
||||||
|
{{ if eq "motion" $voteType }}selected{{ end }}>
|
||||||
|
Motion
|
||||||
|
</option>
|
||||||
|
<option value="veto"
|
||||||
|
{{ if eq "veto" $voteType }}selected{{ end }}>
|
||||||
|
Veto
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
{{ if .Form.FieldErrors.type }}
|
||||||
|
<span class="ui small error text">{{ .Form.FieldErrors.type}}</span>
|
||||||
|
{{ end}}
|
||||||
|
</div>
|
||||||
|
<div class="required field{{ if .Form.FieldErrors.due }} error{{ end }}">
|
||||||
|
<label for="due">Due: (autofilled from chosen
|
||||||
|
option)</label>
|
||||||
|
<select id="due" name="due">
|
||||||
|
<option value="3"{{ if eq 3 .Form.Due }} selected{{ end }}>In 3 Days</option>
|
||||||
|
<option value="7"{{ if eq 7 .Form.Due }} selected{{ end }}>In 1 Week</option>
|
||||||
|
<option value="14"{{ if eq 14 .Form.Due }} selected{{ end }}>In 2 Weeks</option>
|
||||||
|
<option value="28"{{ if eq 28 .Form.Due }} selected{{ end }}>In 4 Weeks</option>
|
||||||
|
</select>
|
||||||
|
{{ if .Form.FieldErrors.due }}
|
||||||
|
<span class="ui small error text">{{ .Form.FieldErrors.due }}</span>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="ui button" type="submit">Modify</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{{ end }}
|
Loading…
Reference in a new issue