Implement user list
This commit is contained in:
parent
71fc599a10
commit
db52f88e25
7 changed files with 225 additions and 6 deletions
|
@ -612,7 +612,18 @@ func (app *application) proxyVoteSubmit(w http.ResponseWriter, r *http.Request)
|
||||||
func (app *application) userList(w http.ResponseWriter, r *http.Request) {
|
func (app *application) userList(w http.ResponseWriter, r *http.Request) {
|
||||||
data := app.newTemplateData(r, topLevelNavUsers, subLevelNavUsers)
|
data := app.newTemplateData(r, topLevelNavUsers, subLevelNavUsers)
|
||||||
|
|
||||||
// TODO: implement user listing
|
users, err := app.users.List(
|
||||||
|
r.Context(),
|
||||||
|
app.users.WithRolesOption(),
|
||||||
|
app.users.CanDeleteOption(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
app.serverError(w, err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Users = users
|
||||||
|
|
||||||
app.render(w, http.StatusOK, "users.html", data)
|
app.render(w, http.StatusOK, "users.html", data)
|
||||||
}
|
}
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -30,6 +30,7 @@ require (
|
||||||
github.com/justinas/nosurf v1.1.1
|
github.com/justinas/nosurf v1.1.1
|
||||||
github.com/lestrrat-go/tcputil v0.0.0-20180223003554-d3c7f98154fb
|
github.com/lestrrat-go/tcputil v0.0.0-20180223003554-d3c7f98154fb
|
||||||
github.com/stretchr/testify v1.7.0
|
github.com/stretchr/testify v1.7.0
|
||||||
|
golang.org/x/text v0.3.7
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|
1
go.sum
1
go.sum
|
@ -1492,6 +1492,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
|
|
@ -55,4 +55,5 @@ const (
|
||||||
errCouldNotScanResult = "could not scan result: %w"
|
errCouldNotScanResult = "could not scan result: %w"
|
||||||
errCouldNotStartTransaction = "could not start transaction: %w"
|
errCouldNotStartTransaction = "could not start transaction: %w"
|
||||||
errCouldNotCommitTransaction = "could not commit transaction: %w"
|
errCouldNotCommitTransaction = "could not commit transaction: %w"
|
||||||
|
errCouldNotCreateInQuery = "could not create query with IN clause: %w"
|
||||||
)
|
)
|
||||||
|
|
|
@ -24,12 +24,29 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
|
"golang.org/x/text/cases"
|
||||||
|
"golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Role struct {
|
type Role struct {
|
||||||
Name string `db:"role"`
|
Name string `db:"role"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Role) String() string {
|
||||||
|
return cases.Title(language.BritishEnglish).String(strings.ToLower(r.Name))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Role) Scan(src any) error {
|
||||||
|
value, ok := src.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("could not cast %v of %T to string", src, src)
|
||||||
|
}
|
||||||
|
|
||||||
|
*r = Role{Name: value}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type RoleName string
|
type RoleName string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -51,6 +68,7 @@ type User struct {
|
||||||
Name string `db:"name"`
|
Name string `db:"name"`
|
||||||
Reminder string `db:"reminder"` // reminder email address
|
Reminder string `db:"reminder"` // reminder email address
|
||||||
roles []*Role `db:"-"`
|
roles []*Role `db:"-"`
|
||||||
|
canDelete bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *User) Roles() ([]*Role, error) {
|
func (u *User) Roles() ([]*Role, error) {
|
||||||
|
@ -83,6 +101,10 @@ outer:
|
||||||
return roleMatched, nil
|
return roleMatched, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *User) CanDelete() bool {
|
||||||
|
return u.canDelete
|
||||||
|
}
|
||||||
|
|
||||||
type UserModel struct {
|
type UserModel struct {
|
||||||
DB *sqlx.DB
|
DB *sqlx.DB
|
||||||
}
|
}
|
||||||
|
@ -322,3 +344,157 @@ WHERE v.id = ?
|
||||||
|
|
||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UserListOption func(context.Context, []*User) error
|
||||||
|
|
||||||
|
func (m *UserModel) List(ctx context.Context, options ...UserListOption) ([]*User, error) {
|
||||||
|
rows, err := m.DB.QueryxContext(ctx, `SELECT id, name FROM voters ORDER BY name`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf(errCouldNotExecuteQuery, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func(rows *sqlx.Rows) {
|
||||||
|
_ = rows.Close()
|
||||||
|
}(rows)
|
||||||
|
|
||||||
|
users := make([]*User, 0)
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
if err = rows.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf(errCouldNotFetchRow, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var user User
|
||||||
|
if err = rows.StructScan(&user); err != nil {
|
||||||
|
return nil, fmt.Errorf(errCouldNotScanResult, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
users = append(users, &user)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, option := range options {
|
||||||
|
if err = option(ctx, users); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return users, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *UserModel) WithRolesOption() UserListOption {
|
||||||
|
return func(ctx context.Context, users []*User) error {
|
||||||
|
if len(users) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
userIDs := make([]int64, len(users))
|
||||||
|
for idx := range users {
|
||||||
|
userIDs[idx] = users[idx].ID
|
||||||
|
}
|
||||||
|
|
||||||
|
query, args, err := sqlx.In(
|
||||||
|
`SELECT voter_id, role FROM user_roles WHERE voter_id IN (?)`,
|
||||||
|
userIDs,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(errCouldNotCreateInQuery, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := m.DB.QueryxContext(ctx, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(errCouldNotExecuteQuery, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func(rows *sqlx.Rows) {
|
||||||
|
_ = rows.Close()
|
||||||
|
}(rows)
|
||||||
|
|
||||||
|
roleMap := make(map[int64][]*Role, len(users))
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return fmt.Errorf(errCouldNotFetchRow, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var userID int64
|
||||||
|
var role Role
|
||||||
|
|
||||||
|
if err := rows.Scan(&userID, &role); err != nil {
|
||||||
|
return fmt.Errorf(errCouldNotScanResult, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := roleMap[userID]; !ok {
|
||||||
|
roleMap[userID] = []*Role{&role}
|
||||||
|
} else {
|
||||||
|
roleMap[userID] = append(roleMap[userID], &role)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx := range users {
|
||||||
|
if roles, ok := roleMap[users[idx].ID]; ok {
|
||||||
|
users[idx].roles = roles
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
users[idx].roles = make([]*Role, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *UserModel) CanDeleteOption() UserListOption {
|
||||||
|
return func(ctx context.Context, users []*User) error {
|
||||||
|
if len(users) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
userIDs := make([]int64, len(users))
|
||||||
|
for idx := range users {
|
||||||
|
userIDs[idx] = users[idx].ID
|
||||||
|
}
|
||||||
|
|
||||||
|
query, args, err := sqlx.In(
|
||||||
|
`SELECT id
|
||||||
|
FROM voters
|
||||||
|
WHERE id IN (?)
|
||||||
|
AND NOT EXISTS(SELECT * FROM decisions WHERE proponent = voters.id)
|
||||||
|
AND NOT EXISTS(SELECT * FROM votes WHERE voter = voters.id)
|
||||||
|
AND NOT EXISTS(SELECT * FROM user_roles WHERE voter_id = voters.id)`,
|
||||||
|
userIDs,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(errCouldNotCreateInQuery, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := m.DB.QueryxContext(ctx, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(errCouldNotExecuteQuery, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func(rows *sqlx.Rows) {
|
||||||
|
_ = rows.Close()
|
||||||
|
}(rows)
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return fmt.Errorf(errCouldNotFetchRow, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var userID int64
|
||||||
|
|
||||||
|
if err := rows.Scan(&userID); err != nil {
|
||||||
|
return fmt.Errorf(errCouldNotScanResult, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx := range users {
|
||||||
|
if userID == users[idx].ID {
|
||||||
|
users[idx].canDelete = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
{{ range .Motions }}
|
{{ range .Motions }}
|
||||||
<div class="ui raised segment">
|
<div class="ui raised segment">
|
||||||
{{ template "motion_display" . }}
|
{{ template "motion_display" . }}
|
||||||
{{ if $user }}{{ template "motion_actions" . }}{{ end }}
|
{{ if canVote $user }}{{ template "motion_actions" . }}{{ end }}
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ template "pagination" $page }}
|
{{ template "pagination" $page }}
|
||||||
|
|
|
@ -2,6 +2,35 @@
|
||||||
|
|
||||||
{{ define "main"}}
|
{{ define "main"}}
|
||||||
{{ if .Users }}
|
{{ if .Users }}
|
||||||
|
<table class="ui selectable basic table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="six wide">Name</th>
|
||||||
|
<th class="four wide">Roles</th>
|
||||||
|
<th class="six wide">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{ $user := .User }}
|
||||||
|
{{ range .Users }}
|
||||||
|
<tr {{ if eq $user.ID .ID }}class="disabled"{{ end }}>
|
||||||
|
<td>{{ .Name }}</td>
|
||||||
|
<td>{{ .Roles | join ", " }}</td>
|
||||||
|
<td>
|
||||||
|
{{ if not (eq $user.ID .ID) }}
|
||||||
|
<a href="/users/{{ .ID }}/" class="ui labeled primary icon button"><i class="edit icon"></i> Edit</a>
|
||||||
|
{{ if .CanDelete }}
|
||||||
|
<a href="/users/{{ .ID }}/delete" class="ui labeled negative icon button" title="{{ .Name }} never participated in a motion and may be deleted">
|
||||||
|
<i class="delete icon"></i> Delete</a>
|
||||||
|
{{ end }}
|
||||||
|
{{ else }}
|
||||||
|
Cannot modify your own user. Ask another administrator or secretary.
|
||||||
|
{{ end }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<div class="ui basic segment">
|
<div class="ui basic segment">
|
||||||
<div class="ui icon message">
|
<div class="ui icon message">
|
||||||
|
|
Loading…
Reference in a new issue