Skip to content

Commit ebc487b

Browse files
Oleg Komarovtechknowlogick
authored andcommitted
User badge model fixes for RemoveUserBadges and AddUserBadges
1 parent 50f7ce1 commit ebc487b

File tree

5 files changed

+124
-13
lines changed

5 files changed

+124
-13
lines changed

models/fixtures/badge.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-
2+
id: 1
3+
slug: badge1
4+
description: just a test badge
5+
image_url: badge1.png

models/migrations/migrations.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"code.gitea.io/gitea/models/migrations/v1_22"
2525
"code.gitea.io/gitea/models/migrations/v1_23"
2626
"code.gitea.io/gitea/models/migrations/v1_24"
27+
"code.gitea.io/gitea/models/migrations/v1_25"
2728
"code.gitea.io/gitea/models/migrations/v1_6"
2829
"code.gitea.io/gitea/models/migrations/v1_7"
2930
"code.gitea.io/gitea/models/migrations/v1_8"
@@ -382,6 +383,9 @@ func prepareMigrationTasks() []*migration {
382383
newMigration(318, "Add anonymous_access_mode for repo_unit", v1_24.AddRepoUnitAnonymousAccessMode),
383384
newMigration(319, "Add ExclusiveOrder to Label table", v1_24.AddExclusiveOrderColumnToLabelTable),
384385
newMigration(320, "Migrate two_factor_policy to login_source table", v1_24.MigrateSkipTwoFactor),
386+
387+
// Gitea 1.24.0 ends at database version 321 (database version 322)
388+
newMigration(321, "Add unique constraint for user badge", v1_25.AddUniqueIndexForUserBadge),
385389
}
386390
return preparedMigrations
387391
}

models/migrations/v1_25/v321.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright 2025 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package v1_25 //nolint
5+
6+
import (
7+
"fmt"
8+
9+
"xorm.io/xorm"
10+
"xorm.io/xorm/schemas"
11+
)
12+
13+
type UserBadge struct { //revive:disable-line:exported
14+
ID int64 `xorm:"pk autoincr"`
15+
BadgeID int64
16+
UserID int64
17+
}
18+
19+
// TableIndices implements xorm's TableIndices interface
20+
func (n *UserBadge) TableIndices() []*schemas.Index {
21+
indices := make([]*schemas.Index, 0, 1)
22+
ubUnique := schemas.NewIndex("unique_user_badge", schemas.UniqueType)
23+
ubUnique.AddColumn("user_id", "badge_id")
24+
indices = append(indices, ubUnique)
25+
return indices
26+
}
27+
28+
// AddUniqueIndexForUserBadge adds a compound unique indexes for user badge table
29+
// and it replaces an old index on user_id
30+
func AddUniqueIndexForUserBadge(x *xorm.Engine) error {
31+
// remove possible duplicated records in table user_badge
32+
type result struct {
33+
UserID int64
34+
BadgeID int64
35+
Cnt int
36+
}
37+
var results []result
38+
if err := x.Select("user_id, badge_id, count(*) as cnt").
39+
Table("user_badge").
40+
GroupBy("user_id, badge_id").
41+
Having("count(*) > 1").
42+
Find(&results); err != nil {
43+
return err
44+
}
45+
for _, r := range results {
46+
if x.Dialect().URI().DBType == schemas.MSSQL {
47+
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 {
48+
return err
49+
}
50+
} else {
51+
var ids []int64
52+
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 {
53+
return err
54+
}
55+
if _, err := x.Table("user_badge").In("id", ids).Delete(); err != nil {
56+
return err
57+
}
58+
}
59+
}
60+
61+
return x.Sync(new(UserBadge))
62+
}

models/user/badge.go

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"code.gitea.io/gitea/modules/util"
1313

1414
"xorm.io/builder"
15+
"xorm.io/xorm/schemas"
1516
)
1617

1718
// Badge represents a user badge
@@ -29,6 +30,15 @@ type UserBadge struct { //nolint:revive // export stutter
2930
UserID int64 `xorm:"INDEX"`
3031
}
3132

