2022-05-09 19:09:24 +00:00
|
|
|
/*
|
|
|
|
Copyright 2017-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 main
|
|
|
|
|
|
|
|
import (
|
2022-05-21 19:02:15 +00:00
|
|
|
"bytes"
|
2022-05-22 12:08:02 +00:00
|
|
|
"database/sql"
|
|
|
|
"errors"
|
2022-05-21 18:49:35 +00:00
|
|
|
"fmt"
|
2022-05-09 19:09:24 +00:00
|
|
|
"html/template"
|
2022-05-21 18:57:32 +00:00
|
|
|
"io/fs"
|
2022-05-09 19:09:24 +00:00
|
|
|
"net/http"
|
2022-05-21 18:49:35 +00:00
|
|
|
"path/filepath"
|
2022-05-21 17:18:17 +00:00
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
2022-05-22 12:08:02 +00:00
|
|
|
"github.com/Masterminds/sprig/v3"
|
|
|
|
"github.com/gorilla/csrf"
|
|
|
|
"github.com/julienschmidt/httprouter"
|
2022-05-22 19:47:27 +00:00
|
|
|
|
|
|
|
"git.cacert.org/cacert-boardvoting/internal/models"
|
|
|
|
"git.cacert.org/cacert-boardvoting/internal/validator"
|
|
|
|
"git.cacert.org/cacert-boardvoting/ui"
|
2022-05-21 18:49:35 +00:00
|
|
|
)
|
2022-05-21 17:18:17 +00:00
|
|
|
|
2022-05-26 13:27:25 +00:00
|
|
|
func checkRole(v *models.User, roles []string) (bool, error) {
|
|
|
|
hasRole, err := v.HasRole(roles)
|
|
|
|
if err != nil {
|
|
|
|
return false, fmt.Errorf("could not determine user roles: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return hasRole, nil
|
|
|
|
}
|
|
|
|
|
2022-05-21 18:49:35 +00:00
|
|
|
func newTemplateCache() (map[string]*template.Template, error) {
|
|
|
|
cache := map[string]*template.Template{}
|
2022-05-09 19:09:24 +00:00
|
|
|
|
2022-05-21 18:57:32 +00:00
|
|
|
pages, err := fs.Glob(ui.Files, "html/pages/*.html")
|
2022-05-21 18:49:35 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("could not find page templates: %w", err)
|
2022-05-09 19:09:24 +00:00
|
|
|
}
|
|
|
|
|
2022-05-21 18:57:32 +00:00
|
|
|
funcMaps := sprig.FuncMap()
|
|
|
|
funcMaps["nl2br"] = func(text string) template.HTML {
|
|
|
|
// #nosec G203 input is sanitized
|
|
|
|
return template.HTML(strings.ReplaceAll(template.HTMLEscapeString(text), "\n", "<br>"))
|
|
|
|
}
|
2022-05-26 13:27:25 +00:00
|
|
|
funcMaps["canManageUsers"] = func(v *models.User) (bool, error) {
|
|
|
|
return checkRole(v, []string{models.RoleSecretary, models.RoleAdmin})
|
|
|
|
}
|
|
|
|
funcMaps["canVote"] = func(v *models.User) (bool, error) {
|
|
|
|
return checkRole(v, []string{models.RoleVoter})
|
|
|
|
}
|
|
|
|
funcMaps["canStartVote"] = func(v *models.User) (bool, error) {
|
|
|
|
return checkRole(v, []string{models.RoleVoter})
|
2022-05-21 18:57:32 +00:00
|
|
|
}
|
|
|
|
funcMaps[csrf.TemplateTag] = csrf.TemplateField
|
|
|
|
|
2022-05-21 18:49:35 +00:00
|
|
|
for _, page := range pages {
|
|
|
|
name := filepath.Base(page)
|
|
|
|
|
2022-05-21 18:57:32 +00:00
|
|
|
ts, err := template.New("").Funcs(funcMaps).ParseFS(
|
|
|
|
ui.Files,
|
|
|
|
"html/base.html",
|
|
|
|
"html/partials/*.html",
|
2022-05-21 18:49:35 +00:00
|
|
|
page,
|
2022-05-21 18:57:32 +00:00
|
|
|
)
|
2022-05-21 18:49:35 +00:00
|
|
|
if err != nil {
|
2022-05-21 18:57:32 +00:00
|
|
|
return nil, fmt.Errorf("could not parse base template: %w", err)
|
2022-05-21 18:49:35 +00:00
|
|
|
}
|
2022-05-21 17:18:17 +00:00
|
|
|
|
2022-05-21 18:49:35 +00:00
|
|
|
cache[name] = ts
|
|
|
|
}
|
2022-05-21 17:18:17 +00:00
|
|
|
|
2022-05-21 18:49:35 +00:00
|
|
|
return cache, nil
|
|
|
|
}
|
2022-05-21 17:18:17 +00:00
|
|
|
|
2022-05-26 13:27:25 +00:00
|
|
|
type topLevelNavItem string
|
|
|
|
type subLevelNavItem string
|
|
|
|
|
|
|
|
const (
|
|
|
|
topLevelNavMotions topLevelNavItem = "motions"
|
|
|
|
topLevelNavUsers topLevelNavItem = "users"
|
|
|
|
|
|
|
|
subLevelNavMotionsAll subLevelNavItem = "all-motions"
|
|
|
|
subLevelNavMotionsUnvoted subLevelNavItem = "unvoted-motions"
|
|
|
|
subLevelNavUsers subLevelNavItem = "users"
|
|
|
|
)
|
|
|
|
|
2022-05-22 10:19:25 +00:00
|
|
|
type templateData struct {
|
2022-05-26 13:27:25 +00:00
|
|
|
PrevPage string
|
|
|
|
NextPage string
|
|
|
|
Motion *models.MotionForDisplay
|
|
|
|
Motions []*models.MotionForDisplay
|
2022-05-26 14:47:57 +00:00
|
|
|
User *models.User
|
|
|
|
Users []*models.User
|
2022-05-26 13:27:25 +00:00
|
|
|
Request *http.Request
|
2022-05-26 14:27:44 +00:00
|
|
|
Flash string
|
2022-05-26 13:27:25 +00:00
|
|
|
Form any
|
|
|
|
ActiveNav topLevelNavItem
|
|
|
|
ActiveSubNav subLevelNavItem
|
2022-05-22 12:08:02 +00:00
|
|
|
}
|
|
|
|
|
2022-05-26 13:27:25 +00:00
|
|
|
func (app *application) newTemplateData(
|
|
|
|
r *http.Request,
|
|
|
|
nav topLevelNavItem,
|
|
|
|
subNav subLevelNavItem,
|
|
|
|
) *templateData {
|
|
|
|
user, err := app.GetUser(r)
|
|
|
|
if err != nil {
|
|
|
|
app.errorLog.Printf("error getting user for template data: %v", err)
|
|
|
|
}
|
|
|
|
|
2022-05-22 19:15:54 +00:00
|
|
|
return &templateData{
|
2022-05-26 13:27:25 +00:00
|
|
|
Request: r,
|
|
|
|
User: user,
|
|
|
|
ActiveNav: nav,
|
|
|
|
ActiveSubNav: subNav,
|
2022-05-26 14:27:44 +00:00
|
|
|
Flash: app.sessionManager.PopString(r.Context(), "flash"),
|
2022-05-22 19:15:54 +00:00
|
|
|
}
|
2022-05-22 10:19:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (app *application) render(w http.ResponseWriter, status int, page string, data *templateData) {
|
2022-05-21 18:49:35 +00:00
|
|
|
ts, ok := app.templateCache[page]
|
|
|
|
if !ok {
|
|
|
|
app.serverError(w, fmt.Errorf("the template %s does not exist", page))
|
2022-05-21 17:18:17 +00:00
|
|
|
|
2022-05-21 18:49:35 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-05-21 19:02:15 +00:00
|
|
|
buf := new(bytes.Buffer)
|
2022-05-21 18:49:35 +00:00
|
|
|
|
2022-05-21 19:02:15 +00:00
|
|
|
err := ts.ExecuteTemplate(buf, "base", data)
|
2022-05-21 18:49:35 +00:00
|
|
|
if err != nil {
|
|
|
|
app.serverError(w, err)
|
2022-05-21 19:02:15 +00:00
|
|
|
|
|
|
|
return
|
2022-05-21 18:49:35 +00:00
|
|
|
}
|
2022-05-21 19:02:15 +00:00
|
|
|
|
|
|
|
w.WriteHeader(status)
|
|
|
|
|
|
|
|
_, _ = buf.WriteTo(w)
|
2022-05-21 18:49:35 +00:00
|
|
|
}
|
|
|
|
|
2022-05-26 13:27:25 +00:00
|
|
|
func (m *templateData) motionPaginationOptions(limit int, first, last *time.Time) error {
|
2022-05-21 18:49:35 +00:00
|
|
|
motions := m.Motions
|
2022-05-21 17:18:17 +00:00
|
|
|
|
2022-05-26 13:27:25 +00:00
|
|
|
if len(motions) == limit && first.Before(motions[len(motions)-1].Proposed) {
|
2022-05-21 18:49:35 +00:00
|
|
|
marshalled, err := motions[len(motions)-1].Proposed.MarshalText()
|
2022-05-21 17:18:17 +00:00
|
|
|
if err != nil {
|
2022-05-21 18:49:35 +00:00
|
|
|
return fmt.Errorf("could not serialize timestamp: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
m.NextPage = string(marshalled)
|
|
|
|
}
|
2022-05-21 17:18:17 +00:00
|
|
|
|
2022-05-21 18:49:35 +00:00
|
|
|
if len(motions) > 0 && last.After(motions[0].Proposed) {
|
|
|
|
marshalled, err := motions[0].Proposed.MarshalText()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("could not serialize timestamp: %w", err)
|
2022-05-21 17:18:17 +00:00
|
|
|
}
|
|
|
|
|
2022-05-21 18:49:35 +00:00
|
|
|
m.PrevPage = string(marshalled)
|
2022-05-21 17:18:17 +00:00
|
|
|
}
|
|
|
|
|
2022-05-21 18:49:35 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (app *application) motionList(w http.ResponseWriter, r *http.Request) {
|
|
|
|
if r.URL.Path != "/motions/" {
|
|
|
|
app.notFound(w)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
listOptions *models.MotionListOptions
|
|
|
|
err error
|
|
|
|
)
|
|
|
|
|
2022-05-26 13:27:25 +00:00
|
|
|
listOptions, err = app.calculateMotionListOptions(r)
|
2022-05-21 18:49:35 +00:00
|
|
|
if err != nil {
|
|
|
|
app.clientError(w, http.StatusBadRequest)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx := r.Context()
|
|
|
|
|
2022-05-21 17:18:17 +00:00
|
|
|
motions, err := app.motions.GetMotions(ctx, listOptions)
|
|
|
|
if err != nil {
|
|
|
|
app.serverError(w, err)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-05-26 13:27:25 +00:00
|
|
|
first, last, err := app.motions.TimestampRange(ctx, listOptions)
|
2022-05-21 17:18:17 +00:00
|
|
|
if err != nil {
|
|
|
|
app.serverError(w, err)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-05-26 13:27:25 +00:00
|
|
|
templateData := app.newTemplateData(r, "motions", "all-motions")
|
|
|
|
|
|
|
|
if listOptions.UnvotedOnly {
|
|
|
|
templateData.ActiveSubNav = subLevelNavMotionsUnvoted
|
|
|
|
}
|
2022-05-22 12:08:02 +00:00
|
|
|
|
|
|
|
templateData.Motions = motions
|
2022-05-09 19:09:24 +00:00
|
|
|
|
2022-05-26 13:27:25 +00:00
|
|
|
err = templateData.motionPaginationOptions(listOptions.Limit, first, last)
|
2022-05-09 19:09:24 +00:00
|
|
|
if err != nil {
|
2022-05-15 18:10:49 +00:00
|
|
|
app.serverError(w, err)
|
2022-05-09 19:09:24 +00:00
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-05-22 10:19:25 +00:00
|
|
|
app.render(w, http.StatusOK, "motions.html", templateData)
|
2022-05-21 18:49:35 +00:00
|
|
|
}
|
2022-05-21 17:18:17 +00:00
|
|
|
|
2022-05-26 13:27:25 +00:00
|
|
|
func (app *application) calculateMotionListOptions(r *http.Request) (*models.MotionListOptions, error) {
|
2022-05-21 18:49:35 +00:00
|
|
|
const (
|
2022-05-26 13:27:25 +00:00
|
|
|
queryParamBefore = "before"
|
|
|
|
queryParamAfter = "after"
|
|
|
|
queryParamUnvoted = "unvoted"
|
|
|
|
motionsPerPage = 10
|
2022-05-21 18:49:35 +00:00
|
|
|
)
|
2022-05-21 17:18:17 +00:00
|
|
|
|
2022-05-21 18:49:35 +00:00
|
|
|
listOptions := &models.MotionListOptions{Limit: motionsPerPage}
|
2022-05-21 17:18:17 +00:00
|
|
|
|
2022-05-21 18:49:35 +00:00
|
|
|
if r.URL.Query().Has(queryParamAfter) {
|
|
|
|
var after time.Time
|
|
|
|
|
|
|
|
err := after.UnmarshalText([]byte(r.URL.Query().Get(queryParamAfter)))
|
2022-05-21 17:18:17 +00:00
|
|
|
if err != nil {
|
2022-05-21 18:49:35 +00:00
|
|
|
return nil, fmt.Errorf("could not unmarshal timestamp: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
listOptions.After = &after
|
|
|
|
} else if r.URL.Query().Has(queryParamBefore) {
|
|
|
|
var before time.Time
|
2022-05-21 17:18:17 +00:00
|
|
|
|
2022-05-21 18:49:35 +00:00
|
|
|
err := before.UnmarshalText([]byte(r.URL.Query().Get(queryParamBefore)))
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("could not unmarshal timestamp: %w", err)
|
2022-05-21 17:18:17 +00:00
|
|
|
}
|
|
|
|
|
2022-05-21 18:49:35 +00:00
|
|
|
listOptions.Before = &before
|
2022-05-21 17:18:17 +00:00
|
|
|
}
|
|
|
|
|
2022-05-26 13:27:25 +00:00
|
|
|
if r.URL.Query().Has(queryParamUnvoted) {
|
|
|
|
listOptions.UnvotedOnly = true
|
|
|
|
|
|
|
|
voter, err := app.GetUser(r)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("could not get voter: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if voter != nil {
|
2022-05-26 14:47:57 +00:00
|
|
|
listOptions.VoterID = voter.ID
|
2022-05-26 13:27:25 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-21 18:49:35 +00:00
|
|
|
return listOptions, nil
|
2022-05-09 19:09:24 +00:00
|
|
|
}
|
|
|
|
|
2022-05-22 12:08:02 +00:00
|
|
|
func (app *application) motionDetails(w http.ResponseWriter, r *http.Request) {
|
|
|
|
params := httprouter.ParamsFromContext(r.Context())
|
|
|
|
|
|
|
|
tag := params.ByName("tag")
|
|
|
|
|
|
|
|
showVotes := r.URL.Query().Has("showvotes")
|
|
|
|
|
|
|
|
motion, err := app.motions.GetMotionByTag(r.Context(), tag, showVotes)
|
|
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
|
|
app.serverError(w, err)
|
2022-05-09 19:09:24 +00:00
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-05-22 12:08:02 +00:00
|
|
|
if motion == nil {
|
|
|
|
app.notFound(w)
|
2022-05-09 19:09:24 +00:00
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-05-26 13:27:25 +00:00
|
|
|
data := app.newTemplateData(r, topLevelNavMotions, subLevelNavMotionsAll)
|
2022-05-22 12:08:02 +00:00
|
|
|
|
|
|
|
data.Motion = motion
|
|
|
|
|
|
|
|
app.render(w, http.StatusOK, "motion.html", data)
|
|
|
|
}
|
|
|
|
|
2022-05-22 19:15:54 +00:00
|
|
|
type NewMotionForm struct {
|
|
|
|
Title string `form:"title"`
|
|
|
|
Content string `form:"content"`
|
|
|
|
Type *models.VoteType `form:"type"`
|
2022-05-26 13:27:25 +00:00
|
|
|
Due int `form:"due"`
|
|
|
|
User *models.User `form:"-"`
|
2022-05-22 19:15:54 +00:00
|
|
|
validator.Validator `form:"-"`
|
2022-05-22 12:08:02 +00:00
|
|
|
}
|
|
|
|
|
2022-05-22 19:15:54 +00:00
|
|
|
func (app *application) newMotionForm(w http.ResponseWriter, r *http.Request) {
|
2022-05-26 13:27:25 +00:00
|
|
|
data := app.newTemplateData(r, "motions", "all-motions")
|
2022-05-22 19:15:54 +00:00
|
|
|
data.Form = &NewMotionForm{
|
2022-05-26 13:27:25 +00:00
|
|
|
User: data.User,
|
|
|
|
Type: models.VoteTypeMotion,
|
2022-05-22 19:15:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
app.render(w, http.StatusOK, "create_motion.html", data)
|
|
|
|
}
|
|
|
|
|
2022-05-26 14:06:31 +00:00
|
|
|
func (form *NewMotionForm) Validate() {
|
2022-05-22 19:15:54 +00:00
|
|
|
const (
|
|
|
|
minimumTitleLength = 3
|
|
|
|
maximumTitleLength = 200
|
|
|
|
minimumContentLength = 3
|
|
|
|
maximumContentLength = 8000
|
2022-05-26 13:27:25 +00:00
|
|
|
|
|
|
|
threeDays = 3
|
|
|
|
oneWeek = 7
|
|
|
|
twoWeeks = 14
|
|
|
|
threeWeeks = 28
|
2022-05-22 19:15:54 +00:00
|
|
|
)
|
|
|
|
|
2022-05-22 19:47:27 +00:00
|
|
|
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),
|
|
|
|
)
|
2022-05-26 13:27:25 +00:00
|
|
|
|
|
|
|
form.CheckField(validator.PermittedInt(
|
|
|
|
form.Due, threeDays, oneWeek, twoWeeks, threeWeeks), "due", "invalid duration choice",
|
|
|
|
)
|
2022-05-26 14:06:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (app *application) newMotionSubmit(w http.ResponseWriter, r *http.Request) {
|
|
|
|
const hoursInDay = 24
|
|
|
|
|
|
|
|
var form NewMotionForm
|
|
|
|
|
|
|
|
err := app.decodePostForm(r, &form)
|
|
|
|
if err != nil {
|
|
|
|
app.clientError(w, http.StatusBadRequest)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
form.Validate()
|
2022-05-22 19:15:54 +00:00
|
|
|
|
|
|
|
if !form.Valid() {
|
2022-05-26 13:27:25 +00:00
|
|
|
form.User = &models.User{}
|
2022-05-22 19:15:54 +00:00
|
|
|
|
2022-05-26 13:27:25 +00:00
|
|
|
data := app.newTemplateData(r, topLevelNavMotions, subLevelNavMotionsAll)
|
2022-05-22 19:15:54 +00:00
|
|
|
data.Form = form
|
|
|
|
|
|
|
|
app.render(w, http.StatusUnprocessableEntity, "create_motion.html", data)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-05-26 13:27:25 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
decisionID, err := app.motions.Create(
|
|
|
|
r.Context(),
|
|
|
|
user,
|
|
|
|
form.Type,
|
|
|
|
form.Title,
|
|
|
|
form.Content,
|
|
|
|
now,
|
|
|
|
now.Add(dueDuration),
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
app.serverError(w, err)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
decision, err := app.motions.GetByID(r.Context(), decisionID)
|
|
|
|
if err != nil {
|
|
|
|
app.serverError(w, err)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
app.mailNotifier.notifyChannel <- &NewDecisionNotification{
|
|
|
|
decision: &models.NewMotion{Decision: decision, Proposer: user},
|
|
|
|
}
|
|
|
|
|
2022-05-26 14:27:44 +00:00
|
|
|
app.sessionManager.Put(r.Context(), "flash", fmt.Sprintf("Started new motion %s: %s", decision.Tag, decision.Title))
|
2022-05-26 13:27:25 +00:00
|
|
|
|
2022-05-26 14:06:31 +00:00
|
|
|
http.Redirect(w, r, fmt.Sprintf("/motions/%s", decision.Tag), http.StatusSeeOther)
|
2022-05-26 13:27:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (app *application) editMotionForm(_ http.ResponseWriter, _ *http.Request) {
|
|
|
|
panic("not implemented")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (app *application) editMotionSubmit(_ http.ResponseWriter, _ *http.Request) {
|
|
|
|
panic("not implemented")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (app *application) withdrawMotionForm(_ http.ResponseWriter, _ *http.Request) {
|
|
|
|
panic("not implemented")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (app *application) withdrawMotionSubmit(_ http.ResponseWriter, _ *http.Request) {
|
|
|
|
panic("not implemented")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (app *application) voteForm(_ http.ResponseWriter, _ *http.Request) {
|
|
|
|
panic("not implemented")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (app *application) voteSubmit(_ http.ResponseWriter, _ *http.Request) {
|
|
|
|
panic("not implemented")
|
|
|
|
}
|
2022-05-22 19:15:54 +00:00
|
|
|
|
2022-05-26 13:27:25 +00:00
|
|
|
func (app *application) proxyVoteForm(_ http.ResponseWriter, _ *http.Request) {
|
|
|
|
panic("not implemented")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (app *application) proxyVoteSubmit(_ http.ResponseWriter, _ *http.Request) {
|
|
|
|
panic("not implemented")
|
|
|
|
}
|
|
|
|
|
2022-05-26 14:47:57 +00:00
|
|
|
func (app *application) userList(w http.ResponseWriter, r *http.Request) {
|
|
|
|
data := app.newTemplateData(r, topLevelNavUsers, subLevelNavUsers)
|
|
|
|
|
|
|
|
app.render(w, http.StatusOK, "users.html", data)
|
2022-05-26 13:27:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (app *application) submitUserRoles(_ http.ResponseWriter, _ *http.Request) {
|
|
|
|
panic("not implemented")
|
|
|
|
}
|
|
|
|
|
|
|
|
func (app *application) editUserForm(_ http.ResponseWriter, _ *http.Request) {
|
|
|
|
panic("not implemented")
|
|
|
|
}
|
|
|
|
func (app *application) editUserSubmit(_ http.ResponseWriter, _ *http.Request) {
|
|
|
|
panic("not implemented")
|
2022-05-22 12:08:02 +00:00
|
|
|
}
|
|
|
|
|
2022-05-26 13:27:25 +00:00
|
|
|
func (app *application) userAddEmailForm(_ http.ResponseWriter, _ *http.Request) {
|
2022-05-22 12:08:02 +00:00
|
|
|
panic("not implemented")
|
|
|
|
}
|
|
|
|
|
2022-05-26 13:27:25 +00:00
|
|
|
func (app *application) userAddEmailSubmit(_ http.ResponseWriter, _ *http.Request) {
|
2022-05-22 12:08:02 +00:00
|
|
|
panic("not implemented")
|
|
|
|
}
|
|
|
|
|
2022-05-26 13:27:25 +00:00
|
|
|
func (app *application) deleteUserForm(_ http.ResponseWriter, _ *http.Request) {
|
2022-05-22 12:08:02 +00:00
|
|
|
panic("not implemented")
|
|
|
|
}
|
|
|
|
|
2022-05-26 14:47:57 +00:00
|
|
|
func (app *application) deleteUserSubmit(_ http.ResponseWriter, _ *http.Request) {
|
2022-05-22 12:08:02 +00:00
|
|
|
panic("not implemented")
|
2022-05-09 19:09:24 +00:00
|
|
|
}
|