Skip to content

Manage User Badges in the UI #31262

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

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8b86c31
Implemented Badge Management in administration panel (#29798)
HenriquerPimentel May 26, 2024
f68e44d
Implemented User Badge Management Interface (#29798)
HenriquerPimentel May 27, 2024
d681313
Fix linting recommendations
HenriquerPimentel Jun 17, 2024
10463ec
Merge branch 'main' into Badge
techknowlogick Sep 7, 2024
52bc49d
fix lints
techknowlogick Sep 8, 2024
401d257
Merge branch 'main' into Badge
techknowlogick Oct 8, 2024
d55e25f
update join
techknowlogick Oct 8, 2024
d9efbf3
close session immediately
techknowlogick Oct 8, 2024
ae9c0a6
Merge branch 'main' into Badge
techknowlogick Oct 13, 2024
9234ef5
Merge branch 'main' into Badge
techknowlogick Jan 15, 2025
f601501
Update per feedback
techknowlogick Jan 15, 2025
789b73b
update per feedback
techknowlogick Jan 16, 2025
f5c6a31
fix lint
techknowlogick Jan 16, 2025
4890a15
Merge branch 'main' into Badge
techknowlogick Jan 22, 2025
52957f4
Merge branch 'main' into Badge
techknowlogick Apr 17, 2025
6e7fc93
Merge remote-tracking branch 'upstream/main' into Badge
techknowlogick Jul 21, 2025
50f7ce1
update per feedback
techknowlogick Jul 21, 2025
ebc487b
User badge model fixes for RemoveUserBadges and AddUserBadges
Jul 21, 2025
2a69304
use the link-action approach for deletion
techknowlogick Jul 21, 2025
3d01354
Merge remote-tracking branch 'upstream/main' into Badge
techknowlogick Aug 6, 2025
ab9e0bd
fix index & add tests
techknowlogick Aug 6, 2025
07819aa
Merge branch 'main' into Badge
techknowlogick Aug 8, 2025
2175864
don't use id with "like"
techknowlogick Aug 8, 2025
4f8f5a2
Merge branch 'main' into Badge
techknowlogick Aug 8, 2025
5eb89f4
use db.withtx as per feedback
techknowlogick Aug 8, 2025
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
5 changes: 5 additions & 0 deletions models/fixtures/badge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-
id: 1
slug: badge1
description: just a test badge
image_url: badge1.png
3 changes: 2 additions & 1 deletion models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -384,8 +384,9 @@ func prepareMigrationTasks() []*migration {
newMigration(319, "Add ExclusiveOrder to Label table", v1_24.AddExclusiveOrderColumnToLabelTable),
newMigration(320, "Migrate two_factor_policy to login_source table", v1_24.MigrateSkipTwoFactor),

// Gitea 1.24.0 ends at database version 321
// Gitea 1.24.0 ends at database version 321 (database version 322)
newMigration(321, "Use LONGTEXT for some columns and fix review_state.updated_files column", v1_25.UseLongTextInSomeColumnsAndFixBugs),
newMigration(322, "Add unique constraint for user badge", v1_25.AddUniqueIndexForUserBadge),
}
return preparedMigrations
}
Expand Down
62 changes: 62 additions & 0 deletions models/migrations/v1_25/v322.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package v1_25

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))
}
85 changes: 85 additions & 0 deletions models/migrations/v1_25/v322_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package v1_25

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)
}
Loading