33+
// TableIndices implements xorm's TableIndices interface
34+
func (n *UserBadge) TableIndices() []*schemas.Index {
35+
indices := make([]*schemas.Index, 0, 1)
36+
ubUnique := schemas.NewIndex("unique_user_badge", schemas.UniqueType)
37+
ubUnique.AddColumn("user_id", "badge_id")
38+
indices = append(indices, ubUnique)
39+
return indices
40+
}
41+
3242
// ErrBadgeAlreadyExist represents a "badge already exists" error.
3343
type ErrBadgeAlreadyExist struct {
3444
Slug string
@@ -189,20 +199,22 @@ func RemoveUserBadge(ctx context.Context, u *User, badge *Badge) error {
189199
// RemoveUserBadges removes specific badges from a user.
190200
func RemoveUserBadges(ctx context.Context, u *User, badges []*Badge) error {
191201
return db.WithTx(ctx, func(ctx context.Context) error {
192-
slugs := make([]string, len(badges))
193-
for i, badge := range badges {
194-
slugs[i] = badge.Slug
202+
badgeSlugs := make([]string, 0, len(badges))
203+
for _, badge := range badges {
204+
badgeSlugs = append(badgeSlugs, badge.Slug)
195205
}
196-
197-
var badgeIDs []int64
198-
if err := db.GetEngine(ctx).Table("badge").In("slug", slugs).Cols("id").Find(&badgeIDs); err != nil {
206+
var userBadges []UserBadge
207+
if err := db.GetEngine(ctx).Table("user_badge").
208+
Join("INNER", "badge", "badge.id = `user_badge`.badge_id").
209+
Where("`user_badge`.user_id = ?", u.ID).In("`badge`.slug", badgeSlugs).
210+
Find(&userBadges); err != nil {
199211
return err
200212
}
201-
202-
if _, err := db.GetEngine(ctx).
203-
Where("user_id = ?", u.ID).
204-
In("badge_id", badgeIDs).
205-
Delete(&UserBadge{}); err != nil {
213+
userBadgeIDs := make([]int64, 0, len(userBadges))
214+
for _, ub := range userBadges {
215+
userBadgeIDs = append(userBadgeIDs, ub.ID)
216+
}
217+
if _, err := db.GetEngine(ctx).Table("user_badge").In("id", userBadgeIDs).Delete(); err != nil {
206218
return err
207219
}
208220
return nil

models/user/badge_test.go

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func TestGetBadgeUsers(t *testing.T) {
3333

3434
// Test getting users with pagination
3535
opts := &user_model.GetBadgeUsersOptions{
36-
Badge: badge,
36+
BadgeSlug: badge.Slug,
3737
ListOptions: db.ListOptions{
3838
Page: 1,
3939
PageSize: 1,
@@ -53,9 +53,37 @@ func TestGetBadgeUsers(t *testing.T) {
5353
assert.Len(t, users, 1)
5454

5555
// Test with non-existent badge
56-
opts.Badge = &user_model.Badge{Slug: "non-existent"}
56+
opts.BadgeSlug = "non-existent"
5757
users, count, err = user_model.GetBadgeUsers(db.DefaultContext, opts)
5858
assert.NoError(t, err)
5959
assert.EqualValues(t, 0, count)
6060
assert.Empty(t, users)
6161
}
62+
63+
func TestAddAndRemoveUserBadges(t *testing.T) {
64+
assert.NoError(t, unittest.PrepareTestDatabase())
65+
badge1 := unittest.AssertExistsAndLoadBean(t, &user_model.Badge{ID: 1})
66+
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
67+
68+
// Add a badge to user and verify that it is returned in the list
69+
assert.NoError(t, user_model.AddUserBadge(db.DefaultContext, user1, badge1))
70+
badges, count, err := user_model.GetUserBadges(db.DefaultContext, user1)
71+
assert.Equal(t, int64(1), count)
72+
assert.Equal(t, badge1.Slug, badges[0].Slug)
73+
assert.NoError(t, err)
74+
75+
// Confirm that it is impossible to duplicate the same badge
76+
assert.Error(t, user_model.AddUserBadge(db.DefaultContext, user1, badge1))
77+
78+
// Nothing happened to the existing badge
79+
badges, count, err = user_model.GetUserBadges(db.DefaultContext, user1)
80+
assert.Equal(t, int64(1), count)
81+
assert.Equal(t, badge1.Slug, badges[0].Slug)
82+
assert.NoError(t, err)
83+
84+
// Remove a badge from user and verify that it is no longer in the list
85+
assert.NoError(t, user_model.RemoveUserBadge(db.DefaultContext, user1, badge1))
86+
_, count, err = user_model.GetUserBadges(db.DefaultContext, user1)
87+
assert.Equal(t, int64(0), count)
88+
assert.NoError(t, err)
89+
}

0 commit comments

Comments
 (0)