1
1
mirror of https://github.com/go-gitea/gitea synced 2025-01-19 06:04:26 +00:00

Implemented User Badge Management Interface (#29798)

Co-authored-by: Diogo Vicente <diogo.m.s.vicente@tecnico.ulisboa.pt>
This commit is contained in:
Henrique Pimentel 2024-05-27 12:05:22 +01:00 committed by Henrique Pimentel
parent 8b86c3140a
commit f68e44daed
9 changed files with 156 additions and 18 deletions

View File

@ -125,6 +125,13 @@ func DeleteUserBadgeRecord(ctx context.Context, badge *Badge) error {
// AddUserBadge adds a badge to a user.
func AddUserBadge(ctx context.Context, u *User, badge *Badge) error {
isExist, err := IsBadgeUserExist(ctx, u.ID, badge.ID)
if err != nil {
return err
} else if isExist {
return ErrBadgeAlreadyExist{}
}
return AddUserBadges(ctx, u, []*Badge{badge})
}
@ -133,11 +140,11 @@ func AddUserBadges(ctx context.Context, u *User, badges []*Badge) error {
return db.WithTx(ctx, func(ctx context.Context) error {
for _, badge := range badges {
// hydrate badge and check if it exists
has, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Get(badge)
has, err := db.GetEngine(ctx).Where("id=?", badge.ID).Get(badge)
if err != nil {
return err
} else if !has {
return fmt.Errorf("badge with slug %s doesn't exist", badge.Slug)
return ErrBadgeNotExist{ID: badge.ID}
}
if err := db.Insert(ctx, &UserBadge{
BadgeID: badge.ID,
@ -159,10 +166,7 @@ func RemoveUserBadge(ctx context.Context, u *User, badge *Badge) error {
func RemoveUserBadges(ctx context.Context, u *User, badges []*Badge) error {
return db.WithTx(ctx, func(ctx context.Context) error {
for _, badge := range badges {
if _, err := db.GetEngine(ctx).
Join("INNER", "badge", "badge.id = `user_badge`.badge_id").
Where("`user_badge`.user_id=? AND `badge`.slug=?", u.ID, badge.Slug).
Delete(&UserBadge{}); err != nil {
if _, err := db.GetEngine(ctx).Delete(&UserBadge{BadgeID: badge.ID, UserID: u.ID}); err != nil {
return err
}
}
@ -192,6 +196,12 @@ func IsBadgeExist(ctx context.Context, uid int64, slug string) (bool, error) {
Get(&Badge{Slug: strings.ToLower(slug)})
}
// IsBadgeUserExist checks if given badge id, uid exist,
func IsBadgeUserExist(ctx context.Context, uid, bid int64) (bool, error) {
return db.GetEngine(ctx).
Get(&UserBadge{UserID: uid, BadgeID: bid})
}
// SearchBadgeOptions represents the options when fdin badges
type SearchBadgeOptions struct {
db.ListOptions

View File

@ -141,7 +141,7 @@ func IsErrBadgeNotExist(err error) bool {
}
func (err ErrBadgeNotExist) Error() string {
return fmt.Sprintf("badge does not exist [slug: %s | id: %i]", err.Slug, err.ID)
return fmt.Sprintf("badge does not exist [slug: %s | id: %d]", err.Slug, err.ID)
}
// Unwrap unwraps this error as a ErrNotExist error

View File

@ -2988,6 +2988,14 @@ badges.edit_badge = Edit Badge
badges.update_badge = Update Badge
badges.delete_badge = Delete Badge
badges.delete_badge_desc = Are you sure you want to permanently delete this badge?
badges.users_with_badge = Users with Badge (%d)
badges.add_user = Add User
badges.remove_user = Remove User
badges.delete_user_desc = Are you sure you want to remove this badge from the user?
badges.not_found = Badge not found!
badges.user_add_success = User has been added to the badge.
badges.user_remove_success = User has been removed from the badge.
badges.manage_users = Manage Users
orgs.org_manage_panel = Organization Management

View File

@ -8,6 +8,7 @@ import (
"net/http"
"net/url"
"strconv"
"strings"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
@ -22,10 +23,11 @@ import (
)
const (
tplBadges base.TplName = "admin/badge/list"
tplBadgeNew base.TplName = "admin/badge/new"
tplBadgeView base.TplName = "admin/badge/view"
tplBadgeEdit base.TplName = "admin/badge/edit"
tplBadges base.TplName = "admin/badge/list"
tplBadgeNew base.TplName = "admin/badge/new"
tplBadgeView base.TplName = "admin/badge/view"
tplBadgeEdit base.TplName = "admin/badge/edit"
tplBadgeUsers base.TplName = "admin/badge/users"
)
// BadgeSearchDefaultAdminSort is the default sort type for admin view
@ -213,3 +215,67 @@ func DeleteBadge(ctx *context.Context) {
ctx.Flash.Success(ctx.Tr("admin.badges.deletion_success"))
ctx.Redirect(setting.AppSubURL + "/admin/badges")
}
func BadgeUsers(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.badges.users_with_badge", ctx.ParamsInt64(":badgeid"))
ctx.Data["PageIsAdminBadges"] = true
users, _, err := user_model.GetBadgeUsers(ctx, &user_model.Badge{ID: ctx.ParamsInt64(":badgeid")})
if err != nil {
ctx.ServerError("GetBadgeUsers", err)
return
}
ctx.Data["Users"] = users
ctx.HTML(http.StatusOK, tplBadgeUsers)
}
// BadgeUsersPost response for actions for user badges
func BadgeUsersPost(ctx *context.Context) {
name := strings.ToLower(ctx.FormString("user"))
u, err := user_model.GetUserByName(ctx, name)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Flash.Error(ctx.Tr("form.user_not_exist"))
ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath())
} else {
ctx.ServerError("GetUserByName", err)
}
return
}
if err = user_model.AddUserBadge(ctx, u, &user_model.Badge{ID: ctx.ParamsInt64(":badgeid")}); err != nil {
if user_model.IsErrBadgeNotExist(err) {
ctx.Flash.Error(ctx.Tr("admin.badges.not_found"))
} else {
ctx.ServerError("AddUserBadge", err)
}
return
}
ctx.Flash.Success(ctx.Tr("admin.badges.user_add_success"))
ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath())
}
// DeleteBadgeUser delete a badge from a user
func DeleteBadgeUser(ctx *context.Context) {
if user, err := user_model.GetUserByID(ctx, ctx.FormInt64("id")); err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.Flash.Error(ctx.Tr("form.user_not_exist"))
} else {
ctx.ServerError("GetUserByName", err)
return
}
} else {
if err := user_model.RemoveUserBadge(ctx, user, &user_model.Badge{ID: ctx.ParamsInt64(":badgeid")}); err == nil {
ctx.Flash.Success(ctx.Tr("admin.badges.user_remove_success"))
} else {
ctx.Flash.Error("DeleteUser: " + err.Error())
return
}
}
ctx.JSONRedirect(setting.AppSubURL + "/admin/badges/" + ctx.Params(":badgeid") + "/users")
}

View File

@ -56,9 +56,8 @@ func RenderBadgeSearch(ctx *context.Context, opts *user_model.SearchBadgeOptions
orderBy = "`badge`.slug ASC"
default:
// in case the sortType is not valid, we set it to recent update
sortOrder = "alphabetically"
ctx.Data["SortType"] = "alphabetically"
orderBy = "`badge`.slug ASC"
ctx.Data["SortType"] = "oldest"
orderBy = "`badge`.id ASC"
}
opts.Keyword = ctx.FormTrim("q")

View File

@ -726,6 +726,8 @@ func registerRoutes(m *web.Router) {
m.Get("/{badgeid}", admin.ViewBadge)
m.Combo("/{badgeid}/edit").Get(admin.EditBadge).Post(web.Bind(forms.AdminCreateBadgeForm{}), admin.EditBadgePost)
m.Post("/{badgeid}/delete", admin.DeleteBadge)
m.Combo("/{badgeid}/users").Get(admin.BadgeUsers).Post(admin.BadgeUsersPost)
m.Post("/{badgeid}/users/delete", admin.DeleteBadgeUser)
})
m.Group("/emails", func() {

View File

@ -52,7 +52,7 @@
<tr>
<td>{{.ID}}</td>
<td>
<a href="">{{.Slug}}</a>
<a href="{{$.Link}}/{{.ID}}">{{.Slug}}</a>
</td>
<td class="gt-ellipsis tw-max-w-48">{{.Description}}</td>
<td></td>
@ -62,7 +62,7 @@
<td></td>
<td>
<div class="tw-flex tw-gap-2">
<a href="{{$.Link}}/{{.ID}}" data-tooltip-content="{{ctx.Locale.Tr "admin.users.details"}}">{{svg "octicon-star"}}</a>
<a href="{{$.Link}}/{{.ID}}" data-tooltip-content="{{ctx.Locale.Tr "admin.badges.details"}}">{{svg "octicon-star"}}</a>
<a href="{{$.Link}}/{{.ID}}/edit" data-tooltip-content="{{ctx.Locale.Tr "edit"}}">{{svg "octicon-pencil"}}</a>
</div>
</td>

View File

@ -0,0 +1,53 @@
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin user")}}
<div class="admin-setting-content">
<h4 class="ui top attached header">
{{.Title}}
</h4>
{{if .Users}}
<div class="ui attached segment">
<div class="flex-list">
{{range .Users}}
<div class="flex-item tw-items-center">
<div class="flex-item-leading">
<a href="{{.HomeLink}}">{{ctx.AvatarUtils.Avatar . 32}}</a>
</div>
<div class="flex-item-main">
<div class="flex-item-title">
{{template "shared/user/name" .}}
</div>
</div>
<div class="flex-item-trailing">
<button class="ui red tiny button inline delete-button" data-url="{{$.Link}}/delete" data-id="{{.ID}}">
{{ctx.Locale.Tr "admin.badges.remove_user"}}
</button>
</div>
</div>
{{end}}
</div>
</div>
{{end}}
<div class="ui bottom attached segment">
<form class="ui form" id="search-badge-user-form" action="{{.Link}}" method="POST">
{{.CsrfTokenHtml}}
<div id="search-user-box" class="ui search input tw-align-middle">
<input class="prompt" name="user" placeholder="{{ctx.Locale.Tr "search.user_kind"}}" autocomplete="off" autofocus required>
</div>
<button class="ui primary button">{{ctx.Locale.Tr "admin.badges.add_user"}}</button>
</form>
</div>
<div class="ui g-modal-confirm delete modal">
<div class="header">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "admin.badges.remove_user"}}
</div>
<form class="ui form" method="post" id="remove-badge-user-form" action="{{.Link}}">
<div class="content">
{{$.CsrfTokenHtml}}
<p>{{ctx.Locale.Tr "admin.badges.delete_user_desc"}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</form>
</div>
{{template "admin/layout_footer" .}}

View File

@ -31,9 +31,9 @@
</div>
</div>
<h4 class="ui top attached header">
{{ctx.Locale.Tr "explore.users"}}
{{ctx.Locale.Tr "explore.users"}} ({{.UsersTotal}})
<div class="ui right">
{{.UsersTotal}}
<a class="ui primary tiny button" href="{{.Link}}/users">{{ctx.Locale.Tr "admin.badges.manage_users"}}</a>
</div>
</h4>
<div class="ui attached segment">