diff --git a/models/user/badge.go b/models/user/badge.go index f5dff4eeae..c19e15da0b 100644 --- a/models/user/badge.go +++ b/models/user/badge.go @@ -9,9 +9,9 @@ import ( "strings" "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/modules/util" "xorm.io/builder" - "xorm.io/xorm" ) // Badge represents a user badge @@ -29,6 +29,50 @@ type UserBadge struct { //nolint:revive UserID int64 `xorm:"INDEX"` } +// 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 + ID int64 +} + +func (err ErrBadgeNotExist) Error() string { + if err.ID > 0 { + return fmt.Sprintf("badge does not exist [id: %d]", err.ID) + } + return fmt.Sprintf("badge does not exist [slug: %s]", err.Slug) +} + +// IsErrBadgeNotExist checks if an error is a ErrBadgeNotExist. +func IsErrBadgeNotExist(err error) bool { + _, ok := err.(ErrBadgeNotExist) + return ok +} + +// Unwrap unwraps this error as a ErrNotExist error +func (err ErrBadgeNotExist) Unwrap() error { + return util.ErrNotExist +} + func init() { db.RegisterModel(new(Badge)) db.RegisterModel(new(UserBadge)) @@ -73,7 +117,6 @@ func GetBadgeUsers(ctx context.Context, opts *GetBadgeUsersOptions) ([]*User, in 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 } @@ -151,11 +194,14 @@ func RemoveUserBadges(ctx context.Context, u *User, badges []*Badge) error { slugs[i] = badge.Slug } + var badgeIDs []int64 + if err := db.GetEngine(ctx).Table("badge").In("slug", slugs).Cols("id").Find(&badgeIDs); err != nil { + return err + } + if _, err := db.GetEngine(ctx). - Table("user_badge"). - Join("INNER", "badge", "`user_badge`.badge_id = badge.id"). - Where("`user_badge`.user_id = ?", u.ID). - And(builder.In("badge.slug", slugs)). + Where("user_id = ?", u.ID). + In("badge_id", badgeIDs). Delete(&UserBadge{}); err != nil { return err } @@ -184,66 +230,29 @@ 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 SearchBadges(ctx context.Context, opts *SearchBadgeOptions) (badges []*Badge, _ int64, _ error) { - sessCount := opts.toSearchQueryBase(ctx) - count, err := sessCount.Count(new(Badge)) - 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} - - 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}, + builder.Like{"badge.slug", lowerKeyword}, + builder.Like{"badge.description", lowerKeyword}, + builder.Like{"badge.id", lowerKeyword}, ) cond = cond.And(keywordCond) } if opts.ID > 0 { - cond = cond.And(builder.Eq{"id": opts.ID}) + cond = cond.And(builder.Eq{"badge.id": opts.ID}) } if len(opts.Slug) > 0 { - cond = cond.And(builder.Eq{"slug": opts.Slug}) + cond = cond.And(builder.Eq{"badge.slug": opts.Slug}) } - e := db.GetEngine(ctx) + return cond +} - return e.Where(cond) +// SearchBadges returns badges based on the provided SearchBadgeOptions options +func SearchBadges(ctx context.Context, opts *SearchBadgeOptions) ([]*Badge, int64, error) { + return db.FindAndCount[Badge](ctx, opts) } // GetBadgeByID returns a specific badge by ID diff --git a/models/user/badge_test.go b/models/user/badge_test.go new file mode 100644 index 0000000000..0dbd35f037 --- /dev/null +++ b/models/user/badge_test.go @@ -0,0 +1,61 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user_test + +import ( + "testing" + + "code.gitea.io/gitea/models/db" + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + + "github.com/stretchr/testify/assert" +) + +func TestGetBadgeUsers(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + // Create a test badge + badge := &user_model.Badge{ + Slug: "test-badge", + Description: "Test Badge", + ImageURL: "test.png", + } + assert.NoError(t, user_model.CreateBadge(db.DefaultContext, badge)) + + // Create test users and assign badges + user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + assert.NoError(t, user_model.AddUserBadge(db.DefaultContext, user1, badge)) + assert.NoError(t, user_model.AddUserBadge(db.DefaultContext, user2, badge)) + + // Test getting users with pagination + opts := &user_model.GetBadgeUsersOptions{ + Badge: badge, + ListOptions: db.ListOptions{ + Page: 1, + PageSize: 1, + }, + } + + users, count, err := user_model.GetBadgeUsers(db.DefaultContext, opts) + assert.NoError(t, err) + assert.EqualValues(t, 2, count) + assert.Len(t, users, 1) + + // Test second page + opts.Page = 2 + users, count, err = user_model.GetBadgeUsers(db.DefaultContext, opts) + assert.NoError(t, err) + assert.EqualValues(t, 2, count) + assert.Len(t, users, 1) + + // Test with non-existent badge + opts.Badge = &user_model.Badge{Slug: "non-existent"} + users, count, err = user_model.GetBadgeUsers(db.DefaultContext, opts) + assert.NoError(t, err) + assert.EqualValues(t, 0, count) + assert.Len(t, users, 0) +} diff --git a/models/user/error.go b/models/user/error.go index 7843558aaf..cbf19998d1 100644 --- a/models/user/error.go +++ b/models/user/error.go @@ -107,47 +107,3 @@ 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 - ID int64 -} - -func (err ErrBadgeNotExist) Error() string { - if err.ID > 0 { - return fmt.Sprintf("badge does not exist [id: %d]", err.ID) - } - return fmt.Sprintf("badge does not exist [slug: %s]", err.Slug) -} - -// IsErrBadgeNotExist checks if an error is a ErrBadgeNotExist. -func IsErrBadgeNotExist(err error) bool { - _, ok := err.(ErrBadgeNotExist) - return ok -} - -// Unwrap unwraps this error as a ErrNotExist error -func (err ErrBadgeNotExist) Unwrap() error { - return util.ErrNotExist -} diff --git a/routers/web/admin/badges.go b/routers/web/admin/badges.go index 19d3ae6a85..e34bc4a09e 100644 --- a/routers/web/admin/badges.go +++ b/routers/web/admin/badges.go @@ -286,7 +286,7 @@ func DeleteBadgeUser(ctx *context.Context) { if err := user_model.RemoveUserBadge(ctx, user, &user_model.Badge{Slug: ctx.PathParam(":badge_slug")}); err == nil { ctx.Flash.Success(ctx.Tr("admin.badges.user_remove_success")) } else { - ctx.Flash.Error("DeleteUser: " + err.Error()) + ctx.Flash.Error("DeleteBadgeUser: " + err.Error()) } ctx.JSONRedirect(fmt.Sprintf("%s/-/admin/badges/%s/users", setting.AppSubURL, ctx.PathParam(":badge_slug")))