basic user manager search

reduce template sizes
add PaginatorMod struct

UserStore:
add SearchOffset method
add CountSearch method

phrases:
add panel_users_search_head
add panel_users_search_name
add panel_users_search_name_placeholder
add panel_users_search_email
add panel_users_search_email_placeholder
add panel_users_search_button
This commit is contained in:
Azareal 2021-01-19 08:11:30 +10:00
parent e792dbc2c7
commit c9e99c075d
7 changed files with 144 additions and 38 deletions

View File

@ -154,6 +154,13 @@ type Paginator struct {
LastPage int LastPage int
} }
type PaginatorMod struct {
Params template.URL
PageList []int
Page int
LastPage int
}
type CustomPagePage struct { type CustomPagePage struct {
*Header *Header
Page *CustomPage Page *CustomPage
@ -604,10 +611,15 @@ type PanelMenuItemPage struct {
Item MenuItem Item MenuItem
} }
type PanelUserPageSearch struct {
Name string
Email string
}
type PanelUserPage struct { type PanelUserPage struct {
*BasePanelPage *BasePanelPage
ItemList []*User ItemList []*User
Paginator Search PanelUserPageSearch
PaginatorMod
} }
type PanelGroupPage struct { type PanelGroupPage struct {

View File

@ -21,6 +21,7 @@ type UserStore interface {
Getn(id int) *User Getn(id int) *User
GetByName(name string) (*User, error) GetByName(name string) (*User, error)
Exists(id int) bool Exists(id int) bool
SearchOffset(name, email string, offset, perPage int) (users []*User, err error)
GetOffset(offset, perPage int) ([]*User, error) GetOffset(offset, perPage int) ([]*User, error)
Each(f func(*User) error) error Each(f func(*User) error) error
//BulkGet(ids []int) ([]*User, error) //BulkGet(ids []int) ([]*User, error)
@ -29,6 +30,7 @@ type UserStore interface {
Create(name, password, email string, group int, active bool) (int, error) Create(name, password, email string, group int, active bool) (int, error)
Reload(id int) error Reload(id int) error
Count() int Count() int
CountSearch(name, email string) int
SetCache(cache UserCache) SetCache(cache UserCache)
GetCache() UserCache GetCache() UserCache
@ -39,12 +41,15 @@ type DefaultUserStore struct {
get *sql.Stmt get *sql.Stmt
getByName *sql.Stmt getByName *sql.Stmt
searchOffset *sql.Stmt
getOffset *sql.Stmt getOffset *sql.Stmt
getAll *sql.Stmt getAll *sql.Stmt
exists *sql.Stmt exists *sql.Stmt
register *sql.Stmt register *sql.Stmt
nameExists *sql.Stmt nameExists *sql.Stmt
count *sql.Stmt count *sql.Stmt
countSearch *sql.Stmt
} }
// NewDefaultUserStore gives you a new instance of DefaultUserStore // NewDefaultUserStore gives you a new instance of DefaultUserStore
@ -58,14 +63,19 @@ func NewDefaultUserStore(cache UserCache) (*DefaultUserStore, error) {
// TODO: Add an admin version of registerStmt with more flexibility? // TODO: Add an admin version of registerStmt with more flexibility?
return &DefaultUserStore{ return &DefaultUserStore{
cache: cache, cache: cache,
get: acc.Select(u).Columns("name,group,active,is_super_admin,session,email,avatar,message,level,score,posts,liked,last_ip,temp_group,createdAt,enable_embeds,profile_comments,who_can_convo").Where("uid=?").Prepare(), get: acc.Select(u).Columns("name,group,active,is_super_admin,session,email,avatar,message,level,score,posts,liked,last_ip,temp_group,createdAt,enable_embeds,profile_comments,who_can_convo").Where("uid=?").Prepare(),
getByName: acc.Select(u).Columns(allCols).Where("name=?").Prepare(), getByName: acc.Select(u).Columns(allCols).Where("name=?").Prepare(),
searchOffset: acc.Select(u).Columns(allCols).Where("(name=? OR ?='') AND (email=? OR ?='')").Orderby("uid ASC").Limit("?,?").Prepare(),
getOffset: acc.Select(u).Columns(allCols).Orderby("uid ASC").Limit("?,?").Prepare(), getOffset: acc.Select(u).Columns(allCols).Orderby("uid ASC").Limit("?,?").Prepare(),
getAll: acc.Select(u).Columns(allCols).Prepare(), getAll: acc.Select(u).Columns(allCols).Prepare(),
exists: acc.Exists(u, "uid").Prepare(), exists: acc.Exists(u, "uid").Prepare(),
register: acc.Insert(u).Columns("name,email,password,salt,group,is_super_admin,session,active,message,createdAt,lastActiveAt,lastLiked,oldestItemLikedCreatedAt").Fields("?,?,?,?,?,0,'',?,'',UTC_TIMESTAMP(),UTC_TIMESTAMP(),UTC_TIMESTAMP(),UTC_TIMESTAMP()").Prepare(), // TODO: Implement user_count on users_groups here register: acc.Insert(u).Columns("name,email,password,salt,group,is_super_admin,session,active,message,createdAt,lastActiveAt,lastLiked,oldestItemLikedCreatedAt").Fields("?,?,?,?,?,0,'',?,'',UTC_TIMESTAMP(),UTC_TIMESTAMP(),UTC_TIMESTAMP(),UTC_TIMESTAMP()").Prepare(), // TODO: Implement user_count on users_groups here
nameExists: acc.Exists(u, "name").Prepare(), nameExists: acc.Exists(u, "name").Prepare(),
count: acc.Count(u).Prepare(), count: acc.Count(u).Prepare(),
countSearch: acc.Count(u).Where("(name=? OR ?='') AND (email=? OR ?='')").Prepare(),
}, acc.FirstError() }, acc.FirstError()
} }
@ -170,6 +180,30 @@ func (s *DefaultUserStore) GetOffset(offset, perPage int) (users []*User, err er
} }
return users, rows.Err() return users, rows.Err()
} }
func (s *DefaultUserStore) SearchOffset(name, email string, offset, perPage int) (users []*User, err error) {
rows, err := s.searchOffset.Query(name, name, email, email, offset, perPage)
if err != nil {
return users, err
}
defer rows.Close()
var embeds int
for rows.Next() {
u := &User{Loggedin: true}
err := rows.Scan(&u.ID, &u.Name, &u.Group, &u.Active, &u.IsSuperAdmin, &u.Session, &u.Email, &u.RawAvatar, &u.Message, &u.Level, &u.Score, &u.Posts, &u.Liked, &u.LastIP, &u.TempGroup, &u.CreatedAt, &embeds, &u.Privacy.ShowComments, &u.Privacy.AllowMessage)
if err != nil {
return nil, err
}
if embeds != -1 {
u.ParseSettings = DefaultParseSettings.CopyPtr()
u.ParseSettings.NoEmbed = embeds == 0
}
u.Init()
s.cache.Set(u)
users = append(users, u)
}
return users, rows.Err()
}
func (s *DefaultUserStore) Each(f func(*User) error) error { func (s *DefaultUserStore) Each(f func(*User) error) error {
rows, e := s.getAll.Query() rows, e := s.getAll.Query()
if e != nil { if e != nil {
@ -350,6 +384,10 @@ func (s *DefaultUserStore) Count() (count int) {
return Countf(s.count) return Countf(s.count)
} }
func (s *DefaultUserStore) CountSearch(name, email string) (count int) {
return Countf(s.countSearch, name, name, email, email)
}
func (s *DefaultUserStore) SetCache(cache UserCache) { func (s *DefaultUserStore) SetCache(cache UserCache) {
s.cache = cache s.cache = cache
} }

View File

@ -895,6 +895,13 @@
"panel_users_ban":"Ban", "panel_users_ban":"Ban",
"panel_users_activate":"Activate", "panel_users_activate":"Activate",
"panel_users_search_head":"Search",
"panel_users_search_name":"Name",
"panel_users_search_name_placeholder":"John Doe",
"panel_users_search_email":"Email",
"panel_users_search_email_placeholder":"john.doe@example.com",
"panel_users_search_button":"Search",
"panel_user_head":"User Editor", "panel_user_head":"User Editor",
"panel_user_avatar":"Avatar", "panel_user_avatar":"Avatar",
"panel_user_avatar_select":"Select", "panel_user_avatar_select":"Select",

View File

@ -2,7 +2,9 @@ package panel
import ( import (
"database/sql" "database/sql"
"html/template"
"net/http" "net/http"
"net/url"
"strconv" "strconv"
c "github.com/Azareal/Gosora/common" c "github.com/Azareal/Gosora/common"
@ -13,17 +15,45 @@ func Users(w http.ResponseWriter, r *http.Request, u *c.User) c.RouteError {
if ferr != nil { if ferr != nil {
return ferr return ferr
} }
name := r.FormValue("s-name")
email := r.FormValue("s-email")
hasParam := name != "" || email != ""
page, _ := strconv.Atoi(r.FormValue("page")) page, _ := strconv.Atoi(r.FormValue("page"))
perPage := 15 perPage := 15
offset, page, lastPage := c.PageOffset(basePage.Stats.Users, page, perPage) userCount := basePage.Stats.Users
if hasParam {
userCount = c.Users.CountSearch(name, email)
}
offset, page, lastPage := c.PageOffset(userCount, page, perPage)
users, err := c.Users.GetOffset(offset, perPage) var users []*c.User
if err != nil { var e error
return c.InternalError(err, w, r) if hasParam {
users, e = c.Users.SearchOffset(name, email, offset, perPage)
} else {
users, e = c.Users.GetOffset(offset, perPage)
}
if e != nil {
return c.InternalError(e, w, r)
} }
name = url.QueryEscape(name)
email = url.QueryEscape(email)
search := c.PanelUserPageSearch{name, email}
var params string
if hasParam {
if name != "" {
params += "s-name=" + name + "&"
}
if email != "" {
params += "s-email=" + email + "&"
}
}
pageList := c.Paginate(page, lastPage, 5) pageList := c.Paginate(page, lastPage, 5)
pi := c.PanelUserPage{basePage, users, c.Paginator{pageList, page, lastPage}} pi := c.PanelUserPage{basePage, users, search, c.PaginatorMod{template.URL(params), pageList, page, lastPage}}
return renderTemplate("panel", w, r, basePage.Header, c.Panel{basePage, "", "", "panel_users", &pi}) return renderTemplate("panel", w, r, basePage.Header, c.Panel{basePage, "", "", "panel_users", &pi})
} }

View File

@ -1,17 +1,17 @@
<div class="colstack_item colstack_head"> <div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_themes_primary_themes"}}</h1></div> <div class="rowitem"><h1>{{lang "panel_themes_primary_themes"}}</h1></div>
</div> </div>
<div id="panel_primary_themes" class="colstack_item panel_themes complex_rowlist"> <div id="panel_primary_themes"class="colstack_item panel_themes complex_rowlist">
{{range .PrimaryThemes}} {{range .PrimaryThemes}}
<div class="theme_row rowitem editable_parent"{{if .FullImage}} style="background-image:url('/s/{{.FullImage}}');background-position:center;background-size:50%;background-repeat:no-repeat;"{{end}}> <div class="theme_row rowitem editable_parent"{{if .FullImage}}style="background-image:url('/s/{{.FullImage}}');background-position:center;background-size:50%;background-repeat:no-repeat;"{{end}}>
<span style="float:left;"> <span style="float:left;">
<a href="/panel/themes/{{.Name}}" class="editable_block" style="font-size:17px;">{{.FriendlyName}}</a><br> <a href="/panel/themes/{{.Name}}"class="editable_block"style="font-size:17px;">{{.FriendlyName}}</a><br>
<small class="panel_theme_author" style="margin-left:2px;">{{lang "panel_themes_author_prefix"}}<a href="//{{.URL}}">{{.Creator}}</a></small> <small class="panel_theme_author" style="margin-left:2px;">{{lang "panel_themes_author_prefix"}}<a href="//{{.URL}}">{{.Creator}}</a></small>
</span> </span>
<span class="panel_floater"> <span class="panel_floater">
{{if .MobileFriendly}}<span class="panel_tag panel_theme_mobile" title="{{lang "panel_themes_mobile_friendly_tooltip"}}" aria-label="{{lang "panel_themes_mobile_friendly_aria"}}">📱</span>{{end}} {{if .MobileFriendly}}<span class="panel_tag panel_theme_mobile"title="{{lang "panel_themes_mobile_friendly_tooltip"}}" aria-label="{{lang "panel_themes_mobile_friendly_aria"}}">📱</span>{{end}}
{{if .Tag}}<span class="panel_tag panel_theme_tag">{{.Tag}}</span>{{end}} {{if .Tag}}<span class="panel_tag panel_theme_tag">{{.Tag}}</span>{{end}}
{{if .Active}}<span class="panel_tag panel_right_button">{{lang "panel_themes_default"}}</span>{{else}}<a href="/panel/themes/default/{{.Name}}?s={{$.CurrentUser.Session}}" class="panel_tag panel_right_button">{{lang "panel_themes_make_default"}}</a>{{end}} {{if .Active}}<span class="panel_tag panel_right_button">{{lang "panel_themes_default"}}</span>{{else}}<a href="/panel/themes/default/{{.Name}}?s={{$.CurrentUser.Session}}"class="panel_tag panel_right_button">{{lang "panel_themes_make_default"}}</a>{{end}}
</span> </span>
</div> </div>
{{end}} {{end}}
@ -20,17 +20,17 @@
<div class="colstack_item colstack_head"> <div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_themes_variant_themes"}}</h1></div> <div class="rowitem"><h1>{{lang "panel_themes_variant_themes"}}</h1></div>
</div> </div>
<div id="panel_variant_themes" class="colstack_item panel_themes"> <div id="panel_variant_themes"class="colstack_item panel_themes">
{{range .VariantThemes}} {{range .VariantThemes}}
<div class="theme_row rowitem editable_parent"{{if .FullImage}} style="background-image:url('/s/{{.FullImage}}');background-position:center;background-size:50%;background-repeat:no-repeat;"{{end}}> <div class="theme_row rowitem editable_parent"{{if .FullImage}}style="background-image:url('/s/{{.FullImage}}');background-position:center;background-size:50%;background-repeat:no-repeat;"{{end}}>
<span style="float:left;"> <span style="float:left;">
<a href="/panel/themes/{{.Name}}" class="editable_block" style="font-size:17px;">{{.FriendlyName}}</a><br> <a href="/panel/themes/{{.Name}}"class="editable_block"style="font-size:17px;">{{.FriendlyName}}</a><br>
<small class="panel_theme_author" style="margin-left:2px;">{{lang "panel_themes_author_prefix"}}<a href="//{{.URL}}">{{.Creator}}</a></small> <small class="panel_theme_author"style="margin-left:2px;">{{lang "panel_themes_author_prefix"}}<a href="//{{.URL}}">{{.Creator}}</a></small>
</span> </span>
<span class="panel_floater"> <span class="panel_floater">
{{if .MobileFriendly}}<span class="panel_tag panel_theme_mobile" title="{{lang "panel_themes_mobile_friendly_tooltip"}}" aria-label="{{lang "panel_themes_mobile_friendly_aria"}}">📱</span>{{end}} {{if .MobileFriendly}}<span class="panel_tag panel_theme_mobile"title="{{lang "panel_themes_mobile_friendly_tooltip"}}" aria-label="{{lang "panel_themes_mobile_friendly_aria"}}">📱</span>{{end}}
{{if .Tag}}<span class="panel_tag panel_theme_tag">{{.Tag}}</span>{{end}} {{if .Tag}}<span class="panel_tag panel_theme_tag">{{.Tag}}</span>{{end}}
{{if .Active}}<span class="panel_tag panel_right_button">{{lang "panel_themes_default"}}</span>{{else}}<a href="/panel/themes/default/{{.Name}}?s={{$.CurrentUser.Session}}" class="panel_tag panel_right_button">{{lang "panel_themes_make_default"}}</a>{{end}} {{if .Active}}<span class="panel_tag panel_right_button">{{lang "panel_themes_default"}}</span>{{else}}<a href="/panel/themes/default/{{.Name}}?s={{$.CurrentUser.Session}}"class="panel_tag panel_right_button">{{lang "panel_themes_make_default"}}</a>{{end}}
</span> </span>
</div> </div>
{{end}} {{end}}

View File

@ -19,7 +19,7 @@
</div> </div>
<div class="formrow"> <div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel_user_name"}}</a></div> <div class="formitem formlabel"><a>{{lang "panel_user_name"}}</a></div>
<div class="formitem"><input form="user_form"name="name"type="text"value="{{.User.Name}}"placeholder="{{lang "panel_user_name_placeholder"}}" autocomplete="off"></div> <div class="formitem"><input form="user_form"name="name"type="text"value="{{.User.Name}}"placeholder="{{lang "panel_user_name_placeholder"}}"autocomplete="off"></div>
</div> </div>
{{if .CurrentUser.Perms.EditUserPassword}}<div class="formrow"> {{if .CurrentUser.Perms.EditUserPassword}}<div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel_user_password"}}</a></div> <div class="formitem formlabel"><a>{{lang "panel_user_password"}}</a></div>

View File

@ -1,20 +1,39 @@
<div class="colstack_item colstack_head"> <div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_users_head"}}</h1></div> <div class="rowitem"><h1>{{lang "panel_users_head"}}</h1></div>
</div> </div>
<div id="panel_users" class="colstack_item rowlist bgavatars"> <div id="panel_users"class="colstack_item rowlist bgavatars">
{{range .ItemList}} {{range .ItemList}}
<div class="rowitem" style="background-image:url('{{.Avatar}}');"> <div class="rowitem"style="background-image:url('{{.Avatar}}');">
<a class="rowAvatar"{{if $.CurrentUser.Perms.EditUser}}href="/panel/users/edit/{{.ID}}"{{end}}> <a class="rowAvatar"{{if $.CurrentUser.Perms.EditUser}}href="/panel/users/edit/{{.ID}}"{{end}}>
<img class="bgsub"src="{{.Avatar}}"alt="Avatar"aria-hidden="true"> <img class="bgsub"src="{{.Avatar}}"alt="Avatar"aria-hidden="true">
</a> </a>
<a class="rowTitle"{{if $.CurrentUser.Perms.EditUser}}href="/panel/users/edit/{{.ID}}"{{end}}>{{.Name}}</a> <a class="rowTitle"{{if $.CurrentUser.Perms.EditUser}}href="/panel/users/edit/{{.ID}}"{{end}}>{{.Name}}</a>
<span class="panel_floater"> <span class="panel_floater">
<a href="{{.Link}}" class="tag-mini profile_url">{{lang "panel_users_profile"}}</a> <a href="{{.Link}}"class="tag-mini profile_url">{{lang "panel_users_profile"}}</a>
{{if (.Tag) and (.IsSuperMod)}}<span class="panel_tag">{{.Tag}}</span></span>{{end}} {{if (.Tag) and (.IsSuperMod)}}<span class="panel_tag">{{.Tag}}</span></span>{{end}}
{{if .IsBanned}}<a href="/users/unban/{{.ID}}?s={{$.CurrentUser.Session}}" class="panel_tag panel_right_button ban_button">{{lang "panel_users_unban"}}</a>{{else if not .IsSuperMod}}<a href="/user/{{.ID}}#ban_user" class="panel_tag panel_right_button ban_button">{{lang "panel_users_ban"}}</a>{{end}} {{if .IsBanned}}<a href="/users/unban/{{.ID}}?s={{$.CurrentUser.Session}}"class="panel_tag panel_right_button ban_button">{{lang "panel_users_unban"}}</a>{{else if not .IsSuperMod}}<a href="/user/{{.ID}}#ban_user"class="panel_tag panel_right_button ban_button">{{lang "panel_users_ban"}}</a>{{end}}
{{if not .Active}}<a href="/users/activate/{{.ID}}?s={{$.CurrentUser.Session}}" class="panel_tag panel_right_button">{{lang "panel_users_activate"}}</a>{{end}} {{if not .Active}}<a href="/users/activate/{{.ID}}?s={{$.CurrentUser.Session}}"class="panel_tag panel_right_button">{{lang "panel_users_activate"}}</a>{{end}}
</span> </span>
</div> </div>
{{end}} {{end}}
</div> </div>
{{template "paginator.html" . }} {{template "paginator_mod.html" . }}
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_users_search_head"}}</h1></div>
</div>
<div class="colstack_item the_form">
<form action="/panel/users/"method="get">
<div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel_users_search_name"}}</a></div>
<div class="formitem"><input name="s-name"type="text"{{if .Search.Name}}value="{{.Search.Name}}"{{end}}placeholder="{{lang "panel_users_search_name_placeholder"}}"></div>
</div>
{{if .CurrentUser.Perms.EditUserEmail}}<div class="formrow">
<div class="formitem formlabel"><a>{{lang "panel_users_search_email"}}</a></div>
<div class="formitem"><input name="s-email"type="email"{{if .Search.Email}}value="{{.Search.Email}}"{{end}}placeholder="{{lang "panel_users_search_email_placeholder"}}"></div>
</div>{{end}}
<div class="formrow form_button_row">
<div class="formitem"><button class="formbutton">{{lang "panel_users_search_button"}}</button></div>
</div>
</form>
</div>