Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Manage User Badges in the interface #29798 #31262

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 117 additions & 6 deletions models/user/badge.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ package user
import (
"context"
"fmt"
"strings"

"code.gitea.io/gitea/models/db"

"xorm.io/builder"
"xorm.io/xorm"
)

// Badge represents a user badge
Expand Down Expand Up @@ -42,13 +46,27 @@ func GetUserBadges(ctx context.Context, u *User) ([]*Badge, int64, error) {
return badges, count, err
}

// GetBadgeUsers returns the users that have a specific badge.
func GetBadgeUsers(ctx context.Context, b *Badge) ([]*User, int64, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think pagination is necessary here because you don't know how many users will have the badges which maybe a big number.

sess := db.GetEngine(ctx).
Select("`user`.*").
Join("INNER", "user_badge", "`user_badge`.user_id=user.id").
Join("INNER", "badge", "`user_badge`.badge_id=badge.id").
Where("badge.slug=?", b.Slug)
users := make([]*User, 0, 8)
count, err := sess.FindAndCount(&users)
return users, count, err
}

// CreateBadge creates a new badge.
func CreateBadge(ctx context.Context, badge *Badge) error {
// this will fail if the badge already exists due to the UNIQUE constraint
_, err := db.GetEngine(ctx).Insert(badge)

return err
}

