Skip to content

Commit

Permalink
Implemented User Badge Management Interface (#29798)
Browse files Browse the repository at this point in the history
Co-authored-by: Diogo Vicente <[email protected]>
  • Loading branch information
HenriquerPimentel and Diogo Vicente committed Jun 5, 2024
1 parent 7e734b6 commit 91b085b
Show file tree
Hide file tree
Showing 9 changed files with 156 additions and 18 deletions.
22 changes: 16 additions & 6 deletions models/user/badge.go
Original file line number Diff line number Diff line change
Expand Up @@ -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})
}

Expand All @@ -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,
Expand All @@ -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
}
}
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion models/user/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2982,6 +2982,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
Expand Down
74 changes: 70 additions & 4 deletions routers/web/admin/badges.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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")
}
5 changes: 2 additions & 3 deletions routers/web/explore/badge.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 2 additions & 0 deletions routers/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,8 @@ func registerRoutes(m *web.Route) {
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() {
Expand Down
4 changes: 2 additions & 2 deletions templates/admin/badge/list.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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>
Expand Down
53 changes: 53 additions & 0 deletions templates/admin/badge/users.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin user")}}
<div class="repo-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" .}}
4 changes: 2 additions & 2 deletions templates/admin/badge/view.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down

0 comments on commit 91b085b

Please sign in to comment.