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

View File

@ -21,6 +21,7 @@ type UserStore interface {
Getn(id int) *User
GetByName(name string) (*User, error)
Exists(id int) bool
SearchOffset(name, email string, offset, perPage int) (users []*User, err error)
GetOffset(offset, perPage int) ([]*User, error)
Each(f func(*User) error) error
//BulkGet(ids []int) ([]*User, error)
@ -29,6 +30,7 @@ type UserStore interface {
Create(name, password, email string, group int, active bool) (int, error)
Reload(id int) error
Count() int
CountSearch(name, email string) int
SetCache(cache UserCache)
GetCache() UserCache
@ -37,14 +39,17 @@ type UserStore interface {
type DefaultUserStore struct {
cache UserCache
get *sql.Stmt
getByName *sql.Stmt
getOffset *sql.Stmt
getAll *sql.Stmt
exists *sql.Stmt
register *sql.Stmt
nameExists *sql.Stmt
count *sql.Stmt
get *sql.Stmt
getByName *sql.Stmt
searchOffset *sql.Stmt
getOffset *sql.Stmt
getAll *sql.Stmt
exists *sql.Stmt
register *sql.Stmt
nameExists *sql.Stmt
count *sql.Stmt
countSearch *sql.Stmt
}
// NewDefaultUserStore gives you a new instance of DefaultUserStore
@ -57,15 +62,20 @@ func NewDefaultUserStore(cache UserCache) (*DefaultUserStore, error) {
allCols := "uid,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"
// TODO: Add an admin version of registerStmt with more flexibility?
return &DefaultUserStore{
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(),
getByName: acc.Select(u).Columns(allCols).Where("name=?").Prepare(),
getOffset: acc.Select(u).Columns(allCols).Orderby("uid ASC").Limit("?,?").Prepare(),
getAll: acc.Select(u).Columns(allCols).Prepare(),
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(),
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(),
getAll: acc.Select(u).Columns(allCols).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
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()
}
@ -170,6 +180,30 @@ func (s *DefaultUserStore) GetOffset(offset, perPage int) (users []*User, err er
}
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 {
rows, e := s.getAll.Query()
if e != nil {
@ -350,6 +384,10 @@ func (s *DefaultUserStore) Count() (count int) {
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) {
s.cache = cache
}

View File

@ -895,6 +895,13 @@
"panel_users_ban":"Ban",
"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_avatar":"Avatar",
"panel_user_avatar_select":"Select",

View File

@ -2,7 +2,9 @@ package panel
import (
"database/sql"
"html/template"
"net/http"
"net/url"
"strconv"
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 {
return ferr
}
name := r.FormValue("s-name")
email := r.FormValue("s-email")
hasParam := name != "" || email != ""
page, _ := strconv.Atoi(r.FormValue("page"))
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)
if err != nil {
return c.InternalError(err, w, r)
var users []*c.User
var e error
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)
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})
}

View File

@ -1,17 +1,17 @@
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_themes_primary_themes"}}</h1></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}}
<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;">
<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>
</span>
<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 .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>
</div>
{{end}}
@ -20,17 +20,17 @@
<div class="colstack_item colstack_head">
<div class="rowitem"><h1>{{lang "panel_themes_variant_themes"}}</h1></div>
</div>
<div id="panel_variant_themes" class="colstack_item panel_themes">
<div id="panel_variant_themes"class="colstack_item panel_themes">
{{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;">
<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>
<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>
</span>
<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 .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>
</div>
{{end}}

View File

@ -19,7 +19,7 @@
</div>
<div class="formrow">
<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>
{{if .CurrentUser.Perms.EditUserPassword}}<div class="formrow">
<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="rowitem"><h1>{{lang "panel_users_head"}}</h1></div>
</div>
<div id="panel_users" class="colstack_item rowlist bgavatars">
<div id="panel_users"class="colstack_item rowlist bgavatars">
{{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}}>
<img class="bgsub"src="{{.Avatar}}"alt="Avatar"aria-hidden="true">
</a>
<a class="rowTitle"{{if $.CurrentUser.Perms.EditUser}}href="/panel/users/edit/{{.ID}}"{{end}}>{{.Name}}</a>
<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 .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 .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}}
</span>
</div>
{{end}}
</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>