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) {
|
||||
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
1
go.mod
|
@ -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
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.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=
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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">
|
||||
|
|
Loading…
Reference in a new issue