Skip to content

Commit 7e734b6

Browse files
HenriquerPimentelDiogo Vicente
and
Diogo Vicente
committed
Implemented Badge Management in administration panel (#29798)
Co-authored-by: Diogo Vicente <diogo.m.s.vicente@tecnico.ulisboa.pt>
1 parent c0880e7 commit 7e734b6

File tree

16 files changed

+903
-3
lines changed

16 files changed

+903
-3
lines changed

models/user/badge.go

Lines changed: 159 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,14 @@ package user
66
import (
77
"context"
88
"fmt"
9+
"net/url"
10+
"strings"
911

1012
"code.gitea.io/gitea/models/db"
13+
"code.gitea.io/gitea/modules/setting"
14+
15+
"xorm.io/builder"
16+
"xorm.io/xorm"
1117
)
1218

1319
// Badge represents a user badge
@@ -30,6 +36,10 @@ func init() {
3036
db.RegisterModel(new(UserBadge))
3137
}
3238

39+
func AdminCreateBadge(ctx context.Context, badge *Badge) error {
40+
return CreateBadge(ctx, badge)
41+
}
42+
3343
// GetUserBadges returns the user's badges.
3444
func GetUserBadges(ctx context.Context, u *User) ([]*Badge, int64, error) {
3545
sess := db.GetEngine(ctx).
@@ -42,9 +52,30 @@ func GetUserBadges(ctx context.Context, u *User) ([]*Badge, int64, error) {
4252
return badges, count, err
4353
}
4454

55+
// GetBadgeUsers returns the badges users.
56+
func GetBadgeUsers(ctx context.Context, b *Badge) ([]*User, int64, error) {
57+
sess := db.GetEngine(ctx).
58+
Select("`user`.*").
59+
Join("INNER", "user_badge", "`user_badge`.user_id=user.id").
60+
Where("user_badge.badge_id=?", b.ID)
61+
62+
users := make([]*User, 0, 8)
63+
count, err := sess.FindAndCount(&users)
64+
return users, count, err
65+
}
66+
4567
// CreateBadge creates a new badge.
4668
func CreateBadge(ctx context.Context, badge *Badge) error {
47-
_, err := db.GetEngine(ctx).Insert(badge)
69+
isExist, err := IsBadgeExist(ctx, 0, badge.Slug)
70+
71+
if err != nil {
72+
return err
73+
} else if isExist {
74+
return ErrBadgeAlreadyExist{badge.Slug}
75+
}
76+
77+
_, err = db.GetEngine(ctx).Insert(badge)
78+
4879
return err
4980
}
5081

@@ -58,9 +89,22 @@ func GetBadge(ctx context.Context, slug string) (*Badge, error) {
5889
return badge, err
5990
}
6091

92+
// GetBadgeByID returns a badge
93+
func GetBadgeByID(ctx context.Context, id int64) (*Badge, error) {
94+
badge := new(Badge)
95+
has, err := db.GetEngine(ctx).Where("id=?", id).Get(badge)
96+
if err != nil {
97+
return nil, err
98+
} else if !has {
99+
return nil, ErrBadgeNotExist{ID: id}
100+
}
101+
102+
return badge, err
103+
}
104+
61105
// UpdateBadge updates a badge based on its slug.
62106
func UpdateBadge(ctx context.Context, badge *Badge) error {
63-
_, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Update(badge)
107+
_, err := db.GetEngine(ctx).Where("id=?", badge.ID).Cols("slug", "description", "image_url").Update(badge)
64108
return err
65109
}
66110

@@ -70,6 +114,15 @@ func DeleteBadge(ctx context.Context, badge *Badge) error {
70114
return err
71115
}
72116

117+
// DeleteUserBadgeRecord deletes a user badge record.
118+
func DeleteUserBadgeRecord(ctx context.Context, badge *Badge) error {
119+
userBadge := &UserBadge{
120+
BadgeID: badge.ID,
121+
}
122+
_, err := db.GetEngine(ctx).Where("badge_id=?", userBadge.BadgeID).Delete(userBadge)
123+
return err
124+
}
125+
73126
// AddUserBadge adds a badge to a user.
74127
func AddUserBadge(ctx context.Context, u *User, badge *Badge) error {
75128
return AddUserBadges(ctx, u, []*Badge{badge})
@@ -122,3 +175,107 @@ func RemoveAllUserBadges(ctx context.Context, u *User) error {
122175
_, err := db.GetEngine(ctx).Where("user_id=?", u.ID).Delete(&UserBadge{})
123176
return err
124177
}
178+
179+
// HTMLURL returns the badges full link.
180+
func (u *Badge) HTMLURL() string {
181+
return setting.AppURL + url.PathEscape(u.Slug)
182+
}
183+
184+
// IsBadgeExist checks if given badge slug exist,
185+
// it is used when creating/updating a badge slug
186+
func IsBadgeExist(ctx context.Context, uid int64, slug string) (bool, error) {
187+
if len(slug) == 0 {
188+
return false, nil
189+
}
190+
return db.GetEngine(ctx).
191+
Where("slug!=?", uid).
192+
Get(&Badge{Slug: strings.ToLower(slug)})
193+
}
194+
195+
// SearchBadgeOptions represents the options when fdin badges
196+
type SearchBadgeOptions struct {
197+
db.ListOptions
198+
199+
Keyword string
200+
Slug string
201+
ID int64
202+
OrderBy db.SearchOrderBy
203+
Actor *User // The user doing the search
204+
205+
ExtraParamStrings map[string]string
206+
}
207+
208+
func (opts *SearchBadgeOptions) ToConds() builder.Cond {
209+
cond := builder.NewCond()
210+
211+
if opts.Keyword != "" {
212+
cond = cond.And(builder.Like{"badge.slug", opts.Keyword})
213+
}
214+
215+
return cond
216+
}
217+
218+
func (opts *SearchBadgeOptions) ToOrders() string {
219+
orderBy := "badge.slug"
220+
return orderBy
221+
}
222+
223+
func (opts *SearchBadgeOptions) ToJoins() []db.JoinFunc {
224+
return []db.JoinFunc{
225+
func(e db.Engine) error {
226+
e.Join("INNER", "badge", "badge.badge_id = badge.id")
227+
return nil
228+
},
229+
}
230+
}
231+
232+
func SearchBadges(ctx context.Context, opts *SearchBadgeOptions) (badges []*Badge, _ int64, _ error) {
233+
sessCount := opts.toSearchQueryBase(ctx)
234+
defer sessCount.Close()
235+
count, err := sessCount.Count(new(Badge))
236+
if err != nil {
237+
return nil, 0, fmt.Errorf("count: %w", err)
238+
}
239+
240+
if len(opts.OrderBy) == 0 {
241+
opts.OrderBy = db.SearchOrderByID
242+
}
243+
244+
sessQuery := opts.toSearchQueryBase(ctx).OrderBy(opts.OrderBy.String())
245+
defer sessQuery.Close()
246+
if opts.Page != 0 {
247+
sessQuery = db.SetSessionPagination(sessQuery, opts)
248+
}
249+
250+
// the sql may contain JOIN, so we must only select Badge related columns
251+
sessQuery = sessQuery.Select("`badge`.*")
252+
badges = make([]*Badge, 0, opts.PageSize)
253+
return badges, count, sessQuery.Find(&badges)
254+
}
255+
256+
func (opts *SearchBadgeOptions) toSearchQueryBase(ctx context.Context) *xorm.Session {
257+
var cond builder.Cond
258+
cond = builder.Neq{"id": -1}
259+
260+
if len(opts.Keyword) > 0 {
261+
lowerKeyword := strings.ToLower(opts.Keyword)
262+
keywordCond := builder.Or(
263+
builder.Like{"slug", lowerKeyword},
264+
builder.Like{"description", lowerKeyword},
265+
builder.Like{"id", lowerKeyword},
266+
)
267+
cond = cond.And(keywordCond)
268+
}
269+
270+
if opts.ID > 0 {
271+
cond = cond.And(builder.Eq{"id": opts.ID})
272+
}
273+
274+
if len(opts.Slug) > 0 {
275+
cond = cond.And(builder.Eq{"slug": opts.Slug})
276+
}
277+
278+
e := db.GetEngine(ctx)
279+
280+
return e.Where(cond)
281+
}

models/user/error.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,44 @@ func IsErrUserIsNotLocal(err error) bool {
107107
_, ok := err.(ErrUserIsNotLocal)
108108
return ok
109109
}
110+
111+
// ErrBadgeAlreadyExist represents a "badge already exists" error.
112+
type ErrBadgeAlreadyExist struct {
113+
Slug string
114+
}
115+
116+
// IsErrBadgeAlreadyExist checks if an error is a ErrBadgeAlreadyExist.
117+
func IsErrBadgeAlreadyExist(err error) bool {
118+
_, ok := err.(ErrBadgeAlreadyExist)
119+
return ok
120+
}
121+
122+
func (err ErrBadgeAlreadyExist) Error() string {
123+
return fmt.Sprintf("badge already exists [slug: %s]", err.Slug)
124+
}
125+
126+
// Unwrap unwraps this error as a ErrExist error
127+
func (err ErrBadgeAlreadyExist) Unwrap() error {
128+
return util.ErrAlreadyExist
129+
}
130+
131+
// ErrBadgeNotExist represents a "BadgeNotExist" kind of error.
132+
type ErrBadgeNotExist struct {
133+
Slug string
134+
ID int64
135+
}
136+
137+
// IsErrBadgeNotExist checks if an error is a ErrBadgeNotExist.
138+
func IsErrBadgeNotExist(err error) bool {
139+
_, ok := err.(ErrBadgeNotExist)
140+
return ok
141+
}
142+
143+
func (err ErrBadgeNotExist) Error() string {
144+
return fmt.Sprintf("badge does not exist [slug: %s | id: %i]", err.Slug, err.ID)
145+
}
146+
147+
// Unwrap unwraps this error as a ErrNotExist error
148+
func (err ErrBadgeNotExist) Unwrap() error {
149+
return util.ErrNotExist
150+
}

modules/validation/binding.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ const (
2626
ErrUsername = "UsernameError"
2727
// ErrInvalidGroupTeamMap is returned when a group team mapping is invalid
2828
ErrInvalidGroupTeamMap = "InvalidGroupTeamMap"
29+
ErrInvalidImageURL = "InvalidImageURL"
30+
ErrInvalidSlug = "InvalidSlug"
2931
)
3032

3133
// AddBindingRules adds additional binding rules
@@ -38,6 +40,8 @@ func AddBindingRules() {
3840
addGlobOrRegexPatternRule()
3941
addUsernamePatternRule()
4042
addValidGroupTeamMapRule()
43+
addValidImageURLBindingRule()
44+
addSlugPatternRule()
4145
}
4246

4347
func addGitRefNameBindingRule() {
@@ -94,6 +98,40 @@ func addValidSiteURLBindingRule() {
9498
})
9599
}
96100

101+
func addValidImageURLBindingRule() {
102+
// URL validation rule
103+
binding.AddRule(&binding.Rule{
104+
IsMatch: func(rule string) bool {
105+
return strings.HasPrefix(rule, "ValidImageUrl")
106+
},
107+
IsValid: func(errs binding.Errors, name string, val any) (bool, binding.Errors) {
108+
str := fmt.Sprintf("%v", val)
109+
if len(str) != 0 && !IsValidImageURL(str) {
110+
errs.Add([]string{name}, ErrInvalidImageURL, "ImageURL")
111+
return false, errs
112+
}
113+
114+
return true, errs
115+
},
116+
})
117+
}
118+
119+
func addSlugPatternRule() {
120+
binding.AddRule(&binding.Rule{
121+
IsMatch: func(rule string) bool {
122+
return rule == "Slug"
123+
},
124+
IsValid: func(errs binding.Errors, name string, val any) (bool, binding.Errors) {
125+
str := fmt.Sprintf("%v", val)
126+
if !IsValidSlug(str) {
127+
errs.Add([]string{name}, ErrInvalidSlug, "invalid slug")
128+
return false, errs
129+
}
130+
return true, errs
131+
},
132+
})
133+
}
134+
97135
func addGlobPatternRule() {
98136
binding.AddRule(&binding.Rule{
99137
IsMatch: func(rule string) bool {

modules/validation/helpers.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package validation
66
import (
77
"net"
88
"net/url"
9+
"path/filepath"
910
"regexp"
1011
"strings"
1112

@@ -50,6 +51,29 @@ func IsValidSiteURL(uri string) bool {
5051
return false
5152
}
5253

54+
// IsValidImageURL checks if URL is valid and points to an image
55+
func IsValidImageURL(uri string) bool {
56+
u, err := url.ParseRequestURI(uri)
57+
if err != nil {
58+
return false
59+
}
60+
61+
if !validPort(portOnly(u.Host)) {
62+
return false
63+
}
64+
65+
for _, scheme := range setting.Service.ValidSiteURLSchemes {
66+
if scheme == u.Scheme {
67+
// Check if the path has an image file extension
68+
ext := strings.ToLower(filepath.Ext(u.Path))
69+
if ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".gif" || ext == ".bmp" || ext == ".svg" || ext == ".webp" {
70+
return true
71+
}
72+
}
73+
}
74+
return false
75+
}
76+
5377
// IsEmailDomainListed checks whether the domain of an email address
5478
// matches a list of domains
5579
func IsEmailDomainListed(globs []glob.Glob, email string) bool {
@@ -127,3 +151,7 @@ func IsValidUsername(name string) bool {
127151
// but it's easier to use positive and negative checks.
128152
return validUsernamePattern.MatchString(name) && !invalidUsernamePattern.MatchString(name)
129153
}
154+
155+
func IsValidSlug(slug string) bool {
156+
return IsValidUsername(slug)
157+
}

modules/web/middleware/binding.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,10 @@ func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Lo
138138
data["ErrorMsg"] = trName + l.TrString("form.username_error")
139139
case validation.ErrInvalidGroupTeamMap:
140140
data["ErrorMsg"] = trName + l.TrString("form.invalid_group_team_map_error", errs[0].Message)
141+
case validation.ErrInvalidImageURL:
142+
data["ErrorMsg"] = l.TrString("form.invalid_image_url_error")
143+
case validation.ErrInvalidSlug:
144+
data["ErrorMsg"] = l.TrString("form.invalid_slug_error")
141145
default:
142146
msg := errs[0].Classification
143147
if msg != "" && errs[0].Message != "" {

0 commit comments

Comments
 (0)