0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-03-29 22:50:12 +02:00
gitea/models/user/badge.go
Nicolas 4ba90207cf
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)
2026-03-22 15:49:45 +00:00

274 lines
7.6 KiB
Go

// Copyright 2022 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user
import (
"context"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder"
"xorm.io/xorm/schemas"
)
// Badge represents a user badge
type Badge struct {
ID int64 `xorm:"pk autoincr"`
Slug string `xorm:"UNIQUE"`
Description string
ImageURL string
}
// UserBadge represents a user badge
type UserBadge struct { //nolint:revive // export stutter
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
}
func init() {
db.RegisterModel(new(Badge))
db.RegisterModel(new(UserBadge))
}
// GetUserBadges returns the user's badges.
func GetUserBadges(ctx context.Context, u *User) ([]*Badge, int64, error) {
sess := db.GetEngine(ctx).
Select("`badge`.*").
Join("INNER", "user_badge", "`user_badge`.badge_id=badge.id").
Where("user_badge.user_id=?", u.ID)
badges := make([]*Badge, 0, 8)
count, err := sess.FindAndCount(&badges)
return badges, count, err
}
// GetBadgeUsersOptions contains options for getting users with a specific badge
type GetBadgeUsersOptions struct {
db.ListOptions
BadgeSlug string
}
// 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 err != nil {
return nil, 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).Cols("description", "image_url").Update(badge)
return err
}
// DeleteBadge deletes a badge and all associated user_badge entries.
func DeleteBadge(ctx context.Context, badge *Badge) error {
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.
func AddUserBadge(ctx context.Context, u *User, badge *Badge) error {
return AddUserBadges(ctx, u, []*Badge{badge})
}
// AddUserBadges adds badges to a user.
func AddUserBadges(ctx context.Context, u *User, badges []*Badge) error {
return db.WithTx(ctx, func(ctx context.Context) error {
for _, badge := range badges {
// hydrate badge and check if it exists
has, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Get(badge)
if err != nil {
return err
} else if !has {
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
}
}
return nil
})
}
// RemoveUserBadge removes a badge from a user.
func RemoveUserBadge(ctx context.Context, u *User, badge *Badge) error {
return RemoveUserBadges(ctx, u, []*Badge{badge})
}
// 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 {
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
})
}
// RemoveAllUserBadges removes all badges from a user.
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
}