Implement user list

This commit is contained in:
Jan Dittberner 2022-05-29 20:46:52 +02:00
parent 71fc599a10
commit db52f88e25
7 changed files with 225 additions and 6 deletions

View file

@ -612,7 +612,18 @@ func (app *application) proxyVoteSubmit(w http.ResponseWriter, r *http.Request)
func (app *application) userList(w http.ResponseWriter, r *http.Request) {
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)
}

1
go.mod
View file

@ -30,6 +30,7 @@ require (
github.com/justinas/nosurf v1.1.1
github.com/lestrrat-go/tcputil v0.0.0-20180223003554-d3c7f98154fb
github.com/stretchr/testify v1.7.0
golang.org/x/text v0.3.7
)
require (

1
go.sum
View file

@ -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.5/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/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=

View file

@ -55,4 +55,5 @@ const (
errCouldNotScanResult = "could not scan result: %w"
errCouldNotStartTransaction = "could not start transaction: %w"
errCouldNotCommitTransaction = "could not commit transaction: %w"
errCouldNotCreateInQuery = "could not create query with IN clause: %w"
)

View file

@ -24,12 +24,29 @@ import (
"strings"
"github.com/jmoiron/sqlx"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
type Role struct {
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
const (
@ -47,10 +64,11 @@ const (
// The columns cannot be dropped in SQLite without dropping and recreating
// the voters table and all foreign key indices pointing to that table.
type User struct {
ID int64 `db:"id"`
Name string `db:"name"`
Reminder string `db:"reminder"` // reminder email address
roles []*Role `db:"-"`
ID int64 `db:"id"`
Name string `db:"name"`
Reminder string `db:"reminder"` // reminder email address
roles []*Role `db:"-"`
canDelete bool
}
func (u *User) Roles() ([]*Role, error) {
@ -83,6 +101,10 @@ outer:
return roleMatched, nil
}
func (u *User) CanDelete() bool {
return u.canDelete
}
type UserModel struct {
DB *sqlx.DB
}
@ -322,3 +344,157 @@ WHERE v.id = ?
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
}
}

View file

@ -8,7 +8,7 @@
{{ range .Motions }}
<div class="ui raised segment">
{{ template "motion_display" . }}
{{ if $user }}{{ template "motion_actions" . }}{{ end }}
{{ if canVote $user }}{{ template "motion_actions" . }}{{ end }}
</div>
{{ end }}
{{ template "pagination" $page }}

View file

@ -2,6 +2,35 @@
{{ define "main"}}
{{ 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 }}
<div class="ui basic segment">
<div class="ui icon message">