// GetBadge returns a badge
// GetBadge returns a specific badge
func GetBadge(ctx context.Context, slug string) (*Badge, error) {
badge := new(Badge)
has, err := db.GetEngine(ctx).Where("slug=?", slug).Get(badge)
Expand All @@ -60,7 +78,7 @@ func GetBadge(ctx context.Context, slug string) (*Badge, error) {

// UpdateBadge updates a badge based on its slug.
func UpdateBadge(ctx context.Context, badge *Badge) error {
_, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Update(badge)
_, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Cols("description", "image_url").Update(badge)
return err
}

Expand All @@ -84,7 +102,7 @@ func AddUserBadges(ctx context.Context, u *User, badges []*Badge) error {
if err != nil {
return err
} else if !has {
return fmt.Errorf("badge with slug %s doesn't exist", badge.Slug)
return ErrBadgeNotExist{Slug: badge.Slug}
}
if err := db.Insert(ctx, &UserBadge{
BadgeID: badge.ID,
Expand All @@ -102,13 +120,18 @@ func RemoveUserBadge(ctx context.Context, u *User, badge *Badge) error {
return RemoveUserBadges(ctx, u, []*Badge{badge})
}

// RemoveUserBadges removes badges from a user.
// RemoveUserBadges removes specific badges from a user.
func RemoveUserBadges(ctx context.Context, u *User, badges []*Badge) error {
return db.WithTx(ctx, func(ctx context.Context) error {
for _, badge := range badges {
subQuery := builder.
Select("id").
From("badge").
Where(builder.Eq{"slug": badge.Slug})

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).
Where("`user_badge`.user_id=?", u.ID).
And(builder.In("badge_id", subQuery)).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why use subquery instead of delete with join?

Delete(&UserBadge{}); err != nil {
return err
}
Expand All @@ -122,3 +145,91 @@ func RemoveAllUserBadges(ctx context.Context, u *User) error {
_, err := db.GetEngine(ctx).Where("user_id=?", u.ID).Delete(&UserBadge{})
return err
}

// SearchBadgeOptions represents the options when fdin badges
type SearchBadgeOptions struct {
db.ListOptions

Keyword string
Slug string
ID int64
OrderBy db.SearchOrderBy
Actor *User // The user doing the search

ExtraParamStrings map[string]string
}

func (opts *SearchBadgeOptions) ToConds() builder.Cond {
cond := builder.NewCond()

if opts.Keyword != "" {
cond = cond.And(builder.Like{"badge.slug", opts.Keyword})
}

return cond
}

func (opts *SearchBadgeOptions) ToOrders() string {
orderBy := "badge.slug"
return orderBy
}

func (opts *SearchBadgeOptions) ToJoins() []db.JoinFunc {
return []db.JoinFunc{
func(e db.Engine) error {
e.Join("INNER", "badge", "`user_badge`.badge_id=badge.id")
return nil
},
}
}

func SearchBadges(ctx context.Context, opts *SearchBadgeOptions) (badges []*Badge, _ int64, _ error) {
sessCount := opts.toSearchQueryBase(ctx)
count, err := sessCount.Count(new(Badge))
techknowlogick marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, 0, fmt.Errorf("count: %w", err)
}
sessCount.Close()

if len(opts.OrderBy) == 0 {
opts.OrderBy = db.SearchOrderByID
}

sessQuery := opts.toSearchQueryBase(ctx).OrderBy(opts.OrderBy.String())
defer sessQuery.Close()
if opts.Page != 0 {
sessQuery = db.SetSessionPagination(sessQuery, opts)
}

// the sql may contain JOIN, so we must only select Badge related columns
sessQuery = sessQuery.Select("`badge`.*")
badges = make([]*Badge, 0, opts.PageSize)
return badges, count, sessQuery.Find(&badges)
}

func (opts *SearchBadgeOptions) toSearchQueryBase(ctx context.Context) *xorm.Session {
var cond builder.Cond
cond = builder.Neq{"id": -1}
techknowlogick marked this conversation as resolved.
Show resolved Hide resolved

if len(opts.Keyword) > 0 {
lowerKeyword := strings.ToLower(opts.Keyword)
keywordCond := builder.Or(
builder.Like{"slug", lowerKeyword},
builder.Like{"description", lowerKeyword},
builder.Like{"id", lowerKeyword},
)
cond = cond.And(keywordCond)
}

if opts.ID > 0 {
cond = cond.And(builder.Eq{"id": opts.ID})
}

if len(opts.Slug) > 0 {
cond = cond.And(builder.Eq{"slug": opts.Slug})
}

e := db.GetEngine(ctx)

return e.Where(cond)
}
40 changes: 40 additions & 0 deletions models/user/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,43 @@ func IsErrUserIsNotLocal(err error) bool {
_, ok := err.(ErrUserIsNotLocal)
return ok
}

// ErrBadgeAlreadyExist represents a "badge already exists" error.
type ErrBadgeAlreadyExist struct {
Slug string
}

// IsErrBadgeAlreadyExist checks if an error is a ErrBadgeAlreadyExist.
func IsErrBadgeAlreadyExist(err error) bool {
_, ok := err.(ErrBadgeAlreadyExist)
return ok
}

func (err ErrBadgeAlreadyExist) Error() string {
return fmt.Sprintf("badge already exists [slug: %s]", err.Slug)
}

// Unwrap unwraps this error as a ErrExist error
func (err ErrBadgeAlreadyExist) Unwrap() error {
return util.ErrAlreadyExist
}

// ErrBadgeNotExist represents a "BadgeNotExist" kind of error.
type ErrBadgeNotExist struct {
Slug string
}

// IsErrBadgeNotExist checks if an error is a ErrBadgeNotExist.
func IsErrBadgeNotExist(err error) bool {
_, ok := err.(ErrBadgeNotExist)
return ok
}

func (err ErrBadgeNotExist) Error() string {
return fmt.Sprintf("badge does not exist [slug: %s]", err.Slug)
}

// Unwrap unwraps this error as a ErrNotExist error
func (err ErrBadgeNotExist) Unwrap() error {
return util.ErrNotExist
}
19 changes: 19 additions & 0 deletions modules/validation/binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ const (
ErrUsername = "UsernameError"
// ErrInvalidGroupTeamMap is returned when a group team mapping is invalid
ErrInvalidGroupTeamMap = "InvalidGroupTeamMap"
// ErrInvalidSlug is returned when a slug is invalid
ErrInvalidSlug = "InvalidSlug"
)

// AddBindingRules adds additional binding rules
Expand All @@ -38,6 +40,7 @@ func AddBindingRules() {
addGlobOrRegexPatternRule()
addUsernamePatternRule()
addValidGroupTeamMapRule()
addSlugPatternRule()
}

func addGitRefNameBindingRule() {
Expand Down Expand Up @@ -94,6 +97,22 @@ func addValidSiteURLBindingRule() {
})
}

func addSlugPatternRule() {
binding.AddRule(&binding.Rule{
IsMatch: func(rule string) bool {
return rule == "Slug"
},
IsValid: func(errs binding.Errors, name string, val any) (bool, binding.Errors) {
str := fmt.Sprintf("%v", val)
if !IsValidSlug(str) {
errs.Add([]string{name}, ErrInvalidSlug, "invalid slug")
return false, errs
}
return true, errs
},
})
}

func addGlobPatternRule() {
binding.AddRule(&binding.Rule{
IsMatch: func(rule string) bool {
Expand Down
4 changes: 4 additions & 0 deletions modules/validation/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,7 @@ func IsValidUsername(name string) bool {
// but it's easier to use positive and negative checks.
return validUsernamePattern.MatchString(name) && !invalidUsernamePattern.MatchString(name)
}

func IsValidSlug(slug string) bool {
return IsValidUsername(slug)
}
2 changes: 2 additions & 0 deletions modules/web/middleware/binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Lo
data["ErrorMsg"] = trName + l.TrString("form.username_error")
case validation.ErrInvalidGroupTeamMap:
data["ErrorMsg"] = trName + l.TrString("form.invalid_group_team_map_error", errs[0].Message)
case validation.ErrInvalidSlug:
data["ErrorMsg"] = l.TrString("form.invalid_slug_error")
default:
msg := errs[0].Classification
if msg != "" && errs[0].Message != "" {
Expand Down
30 changes: 30 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ exact = Exact
exact_tooltip = Include only results that match the exact search term
repo_kind = Search repos...
user_kind = Search users...
badge_kind = Search badges...
org_kind = Search orgs...
team_kind = Search teams...
code_kind = Search code...
Expand Down Expand Up @@ -548,6 +549,7 @@ PayloadUrl = Payload URL
TeamName = Team name
AuthName = Authorization name
AdminEmail = Admin email
ImageURL = Image URL

NewBranchName = New branch name
CommitSummary = Commit summary
Expand Down Expand Up @@ -577,12 +579,15 @@ unknown_error = Unknown error:
captcha_incorrect = The CAPTCHA code is incorrect.
password_not_match = The passwords do not match.
lang_select_error = Select a language from the list.
invalid_image_url_error = `Please provide a valid image URL.`
invalid_slug_error = `Please provide a valid slug.`

username_been_taken = The username is already taken.
username_change_not_local_user = Non-local users are not allowed to change their username.
change_username_disabled = Changing username is disabled.
change_full_name_disabled = Changing full name is disabled.
username_has_not_been_changed = Username has not been changed
slug_been_taken = The slug is already taken.
repo_name_been_taken = The repository name is already used.
repository_force_private = Force Private is enabled: private repositories cannot be made public.
repository_files_already_exist = Files already exist for this repository. Contact the system administrator.
Expand Down Expand Up @@ -2844,6 +2849,7 @@ dashboard = Dashboard
self_check = Self Check
identity_access = Identity & Access
users = User Accounts
badges = Badges
organizations = Organizations
assets = Code Assets
repositories = Repositories
Expand Down Expand Up @@ -3023,6 +3029,30 @@ emails.delete_desc = Are you sure you want to delete this email address?
emails.deletion_success = The email address has been deleted.
emails.delete_primary_email_error = You can not delete the primary email.

badges.badges_manage_panel = Badge Management
badges.details = Badge Details
badges.new_badge = Create New Badge
badges.slug = Slug
badges.description = Description
badges.image_url = Image URL
badges.slug.must_fill = Slug must be filled.
badges.new_success = The badge "%s" has been created.
badges.update_success = The badge has been updated.
badges.deletion_success = The badge has been deleted.
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 (%s)
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
orgs.name = Name
orgs.teams = Teams
Expand Down
Loading
Loading