Add user badges (#36752)
Implemented #29798 This feature implements list badges, create new badges, view badge, edit badge and assign badge to users. - List all badges  - Create new badges  - View badge   - Edit badge  - Add user to badge 
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
-
|
||||
id: 1
|
||||
slug: badge1
|
||||
description: just a test badge
|
||||
image_url: badge1.png
|
||||
@@ -403,6 +403,7 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(326, "Migrate commit status target URL to use run ID and job ID", v1_26.FixCommitStatusTargetURLToUseRunAndJobID),
|
||||
newMigration(327, "Add disabled state to action runners", v1_26.AddDisabledToActionRunner),
|
||||
newMigration(328, "Add TokenPermissions column to ActionRunJob", v1_26.AddTokenPermissionsToActionRunJob),
|
||||
newMigration(329, "Add unique constraint for user badge", v1_26.AddUniqueIndexForUserBadge),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_26
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
type UserBadge struct { //revive:disable-line:exported
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
BadgeID int64
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// TableIndices implements xorm's TableIndices interface
|
||||
func (n *UserBadge) TableIndices() []*schemas.Index {
|
||||
indices := make([]*schemas.Index, 0, 1)
|
||||
ubUnique := schemas.NewIndex("unique_user_badge", schemas.UniqueType)
|
||||
ubUnique.AddColumn("user_id", "badge_id")
|
||||
indices = append(indices, ubUnique)
|
||||
return indices
|
||||
}
|
||||
|
||||
// AddUniqueIndexForUserBadge adds a compound unique indexes for user badge table
|
||||
// and it replaces an old index on user_id
|
||||
func AddUniqueIndexForUserBadge(x *xorm.Engine) error {
|
||||
// remove possible duplicated records in table user_badge
|
||||
type result struct {
|
||||
UserID int64
|
||||
BadgeID int64
|
||||
Cnt int
|
||||
}
|
||||
var results []result
|
||||
if err := x.Select("user_id, badge_id, count(*) as cnt").
|
||||
Table("user_badge").
|
||||
GroupBy("user_id, badge_id").
|
||||
Having("count(*) > 1").
|
||||
Find(&results); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, r := range results {
|
||||
if x.Dialect().URI().DBType == schemas.MSSQL {
|
||||
if _, err := x.Exec(fmt.Sprintf("delete from user_badge where id in (SELECT top %d id FROM user_badge WHERE user_id = ? and badge_id = ?)", r.Cnt-1), r.UserID, r.BadgeID); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
var ids []int64
|
||||
if err := x.SQL("SELECT id FROM user_badge WHERE user_id = ? and badge_id = ? limit ?", r.UserID, r.BadgeID, r.Cnt-1).Find(&ids); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := x.Table("user_badge").In("id", ids).Delete(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return x.Sync(new(UserBadge))
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_26
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/migrations/base"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type UserBadgeBefore struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
BadgeID int64
|
||||
UserID int64 `xorm:"INDEX"`
|
||||
}
|
||||
|
||||
func (UserBadgeBefore) TableName() string {
|
||||
return "user_badge"
|
||||
}
|
||||
|
||||
func Test_AddUniqueIndexForUserBadge(t *testing.T) {
|
||||
x, deferable := base.PrepareTestEnv(t, 0, new(UserBadgeBefore))
|
||||
defer deferable()
|
||||
if x == nil || t.Failed() {
|
||||
return
|
||||
}
|
||||
|
||||
testData := []*UserBadgeBefore{
|
||||
{UserID: 1, BadgeID: 1},
|
||||
{UserID: 1, BadgeID: 1}, // duplicate
|
||||
{UserID: 2, BadgeID: 1},
|
||||
{UserID: 1, BadgeID: 2},
|
||||
{UserID: 3, BadgeID: 3},
|
||||
{UserID: 3, BadgeID: 3}, // duplicate
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
_, err := x.Insert(data)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// check that we have duplicates
|
||||
count, err := x.Where("user_id = ? AND badge_id = ?", 1, 1).Count(&UserBadgeBefore{})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(2), count)
|
||||
|
||||
count, err = x.Where("user_id = ? AND badge_id = ?", 3, 3).Count(&UserBadgeBefore{})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(2), count)
|
||||
|
||||
totalCount, err := x.Count(&UserBadgeBefore{})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(6), totalCount)
|
||||
|
||||
// run the migration
|
||||
if err := AddUniqueIndexForUserBadge(x); err != nil {
|
||||
assert.NoError(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
// verify the duplicates were removed
|
||||
count, err = x.Where("user_id = ? AND badge_id = ?", 1, 1).Count(&UserBadgeBefore{})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(1), count)
|
||||
|
||||
count, err = x.Where("user_id = ? AND badge_id = ?", 3, 3).Count(&UserBadgeBefore{})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(1), count)
|
||||
|
||||
// check total count
|
||||
totalCount, err = x.Count(&UserBadgeBefore{})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(4), totalCount)
|
||||
|
||||
// fail to insert a duplicate
|
||||
_, err = x.Insert(&UserBadge{UserID: 1, BadgeID: 1})
|
||||
assert.Error(t, err)
|
||||
|
||||
// succeed adding a non-duplicate
|
||||
_, err = x.Insert(&UserBadge{UserID: 4, BadgeID: 1})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
+170
-21
@@ -5,9 +5,12 @@ package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
// Badge represents a user badge
|
||||
@@ -22,7 +25,16 @@ type Badge struct {
|
||||
type UserBadge struct { //nolint:revive // export stutter
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
BadgeID int64
|
||||
UserID int64 `xorm:"INDEX"`
|
||||
UserID int64
|
||||
}
|
||||
|
||||
// TableIndices implements xorm's TableIndices interface
|
||||
func (n *UserBadge) TableIndices() []*schemas.Index {
|
||||
indices := make([]*schemas.Index, 0, 1)
|
||||
ubUnique := schemas.NewIndex("unique_user_badge", schemas.UniqueType)
|
||||
ubUnique.AddColumn("user_id", "badge_id")
|
||||
indices = append(indices, ubUnique)
|
||||
return indices
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -42,32 +54,85 @@ func GetUserBadges(ctx context.Context, u *User) ([]*Badge, int64, error) {
|
||||
return badges, count, err
|
||||
}
|
||||
|
||||
// CreateBadge creates a new badge.
|
||||
func CreateBadge(ctx context.Context, badge *Badge) error {
|
||||
_, err := db.GetEngine(ctx).Insert(badge)
|
||||
return err
|
||||
// GetBadgeUsersOptions contains options for getting users with a specific badge
|
||||
type GetBadgeUsersOptions struct {
|
||||
db.ListOptions
|
||||
BadgeSlug string
|
||||
}
|
||||
|
||||
// GetBadge returns a badge
|
||||
// GetBadgeUsers returns the users that have a specific badge with pagination support.
|
||||
func GetBadgeUsers(ctx context.Context, opts *GetBadgeUsersOptions) ([]*User, int64, error) {
|
||||
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=?", opts.BadgeSlug)
|
||||
|
||||
if opts.Page > 0 {
|
||||
sess = db.SetSessionPagination(sess, opts)
|
||||
}
|
||||
|
||||
users := make([]*User, 0, opts.PageSize)
|
||||
count, err := sess.FindAndCount(&users)
|
||||
return users, count, err
|
||||
}
|
||||
|
||||
// CreateBadge creates a new badge.
|
||||
func CreateBadge(ctx context.Context, badge *Badge) error {
|
||||
exists, err := db.GetEngine(ctx).Where("slug = ?", badge.Slug).Exist(new(Badge))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return util.NewAlreadyExistErrorf("badge already exists [slug: %s]", badge.Slug)
|
||||
}
|
||||
|
||||
if _, err := db.GetEngine(ctx).Insert(badge); err != nil {
|
||||
// Handle race between existence check and insert.
|
||||
exists, existErr := db.GetEngine(ctx).Where("slug = ?", badge.Slug).Exist(new(Badge))
|
||||
if existErr == nil && exists {
|
||||
return util.NewAlreadyExistErrorf("badge already exists [slug: %s]", badge.Slug)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
if !has {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return badge, err
|
||||
if !has {
|
||||
return nil, util.NewNotExistErrorf("badge does not exist [slug: %s]", slug)
|
||||
}
|
||||
return badge, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// DeleteBadge deletes a badge.
|
||||
// DeleteBadge deletes a badge and all associated user_badge entries.
|
||||
func DeleteBadge(ctx context.Context, badge *Badge) error {
|
||||
_, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Delete(badge)
|
||||
return err
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
// First delete all user_badge entries for this badge
|
||||
if _, err := db.GetEngine(ctx).
|
||||
Where("badge_id = ?", badge.ID).
|
||||
Delete(&UserBadge{}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Then delete the badge itself
|
||||
if _, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Delete(badge); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// AddUserBadge adds a badge to a user.
|
||||
@@ -84,12 +149,25 @@ 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 util.NewNotExistErrorf("badge does not exist [slug: %s]", badge.Slug)
|
||||
}
|
||||
|
||||
exists, err := db.GetEngine(ctx).Where("badge_id = ? AND user_id = ?", badge.ID, u.ID).Exist(new(UserBadge))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return util.NewAlreadyExistErrorf("user badge already exists [user_id: %d, badge_id: %d]", u.ID, badge.ID)
|
||||
}
|
||||
|
||||
if err := db.Insert(ctx, &UserBadge{
|
||||
BadgeID: badge.ID,
|
||||
UserID: u.ID,
|
||||
}); err != nil {
|
||||
exists, existErr := db.GetEngine(ctx).Where("badge_id = ? AND user_id = ?", badge.ID, u.ID).Exist(new(UserBadge))
|
||||
if existErr == nil && exists {
|
||||
return util.NewAlreadyExistErrorf("user badge already exists [user_id: %d, badge_id: %d]", u.ID, badge.ID)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -102,16 +180,33 @@ 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 {
|
||||
if len(badges) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
badgeSlugs := make([]string, 0, len(badges))
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
badgeSlugs = append(badgeSlugs, badge.Slug)
|
||||
}
|
||||
var userBadges []UserBadge
|
||||
if err := db.GetEngine(ctx).Table("user_badge").
|
||||
Join("INNER", "badge", "badge.id = `user_badge`.badge_id").
|
||||
Where("`user_badge`.user_id = ?", u.ID).In("`badge`.slug", badgeSlugs).
|
||||
Find(&userBadges); err != nil {
|
||||
return err
|
||||
}
|
||||
userBadgeIDs := make([]int64, 0, len(userBadges))
|
||||
for _, ub := range userBadges {
|
||||
userBadgeIDs = append(userBadgeIDs, ub.ID)
|
||||
}
|
||||
if len(userBadgeIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
if _, err := db.GetEngine(ctx).Table("user_badge").In("id", userBadgeIDs).Delete(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@@ -122,3 +217,57 @@ 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 finding badges
|
||||
type SearchBadgeOptions struct {
|
||||
db.ListOptions
|
||||
|
||||
Keyword string
|
||||
Slug string
|
||||
ID int64
|
||||
OrderBy db.SearchOrderBy
|
||||
}
|
||||
|
||||
func (opts *SearchBadgeOptions) ToConds() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
|
||||
if opts.Keyword != "" {
|
||||
keywordCond := builder.Or(
|
||||
db.BuildCaseInsensitiveLike("badge.slug", opts.Keyword),
|
||||
db.BuildCaseInsensitiveLike("badge.description", opts.Keyword),
|
||||
)
|
||||
cond = cond.And(keywordCond)
|
||||
}
|
||||
|
||||
if opts.ID > 0 {
|
||||
cond = cond.And(builder.Eq{"badge.id": opts.ID})
|
||||
}
|
||||
|
||||
if len(opts.Slug) > 0 {
|
||||
cond = cond.And(builder.Eq{"badge.slug": opts.Slug})
|
||||
}
|
||||
|
||||
return cond
|
||||
}
|
||||
|
||||
func (opts *SearchBadgeOptions) ToOrders() string {
|
||||
return opts.OrderBy.String()
|
||||
}
|
||||
|
||||
// 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
|
||||
func GetBadgeByID(ctx context.Context, id int64) (*Badge, error) {
|
||||
badge := new(Badge)
|
||||
has, err := db.GetEngine(ctx).ID(id).Get(badge)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, util.NewNotExistErrorf("badge does not exist [id: %d]", id)
|
||||
}
|
||||
return badge, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
// Copyright 2026 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"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBadge(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
t.Run("GetBadgeNotExist", testGetBadgeNotExist)
|
||||
t.Run("CreateBadgeAlreadyExists", testCreateBadgeAlreadyExists)
|
||||
t.Run("GetBadgeUsers", testGetBadgeUsers)
|
||||
t.Run("AddAndRemoveUserBadges", testAddAndRemoveUserBadges)
|
||||
t.Run("SearchBadgesOrderingAndKeyword", testSearchBadgesOrderingAndKeyword)
|
||||
}
|
||||
|
||||
func testGetBadgeNotExist(t *testing.T) {
|
||||
badge, err := user_model.GetBadge(t.Context(), "does-not-exist")
|
||||
assert.Nil(t, badge)
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, util.ErrNotExist)
|
||||
}
|
||||
|
||||
func testCreateBadgeAlreadyExists(t *testing.T) {
|
||||
badge := &user_model.Badge{
|
||||
Slug: "duplicate-badge-slug",
|
||||
Description: "First",
|
||||
}
|
||||
assert.NoError(t, user_model.CreateBadge(t.Context(), badge))
|
||||
|
||||
err := user_model.CreateBadge(t.Context(), &user_model.Badge{
|
||||
Slug: "duplicate-badge-slug",
|
||||
Description: "Second",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, util.ErrAlreadyExist)
|
||||
}
|
||||
|
||||
func testGetBadgeUsers(t *testing.T) {
|
||||
// Create a test badge
|
||||
badge := &user_model.Badge{
|
||||
Slug: "test-badge-users",
|
||||
Description: "Test Badge",
|
||||
ImageURL: "test.png",
|
||||
}
|
||||
assert.NoError(t, user_model.CreateBadge(t.Context(), 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(t.Context(), user1, badge))
|
||||
assert.NoError(t, user_model.AddUserBadge(t.Context(), user2, badge))
|
||||
defer func() {
|
||||
assert.NoError(t, user_model.RemoveUserBadge(t.Context(), user1, badge))
|
||||
assert.NoError(t, user_model.RemoveUserBadge(t.Context(), user2, badge))
|
||||
}()
|
||||
|
||||
// Test getting users with pagination
|
||||
opts := &user_model.GetBadgeUsersOptions{
|
||||
BadgeSlug: badge.Slug,
|
||||
ListOptions: db.ListOptions{
|
||||
Page: 1,
|
||||
PageSize: 1,
|
||||
},
|
||||
}
|
||||
|
||||
users, count, err := user_model.GetBadgeUsers(t.Context(), 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(t.Context(), opts)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 2, count)
|
||||
assert.Len(t, users, 1)
|
||||
|
||||
// Test with non-existent badge
|
||||
opts.BadgeSlug = "non-existent"
|
||||
users, count, err = user_model.GetBadgeUsers(t.Context(), opts)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 0, count)
|
||||
assert.Empty(t, users)
|
||||
}
|
||||
|
||||
func testAddAndRemoveUserBadges(t *testing.T) {
|
||||
badge1 := unittest.AssertExistsAndLoadBean(t, &user_model.Badge{ID: 1})
|
||||
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
|
||||
// Add a badge to user and verify that it is returned in the list
|
||||
assert.NoError(t, user_model.AddUserBadge(t.Context(), user1, badge1))
|
||||
badges, count, err := user_model.GetUserBadges(t.Context(), user1)
|
||||
assert.Equal(t, int64(1), count)
|
||||
assert.Equal(t, badge1.Slug, badges[0].Slug)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Confirm that it is impossible to duplicate the same badge
|
||||
err = user_model.AddUserBadge(t.Context(), user1, badge1)
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, util.ErrAlreadyExist)
|
||||
|
||||
// Nothing happened to the existing badge
|
||||
badges, count, err = user_model.GetUserBadges(t.Context(), user1)
|
||||
assert.Equal(t, int64(1), count)
|
||||
assert.Equal(t, badge1.Slug, badges[0].Slug)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Remove a badge from user and verify that it is no longer in the list
|
||||
assert.NoError(t, user_model.RemoveUserBadge(t.Context(), user1, badge1))
|
||||
_, count, err = user_model.GetUserBadges(t.Context(), user1)
|
||||
assert.Equal(t, int64(0), count)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Removing empty or missing badge selections should be a no-op.
|
||||
assert.NoError(t, user_model.RemoveUserBadges(t.Context(), user1, nil))
|
||||
assert.NoError(t, user_model.RemoveUserBadges(t.Context(), user1, []*user_model.Badge{{Slug: "does-not-exist"}}))
|
||||
}
|
||||
|
||||
func testSearchBadgesOrderingAndKeyword(t *testing.T) {
|
||||
createdBadges := []*user_model.Badge{
|
||||
{Slug: "badge-sort-b", Description: "Badge Sort B"},
|
||||
{Slug: "badge-sort-c", Description: "Badge Sort C"},
|
||||
{Slug: "badge-sort-a", Description: "Badge Sort A"},
|
||||
{Slug: "badge-sort-case", Description: "MiXeDCaSeKeyword"},
|
||||
}
|
||||
for _, badge := range createdBadges {
|
||||
assert.NoError(t, user_model.CreateBadge(t.Context(), badge))
|
||||
}
|
||||
|
||||
opts := &user_model.SearchBadgeOptions{
|
||||
ListOptions: db.ListOptions{ListAll: true},
|
||||
Keyword: "badge-sort-",
|
||||
OrderBy: db.SearchOrderBy("`badge`.id ASC"),
|
||||
}
|
||||
|
||||
oldestFirst, count, err := user_model.SearchBadges(t.Context(), opts)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 4, count)
|
||||
assert.Equal(t, []string{"badge-sort-b", "badge-sort-c", "badge-sort-a", "badge-sort-case"}, collectBadgeSlugs(oldestFirst))
|
||||
|
||||
opts.OrderBy = db.SearchOrderBy("`badge`.id DESC")
|
||||
newestFirst, count, err := user_model.SearchBadges(t.Context(), opts)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 4, count)
|
||||
assert.Equal(t, []string{"badge-sort-case", "badge-sort-a", "badge-sort-c", "badge-sort-b"}, collectBadgeSlugs(newestFirst))
|
||||
|
||||
opts.OrderBy = db.SearchOrderBy("`badge`.slug ASC")
|
||||
alpha, count, err := user_model.SearchBadges(t.Context(), opts)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 4, count)
|
||||
assert.Equal(t, []string{"badge-sort-a", "badge-sort-b", "badge-sort-c", "badge-sort-case"}, collectBadgeSlugs(alpha))
|
||||
|
||||
opts.OrderBy = db.SearchOrderBy("`badge`.slug DESC")
|
||||
reverseAlpha, count, err := user_model.SearchBadges(t.Context(), opts)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 4, count)
|
||||
assert.Equal(t, []string{"badge-sort-case", "badge-sort-c", "badge-sort-b", "badge-sort-a"}, collectBadgeSlugs(reverseAlpha))
|
||||
|
||||
opts.Keyword = "mixedcasekeyword"
|
||||
opts.OrderBy = db.SearchOrderBy("`badge`.slug ASC")
|
||||
caseInsensitive, count, err := user_model.SearchBadges(t.Context(), opts)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 1, count)
|
||||
assert.Equal(t, []string{"badge-sort-case"}, collectBadgeSlugs(caseInsensitive))
|
||||
}
|
||||
|
||||
func collectBadgeSlugs(badges []*user_model.Badge) []string {
|
||||
slugs := make([]string, 0, len(badges))
|
||||
for _, badge := range badges {
|
||||
slugs = append(slugs, badge.Slug)
|
||||
}
|
||||
return slugs
|
||||
}
|
||||
Reference in New Issue
Block a user