Skip to content

Commit 4ba9020

Browse files
authored
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 ![(screenshot)](https://github.com/user-attachments/assets/9dbf243e-c704-49f8-915a-73704e226da9) - Create new badges ![(screenshot)](https://github.com/user-attachments/assets/8a3fff7e-fe6f-49b0-a7c5-bbba34478019) - View badge ![(screenshot)](https://github.com/user-attachments/assets/dd7a882b-6e2c-47d2-93e0-05a2698a41e5) ![(screenshot)](https://private-user-images.githubusercontent.com/75789103/558982759-53536300-e189-406b-8b0e-824e1a768b92.png?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NzQxOTMyMjUsIm5iZiI6MTc3NDE5MjkyNSwicGF0aCI6Ii83NTc4OTEwMy81NTg5ODI3NTktNTM1MzYzMDAtZTE4OS00MDZiLThiMGUtODI0ZTFhNzY4YjkyLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNjAzMjIlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjYwMzIyVDE1MjIwNVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTUxNjQ5ZDUyMGVlNWRmODg1OGUyN2NiOWI3YTAxODhiMjRhM2U1OGQ1NWMwNjQ0MTBmNTRjNTBjYjIzN2ExMWEmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.4aAfpFaziiXDG7W2HaNJop0B62-NR4f0Ni9YNjTZq0M) - Edit badge ![(screenshot)](https://github.com/user-attachments/assets/7124671a-ed97-4c98-ac7d-34863377fa62) - Add user to badge ![(screenshot)](https://github.com/user-attachments/assets/3438b492-0197-4acb-b9f2-2f9f7c80582e)
1 parent aa9aea2 commit 4ba9020

22 files changed

Lines changed: 1193 additions & 33 deletions

File tree

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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ func prepareMigrationTasks() []*migration {
403403
newMigration(326, "Migrate commit status target URL to use run ID and job ID", v1_26.FixCommitStatusTargetURLToUseRunAndJobID),
404404
newMigration(327, "Add disabled state to action runners", v1_26.AddDisabledToActionRunner),
405405
newMigration(328, "Add TokenPermissions column to ActionRunJob", v1_26.AddTokenPermissionsToActionRunJob),
406+
newMigration(329, "Add unique constraint for user badge", v1_26.AddUniqueIndexForUserBadge),
406407
}
407408
return preparedMigrations
408409
}

models/migrations/v1_26/v329.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright 2026 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package v1_26
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+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright 2026 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package v1_26
5+
6+
import (
7+
"testing"
8+
9+
"code.gitea.io/gitea/models/migrations/base"
10+
11+
"github.com/stretchr/testify/assert"
12+
)
13+
14+
type UserBadgeBefore struct {
15+
ID int64 `xorm:"pk autoincr"`
16+
BadgeID int64
17+
UserID int64 `xorm:"INDEX"`
18+
}
19+
20+
func (UserBadgeBefore) TableName() string {
21+
return "user_badge"
22+
}
23+
24+
func Test_AddUniqueIndexForUserBadge(t *testing.T) {
25+
x, deferable := base.PrepareTestEnv(t, 0, new(UserBadgeBefore))
26+
defer deferable()
27+
if x == nil || t.Failed() {
28+
return
29+
}
30+
31+
testData := []*UserBadgeBefore{
32+
{UserID: 1, BadgeID: 1},
33+
{UserID: 1, BadgeID: 1}, // duplicate
34+
{UserID: 2, BadgeID: 1},
35+
{UserID: 1, BadgeID: 2},
36+
{UserID: 3, BadgeID: 3},
37+
{UserID: 3, BadgeID: 3}, // duplicate
38+
}
39+
40+
for _, data := range testData {
41+
_, err := x.Insert(data)
42+
assert.NoError(t, err)
43+
}
44+
45+
// check that we have duplicates
46+
count, err := x.Where("user_id = ? AND badge_id = ?", 1, 1).Count(&UserBadgeBefore{})
47+
assert.NoError(t, err)
48+
assert.Equal(t, int64(2), count)
49+
50+
count, err = x.Where("user_id = ? AND badge_id = ?", 3, 3).Count(&UserBadgeBefore{})
51+
assert.NoError(t, err)
52+
assert.Equal(t, int64(2), count)
53+
54+
totalCount, err := x.Count(&UserBadgeBefore{})
55+
assert.NoError(t, err)
56+
assert.Equal(t, int64(6), totalCount)
57+
58+
// run the migration
59+
if err := AddUniqueIndexForUserBadge(x); err != nil {
60+
assert.NoError(t, err)
61+
return
62+
}
63+
64+
// verify the duplicates were removed
65+
count, err = x.Where("user_id = ? AND badge_id = ?", 1, 1).Count(&UserBadgeBefore{})
66+
assert.NoError(t, err)
67+
assert.Equal(t, int64(1), count)
68+
69+
count, err = x.Where("user_id = ? AND badge_id = ?", 3, 3).Count(&UserBadgeBefore{})
70+
assert.NoError(t, err)
71+
assert.Equal(t, int64(1), count)
72+
73+
// check total count
74+
totalCount, err = x.Count(&UserBadgeBefore{})
75+
assert.NoError(t, err)
76+
assert.Equal(t, int64(4), totalCount)
77+
78+
// fail to insert a duplicate
79+
_, err = x.Insert(&UserBadge{UserID: 1, BadgeID: 1})
80+
assert.Error(t, err)
81+
82+
// succeed adding a non-duplicate
83+
_, err = x.Insert(&UserBadge{UserID: 4, BadgeID: 1})
84+
assert.NoError(t, err)
85+
}

0 commit comments

Comments
 (0)