mirror of
https://github.com/go-gitea/gitea.git
synced 2025-07-20 16:58:31 +02:00
Merge 52957f4406bb2c0f510897f65047bb2433d96d7f into 6599efb3b1400ac06d06e1c8b68ae6037fbb7952
This commit is contained in:
commit
62bf44aa4d
@ -6,8 +6,12 @@ package user
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Badge represents a user badge
|
// Badge represents a user badge
|
||||||
@ -25,6 +29,50 @@ type UserBadge struct { //nolint:revive // export stutter
|
|||||||
UserID int64 `xorm:"INDEX"`
|
UserID int64 `xorm:"INDEX"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrBadgeAlreadyExist represents a "badge already exists" error.
|
||||||
|
type ErrBadgeAlreadyExist struct {
|
||||||
|
Slug string
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrBadgeAlreadyExist checks if an error is a ErrBadgeAlreadyExist.
|
||||||
|
func IsErrBadgeAlreadyExist(err error) bool {
|
||||||
|
_, ok := err.(ErrBadgeAlreadyExist)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrBadgeAlreadyExist) Error() string {
|
||||||
|
return fmt.Sprintf("badge already exists [slug: %s]", err.Slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap unwraps this error as a ErrExist error
|
||||||
|
func (err ErrBadgeAlreadyExist) Unwrap() error {
|
||||||
|
return util.ErrAlreadyExist
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrBadgeNotExist represents a "BadgeNotExist" kind of error.
|
||||||
|
type ErrBadgeNotExist struct {
|
||||||
|
Slug string
|
||||||
|
ID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrBadgeNotExist) Error() string {
|
||||||
|
if err.ID > 0 {
|
||||||
|
return fmt.Sprintf("badge does not exist [id: %d]", err.ID)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("badge does not exist [slug: %s]", err.Slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrBadgeNotExist checks if an error is a ErrBadgeNotExist.
|
||||||
|
func IsErrBadgeNotExist(err error) bool {
|
||||||
|
_, ok := err.(ErrBadgeNotExist)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap unwraps this error as a ErrNotExist error
|
||||||
|
func (err ErrBadgeNotExist) Unwrap() error {
|
||||||
|
return util.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
db.RegisterModel(new(Badge))
|
db.RegisterModel(new(Badge))
|
||||||
db.RegisterModel(new(UserBadge))
|
db.RegisterModel(new(UserBadge))
|
||||||
@ -42,13 +90,37 @@ func GetUserBadges(ctx context.Context, u *User) ([]*Badge, int64, error) {
|
|||||||
return badges, count, err
|
return badges, count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetBadgeUsersOptions contains options for getting users with a specific badge
|
||||||
|
type GetBadgeUsersOptions struct {
|
||||||
|
db.ListOptions
|
||||||
|
Badge *Badge
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.Badge.Slug)
|
||||||
|
|
||||||
|
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.
|
// CreateBadge creates a new badge.
|
||||||
func CreateBadge(ctx context.Context, badge *Badge) error {
|
func CreateBadge(ctx context.Context, badge *Badge) error {
|
||||||
|
// this will fail if the badge already exists due to the UNIQUE constraint
|
||||||
_, err := db.GetEngine(ctx).Insert(badge)
|
_, err := db.GetEngine(ctx).Insert(badge)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBadge returns a badge
|
// GetBadge returns a specific badge
|
||||||
func GetBadge(ctx context.Context, slug string) (*Badge, error) {
|
func GetBadge(ctx context.Context, slug string) (*Badge, error) {
|
||||||
badge := new(Badge)
|
badge := new(Badge)
|
||||||
has, err := db.GetEngine(ctx).Where("slug=?", slug).Get(badge)
|
has, err := db.GetEngine(ctx).Where("slug=?", slug).Get(badge)
|
||||||
@ -60,16 +132,28 @@ func GetBadge(ctx context.Context, slug string) (*Badge, error) {
|
|||||||
|
|
||||||
// UpdateBadge updates a badge based on its slug.
|
// UpdateBadge updates a badge based on its slug.
|
||||||
func UpdateBadge(ctx context.Context, badge *Badge) error {
|
func UpdateBadge(ctx context.Context, badge *Badge) error {
|
||||||
_, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Update(badge)
|
_, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Cols("description", "image_url").Update(badge)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteBadge deletes a badge.
|
// DeleteBadge deletes a badge and all associated user_badge entries.
|
||||||
func DeleteBadge(ctx context.Context, badge *Badge) error {
|
func DeleteBadge(ctx context.Context, badge *Badge) error {
|
||||||
_, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Delete(badge)
|
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 = (SELECT id FROM badge WHERE slug = ?)", badge.Slug).
|
||||||
|
Delete(&UserBadge{}); err != nil {
|
||||||
return err
|
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.
|
// AddUserBadge adds a badge to a user.
|
||||||
func AddUserBadge(ctx context.Context, u *User, badge *Badge) error {
|
func AddUserBadge(ctx context.Context, u *User, badge *Badge) error {
|
||||||
return AddUserBadges(ctx, u, []*Badge{badge})
|
return AddUserBadges(ctx, u, []*Badge{badge})
|
||||||
@ -84,7 +168,7 @@ func AddUserBadges(ctx context.Context, u *User, badges []*Badge) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if !has {
|
} else if !has {
|
||||||
return fmt.Errorf("badge with slug %s doesn't exist", badge.Slug)
|
return ErrBadgeNotExist{Slug: badge.Slug}
|
||||||
}
|
}
|
||||||
if err := db.Insert(ctx, &UserBadge{
|
if err := db.Insert(ctx, &UserBadge{
|
||||||
BadgeID: badge.ID,
|
BadgeID: badge.ID,
|
||||||
@ -102,16 +186,24 @@ func RemoveUserBadge(ctx context.Context, u *User, badge *Badge) error {
|
|||||||
return RemoveUserBadges(ctx, u, []*Badge{badge})
|
return RemoveUserBadges(ctx, u, []*Badge{badge})
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveUserBadges removes badges from a user.
|
// RemoveUserBadges removes specific badges from a user.
|
||||||
func RemoveUserBadges(ctx context.Context, u *User, badges []*Badge) error {
|
func RemoveUserBadges(ctx context.Context, u *User, badges []*Badge) error {
|
||||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
for _, badge := range badges {
|
slugs := make([]string, len(badges))
|
||||||
if _, err := db.GetEngine(ctx).
|
for i, badge := range badges {
|
||||||
Join("INNER", "badge", "badge.id = `user_badge`.badge_id").
|
slugs[i] = badge.Slug
|
||||||
Where("`user_badge`.user_id=? AND `badge`.slug=?", u.ID, badge.Slug).
|
}
|
||||||
Delete(&UserBadge{}); err != nil {
|
|
||||||
|
var badgeIDs []int64
|
||||||
|
if err := db.GetEngine(ctx).Table("badge").In("slug", slugs).Cols("id").Find(&badgeIDs); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, err := db.GetEngine(ctx).
|
||||||
|
Where("user_id = ?", u.ID).
|
||||||
|
In("badge_id", badgeIDs).
|
||||||
|
Delete(&UserBadge{}); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@ -122,3 +214,56 @@ func RemoveAllUserBadges(ctx context.Context, u *User) error {
|
|||||||
_, err := db.GetEngine(ctx).Where("user_id=?", u.ID).Delete(&UserBadge{})
|
_, err := db.GetEngine(ctx).Where("user_id=?", u.ID).Delete(&UserBadge{})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SearchBadgeOptions represents the options when fdin badges
|
||||||
|
type SearchBadgeOptions struct {
|
||||||
|
db.ListOptions
|
||||||
|
|
||||||
|
Keyword string
|
||||||
|
Slug string
|
||||||
|
ID int64
|
||||||
|
OrderBy db.SearchOrderBy
|
||||||
|
Actor *User // The user doing the search
|
||||||
|
}
|
||||||
|
|
||||||
|
func (opts *SearchBadgeOptions) ToConds() builder.Cond {
|
||||||
|
cond := builder.NewCond()
|
||||||
|
|
||||||
|
if opts.Keyword != "" {
|
||||||
|
lowerKeyword := strings.ToLower(opts.Keyword)
|
||||||
|
keywordCond := builder.Or(
|
||||||
|
builder.Like{"badge.slug", lowerKeyword},
|
||||||
|
builder.Like{"badge.description", lowerKeyword},
|
||||||
|
builder.Like{"badge.id", lowerKeyword},
|
||||||
|
)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, ErrBadgeNotExist{ID: id}
|
||||||
|
}
|
||||||
|
return badge, nil
|
||||||
|
}
|
||||||
|
61
models/user/badge_test.go
Normal file
61
models/user/badge_test.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package user_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetBadgeUsers(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
// Create a test badge
|
||||||
|
badge := &user_model.Badge{
|
||||||
|
Slug: "test-badge",
|
||||||
|
Description: "Test Badge",
|
||||||
|
ImageURL: "test.png",
|
||||||
|
}
|
||||||
|
assert.NoError(t, user_model.CreateBadge(db.DefaultContext, badge))
|
||||||
|
|
||||||
|
// Create test users and assign badges
|
||||||
|
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||||
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
|
||||||
|
assert.NoError(t, user_model.AddUserBadge(db.DefaultContext, user1, badge))
|
||||||
|
assert.NoError(t, user_model.AddUserBadge(db.DefaultContext, user2, badge))
|
||||||
|
|
||||||
|
// Test getting users with pagination
|
||||||
|
opts := &user_model.GetBadgeUsersOptions{
|
||||||
|
Badge: badge,
|
||||||
|
ListOptions: db.ListOptions{
|
||||||
|
Page: 1,
|
||||||
|
PageSize: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
users, count, err := user_model.GetBadgeUsers(db.DefaultContext, opts)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, 2, count)
|
||||||
|
assert.Len(t, users, 1)
|
||||||
|
|
||||||
|
// Test second page
|
||||||
|
opts.Page = 2
|
||||||
|
users, count, err = user_model.GetBadgeUsers(db.DefaultContext, opts)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, 2, count)
|
||||||
|
assert.Len(t, users, 1)
|
||||||
|
|
||||||
|
// Test with non-existent badge
|
||||||
|
opts.Badge = &user_model.Badge{Slug: "non-existent"}
|
||||||
|
users, count, err = user_model.GetBadgeUsers(db.DefaultContext, opts)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, 0, count)
|
||||||
|
assert.Empty(t, users)
|
||||||
|
}
|
@ -27,6 +27,8 @@ const (
|
|||||||
ErrUsername = "UsernameError"
|
ErrUsername = "UsernameError"
|
||||||
// ErrInvalidGroupTeamMap is returned when a group team mapping is invalid
|
// ErrInvalidGroupTeamMap is returned when a group team mapping is invalid
|
||||||
ErrInvalidGroupTeamMap = "InvalidGroupTeamMap"
|
ErrInvalidGroupTeamMap = "InvalidGroupTeamMap"
|
||||||
|
// ErrInvalidSlug is returned when a slug is invalid
|
||||||
|
ErrInvalidSlug = "InvalidSlug"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AddBindingRules adds additional binding rules
|
// AddBindingRules adds additional binding rules
|
||||||
@ -40,6 +42,7 @@ func AddBindingRules() {
|
|||||||
addGlobOrRegexPatternRule()
|
addGlobOrRegexPatternRule()
|
||||||
addUsernamePatternRule()
|
addUsernamePatternRule()
|
||||||
addValidGroupTeamMapRule()
|
addValidGroupTeamMapRule()
|
||||||
|
addSlugPatternRule()
|
||||||
}
|
}
|
||||||
|
|
||||||
func addGitRefNameBindingRule() {
|
func addGitRefNameBindingRule() {
|
||||||
@ -123,6 +126,22 @@ func addValidSiteURLBindingRule() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func addSlugPatternRule() {
|
||||||
|
binding.AddRule(&binding.Rule{
|
||||||
|
IsMatch: func(rule string) bool {
|
||||||
|
return rule == "Slug"
|
||||||
|
},
|
||||||
|
IsValid: func(errs binding.Errors, name string, val any) (bool, binding.Errors) {
|
||||||
|
str := fmt.Sprintf("%v", val)
|
||||||
|
if !IsValidSlug(str) {
|
||||||
|
errs.Add([]string{name}, ErrInvalidSlug, "invalid slug")
|
||||||
|
return false, errs
|
||||||
|
}
|
||||||
|
return true, errs
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func addGlobPatternRule() {
|
func addGlobPatternRule() {
|
||||||
binding.AddRule(&binding.Rule{
|
binding.AddRule(&binding.Rule{
|
||||||
IsMatch: func(rule string) bool {
|
IsMatch: func(rule string) bool {
|
||||||
|
@ -132,3 +132,7 @@ func IsValidUsername(name string) bool {
|
|||||||
vars := globalVars()
|
vars := globalVars()
|
||||||
return vars.validUsernamePattern.MatchString(name) && !vars.invalidUsernamePattern.MatchString(name)
|
return vars.validUsernamePattern.MatchString(name) && !vars.invalidUsernamePattern.MatchString(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsValidSlug(slug string) bool {
|
||||||
|
return IsValidUsername(slug)
|
||||||
|
}
|
||||||
|
@ -138,6 +138,8 @@ func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Lo
|
|||||||
data["ErrorMsg"] = trName + l.TrString("form.username_error")
|
data["ErrorMsg"] = trName + l.TrString("form.username_error")
|
||||||
case validation.ErrInvalidGroupTeamMap:
|
case validation.ErrInvalidGroupTeamMap:
|
||||||
data["ErrorMsg"] = trName + l.TrString("form.invalid_group_team_map_error", errs[0].Message)
|
data["ErrorMsg"] = trName + l.TrString("form.invalid_group_team_map_error", errs[0].Message)
|
||||||
|
case validation.ErrInvalidSlug:
|
||||||
|
data["ErrorMsg"] = l.TrString("form.invalid_slug_error")
|
||||||
default:
|
default:
|
||||||
msg := errs[0].Classification
|
msg := errs[0].Classification
|
||||||
if msg != "" && errs[0].Message != "" {
|
if msg != "" && errs[0].Message != "" {
|
||||||
|
@ -180,6 +180,7 @@ exact = Exact
|
|||||||
exact_tooltip = Include only results that match the exact search term
|
exact_tooltip = Include only results that match the exact search term
|
||||||
repo_kind = Search repos...
|
repo_kind = Search repos...
|
||||||
user_kind = Search users...
|
user_kind = Search users...
|
||||||
|
badge_kind = Search badges...
|
||||||
org_kind = Search orgs...
|
org_kind = Search orgs...
|
||||||
team_kind = Search teams...
|
team_kind = Search teams...
|
||||||
code_kind = Search code...
|
code_kind = Search code...
|
||||||
@ -574,6 +575,7 @@ PayloadUrl = Payload URL
|
|||||||
TeamName = Team name
|
TeamName = Team name
|
||||||
AuthName = Authorization name
|
AuthName = Authorization name
|
||||||
AdminEmail = Admin email
|
AdminEmail = Admin email
|
||||||
|
ImageURL = Image URL
|
||||||
|
|
||||||
NewBranchName = New branch name
|
NewBranchName = New branch name
|
||||||
CommitSummary = Commit summary
|
CommitSummary = Commit summary
|
||||||
@ -603,12 +605,15 @@ unknown_error = Unknown error:
|
|||||||
captcha_incorrect = The CAPTCHA code is incorrect.
|
captcha_incorrect = The CAPTCHA code is incorrect.
|
||||||
password_not_match = The passwords do not match.
|
password_not_match = The passwords do not match.
|
||||||
lang_select_error = Select a language from the list.
|
lang_select_error = Select a language from the list.
|
||||||
|
invalid_image_url_error = `Please provide a valid image URL.`
|
||||||
|
invalid_slug_error = `Please provide a valid slug.`
|
||||||
|
|
||||||
username_been_taken = The username is already taken.
|
username_been_taken = The username is already taken.
|
||||||
username_change_not_local_user = Non-local users are not allowed to change their username.
|
username_change_not_local_user = Non-local users are not allowed to change their username.
|
||||||
change_username_disabled = Changing username is disabled.
|
change_username_disabled = Changing username is disabled.
|
||||||
change_full_name_disabled = Changing full name is disabled.
|
change_full_name_disabled = Changing full name is disabled.
|
||||||
username_has_not_been_changed = Username has not been changed
|
username_has_not_been_changed = Username has not been changed
|
||||||
|
slug_been_taken = The slug is already taken.
|
||||||
repo_name_been_taken = The repository name is already used.
|
repo_name_been_taken = The repository name is already used.
|
||||||
repository_force_private = Force Private is enabled: private repositories cannot be made public.
|
repository_force_private = Force Private is enabled: private repositories cannot be made public.
|
||||||
repository_files_already_exist = Files already exist for this repository. Contact the system administrator.
|
repository_files_already_exist = Files already exist for this repository. Contact the system administrator.
|
||||||
@ -2960,6 +2965,7 @@ dashboard = Dashboard
|
|||||||
self_check = Self Check
|
self_check = Self Check
|
||||||
identity_access = Identity & Access
|
identity_access = Identity & Access
|
||||||
users = User Accounts
|
users = User Accounts
|
||||||
|
badges = Badges
|
||||||
organizations = Organizations
|
organizations = Organizations
|
||||||
assets = Code Assets
|
assets = Code Assets
|
||||||
repositories = Repositories
|
repositories = Repositories
|
||||||
@ -3139,6 +3145,30 @@ emails.delete_desc = Are you sure you want to delete this email address?
|
|||||||
emails.deletion_success = The email address has been deleted.
|
emails.deletion_success = The email address has been deleted.
|
||||||
emails.delete_primary_email_error = You can not delete the primary email.
|
emails.delete_primary_email_error = You can not delete the primary email.
|
||||||
|
|
||||||
|
badges.badges_manage_panel = Badge Management
|
||||||
|
badges.details = Badge Details
|
||||||
|
badges.new_badge = Create New Badge
|
||||||
|
badges.slug = Slug
|
||||||
|
badges.description = Description
|
||||||
|
badges.image_url = Image URL
|
||||||
|
badges.slug.must_fill = Slug must be filled.
|
||||||
|
badges.new_success = The badge "%s" has been created.
|
||||||
|
badges.update_success = The badge has been updated.
|
||||||
|
badges.deletion_success = The badge has been deleted.
|
||||||
|
badges.edit_badge = Edit Badge
|
||||||
|
badges.update_badge = Update Badge
|
||||||
|
badges.delete_badge = Delete Badge
|
||||||
|
badges.delete_badge_desc = Are you sure you want to permanently delete this badge?
|
||||||
|
badges.users_with_badge = Users with Badge (%s)
|
||||||
|
badges.add_user = Add User
|
||||||
|
badges.remove_user = Remove User
|
||||||
|
badges.delete_user_desc = Are you sure you want to remove this badge from the user?
|
||||||
|
badges.not_found = Badge not found!
|
||||||
|
badges.user_add_success = User has been added to the badge.
|
||||||
|
badges.user_remove_success = User has been removed from the badge.
|
||||||
|
badges.manage_users = Manage Users
|
||||||
|
|
||||||
|
|
||||||
orgs.org_manage_panel = Organization Management
|
orgs.org_manage_panel = Organization Management
|
||||||
orgs.name = Name
|
orgs.name = Name
|
||||||
orgs.teams = Teams
|
orgs.teams = Teams
|
||||||
|
327
routers/web/admin/badges.go
Normal file
327
routers/web/admin/badges.go
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/templates"
|
||||||
|
"code.gitea.io/gitea/modules/web"
|
||||||
|
"code.gitea.io/gitea/routers/web/explore"
|
||||||
|
"code.gitea.io/gitea/services/context"
|
||||||
|
"code.gitea.io/gitea/services/forms"
|
||||||
|
user_service "code.gitea.io/gitea/services/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tplBadges templates.TplName = "admin/badge/list"
|
||||||
|
tplBadgeNew templates.TplName = "admin/badge/new"
|
||||||
|
tplBadgeView templates.TplName = "admin/badge/view"
|
||||||
|
tplBadgeEdit templates.TplName = "admin/badge/edit"
|
||||||
|
tplBadgeUsers templates.TplName = "admin/badge/users"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BadgeSearchDefaultAdminSort is the default sort type for admin view
|
||||||
|
const BadgeSearchDefaultAdminSort = "oldest"
|
||||||
|
|
||||||
|
// Badges show all the badges
|
||||||
|
func Badges(ctx *context.Context) {
|
||||||
|
ctx.Data["Title"] = ctx.Tr("admin.badges")
|
||||||
|
ctx.Data["PageIsAdminBadges"] = true
|
||||||
|
|
||||||
|
sortType := ctx.FormString("sort")
|
||||||
|
if sortType == "" {
|
||||||
|
sortType = BadgeSearchDefaultAdminSort
|
||||||
|
ctx.SetFormString("sort", sortType)
|
||||||
|
}
|
||||||
|
ctx.PageData["adminBadgeListSearchForm"] = map[string]any{
|
||||||
|
"SortType": sortType,
|
||||||
|
}
|
||||||
|
|
||||||
|
explore.RenderBadgeSearch(ctx, &user_model.SearchBadgeOptions{
|
||||||
|
Actor: ctx.Doer,
|
||||||
|
ListOptions: db.ListOptions{
|
||||||
|
PageSize: setting.UI.Admin.UserPagingNum,
|
||||||
|
},
|
||||||
|
}, tplBadges)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBadge render adding a new badge
|
||||||
|
func NewBadge(ctx *context.Context) {
|
||||||
|
ctx.Data["Title"] = ctx.Tr("admin.badges.new_badge")
|
||||||
|
ctx.Data["PageIsAdminBadges"] = true
|
||||||
|
|
||||||
|
ctx.HTML(http.StatusOK, tplBadgeNew)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBadgePost response for adding a new badge
|
||||||
|
func NewBadgePost(ctx *context.Context) {
|
||||||
|
form := web.GetForm(ctx).(*forms.AdminCreateBadgeForm)
|
||||||
|
ctx.Data["Title"] = ctx.Tr("admin.badges.new_badge")
|
||||||
|
ctx.Data["PageIsAdminBadges"] = true
|
||||||
|
|
||||||
|
if ctx.HasError() {
|
||||||
|
ctx.HTML(http.StatusOK, tplBadgeNew)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b := &user_model.Badge{
|
||||||
|
Slug: form.Slug,
|
||||||
|
Description: form.Description,
|
||||||
|
ImageURL: form.ImageURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(form.Slug) < 1 {
|
||||||
|
ctx.Data["Err_Slug"] = true
|
||||||
|
ctx.RenderWithErr(ctx.Tr("admin.badges.must_fill"), tplBadgeNew, &form)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user_model.CreateBadge(ctx, b); err != nil {
|
||||||
|
switch {
|
||||||
|
default:
|
||||||
|
ctx.ServerError("CreateBadge", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Trace("Badge created by admin (%s): %s", ctx.Doer.Name, b.Slug)
|
||||||
|
|
||||||
|
ctx.Flash.Success(ctx.Tr("admin.badges.new_success", b.Slug))
|
||||||
|
ctx.Redirect(setting.AppSubURL + "/-/admin/badges/" + url.PathEscape(b.Slug))
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareBadgeInfo(ctx *context.Context) *user_model.Badge {
|
||||||
|
b, err := user_model.GetBadge(ctx, ctx.PathParam(":badge_slug"))
|
||||||
|
if err != nil {
|
||||||
|
if user_model.IsErrBadgeNotExist(err) {
|
||||||
|
ctx.Redirect(setting.AppSubURL + "/-/admin/badges")
|
||||||
|
} else {
|
||||||
|
ctx.ServerError("GetBadge", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ctx.Data["Badge"] = b
|
||||||
|
|
||||||
|
opts := &user_model.GetBadgeUsersOptions{
|
||||||
|
ListOptions: db.ListOptions{
|
||||||
|
PageSize: setting.UI.User.RepoPagingNum,
|
||||||
|
},
|
||||||
|
Badge: b,
|
||||||
|
}
|
||||||
|
users, count, err := user_model.GetBadgeUsers(ctx, opts)
|
||||||
|
if err != nil {
|
||||||
|
if user_model.IsErrUserNotExist(err) {
|
||||||
|
ctx.Redirect(setting.AppSubURL + "/-/admin/badges")
|
||||||
|
} else {
|
||||||
|
ctx.ServerError("GetBadgeUsers", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ctx.Data["Users"] = users
|
||||||
|
ctx.Data["UsersTotal"] = int(count)
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func ViewBadge(ctx *context.Context) {
|
||||||
|
ctx.Data["Title"] = ctx.Tr("admin.badges.details")
|
||||||
|
ctx.Data["PageIsAdminBadges"] = true
|
||||||
|
|
||||||
|
prepareBadgeInfo(ctx)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.HTML(http.StatusOK, tplBadgeView)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EditBadge show editing badge page
|
||||||
|
func EditBadge(ctx *context.Context) {
|
||||||
|
ctx.Data["Title"] = ctx.Tr("admin.badges.edit_badges")
|
||||||
|
ctx.Data["PageIsAdminBadges"] = true
|
||||||
|
prepareBadgeInfo(ctx)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.HTML(http.StatusOK, tplBadgeEdit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EditBadgePost response for editing badge
|
||||||
|
func EditBadgePost(ctx *context.Context) {
|
||||||
|
ctx.Data["Title"] = ctx.Tr("admin.badges.edit_badges")
|
||||||
|
ctx.Data["PageIsAdminBadges"] = true
|
||||||
|
b := prepareBadgeInfo(ctx)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
form := web.GetForm(ctx).(*forms.AdminCreateBadgeForm)
|
||||||
|
if ctx.HasError() {
|
||||||
|
ctx.HTML(http.StatusOK, tplBadgeEdit)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if form.Slug != "" {
|
||||||
|
if err := user_service.UpdateBadge(ctx, ctx.Data["Badge"].(*user_model.Badge)); err != nil {
|
||||||
|
switch {
|
||||||
|
default:
|
||||||
|
ctx.ServerError("UpdateBadge", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ImageURL = form.ImageURL
|
||||||
|
b.Description = form.Description
|
||||||
|
|
||||||
|
if err := user_model.UpdateBadge(ctx, ctx.Data["Badge"].(*user_model.Badge)); err != nil {
|
||||||
|
ctx.ServerError("UpdateBadge", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Trace("Badge updated by admin (%s): %s", ctx.Doer.Name, b.Slug)
|
||||||
|
|
||||||
|
ctx.Flash.Success(ctx.Tr("admin.badges.update_success"))
|
||||||
|
ctx.Redirect(setting.AppSubURL + "/-/admin/badges/" + url.PathEscape(ctx.PathParam(":badge_slug")))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteBadge response for deleting a badge
|
||||||
|
func DeleteBadge(ctx *context.Context) {
|
||||||
|
b, err := user_model.GetBadge(ctx, ctx.PathParam(":badge_slug"))
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetBadge", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = user_service.DeleteBadge(ctx, b); err != nil {
|
||||||
|
ctx.ServerError("DeleteBadge", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Trace("Badge deleted by admin (%s): %s", ctx.Doer.Name, b.Slug)
|
||||||
|
|
||||||
|
ctx.Flash.Success(ctx.Tr("admin.badges.deletion_success"))
|
||||||
|
ctx.Redirect(setting.AppSubURL + "/-/admin/badges")
|
||||||
|
}
|
||||||
|
|
||||||
|
func BadgeUsers(ctx *context.Context) {
|
||||||
|
ctx.Data["Title"] = ctx.Tr("admin.badges.users_with_badge", ctx.PathParam(":badge_slug"))
|
||||||
|
ctx.Data["PageIsAdminBadges"] = true
|
||||||
|
|
||||||
|
page := ctx.FormInt("page")
|
||||||
|
if page <= 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
badge := &user_model.Badge{Slug: ctx.PathParam(":badge_slug")}
|
||||||
|
opts := &user_model.GetBadgeUsersOptions{
|
||||||
|
ListOptions: db.ListOptions{
|
||||||
|
Page: page,
|
||||||
|
PageSize: setting.UI.User.RepoPagingNum,
|
||||||
|
},
|
||||||
|
Badge: badge,
|
||||||
|
}
|
||||||
|
users, count, err := user_model.GetBadgeUsers(ctx, opts)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetBadgeUsers", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Data["Users"] = users
|
||||||
|
ctx.Data["Total"] = count
|
||||||
|
ctx.Data["Page"] = context.NewPagination(int(count), setting.UI.User.RepoPagingNum, page, 5)
|
||||||
|
|
||||||
|
ctx.HTML(http.StatusOK, tplBadgeUsers)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BadgeUsersPost response for actions for user badges
|
||||||
|
func BadgeUsersPost(ctx *context.Context) {
|
||||||
|
name := strings.ToLower(ctx.FormString("user"))
|
||||||
|
|
||||||
|
u, err := user_model.GetUserByName(ctx, name)
|
||||||
|
if err != nil {
|
||||||
|
if user_model.IsErrUserNotExist(err) {
|
||||||
|
ctx.Flash.Error(ctx.Tr("form.user_not_exist"))
|
||||||
|
ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath())
|
||||||
|
} else {
|
||||||
|
ctx.ServerError("GetUserByName", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = user_model.AddUserBadge(ctx, u, &user_model.Badge{Slug: ctx.PathParam(":badge_slug")}); err != nil {
|
||||||
|
if user_model.IsErrBadgeNotExist(err) {
|
||||||
|
ctx.Flash.Error(ctx.Tr("admin.badges.not_found"))
|
||||||
|
} else {
|
||||||
|
ctx.ServerError("AddUserBadge", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Flash.Success(ctx.Tr("admin.badges.user_add_success"))
|
||||||
|
ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath())
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteBadgeUser delete a badge from a user
|
||||||
|
func DeleteBadgeUser(ctx *context.Context) {
|
||||||
|
user, err := user_model.GetUserByID(ctx, ctx.FormInt64("id"))
|
||||||
|
if err != nil {
|
||||||
|
if user_model.IsErrUserNotExist(err) {
|
||||||
|
ctx.Flash.Error(ctx.Tr("form.user_not_exist"))
|
||||||
|
} else {
|
||||||
|
ctx.ServerError("GetUserByName", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := user_model.RemoveUserBadge(ctx, user, &user_model.Badge{Slug: ctx.PathParam(":badge_slug")}); err == nil {
|
||||||
|
ctx.Flash.Success(ctx.Tr("admin.badges.user_remove_success"))
|
||||||
|
} else {
|
||||||
|
ctx.Flash.Error("DeleteBadgeUser: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSONRedirect(fmt.Sprintf("%s/-/admin/badges/%s/users", setting.AppSubURL, ctx.PathParam(":badge_slug")))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ViewBadgeUsers render badge's users page
|
||||||
|
func ViewBadgeUsers(ctx *context.Context) {
|
||||||
|
badge, err := user_model.GetBadge(ctx, ctx.PathParam(":slug"))
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetBadge", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
page := ctx.FormInt("page")
|
||||||
|
if page <= 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := &user_model.GetBadgeUsersOptions{
|
||||||
|
ListOptions: db.ListOptions{
|
||||||
|
Page: page,
|
||||||
|
PageSize: setting.UI.User.RepoPagingNum,
|
||||||
|
},
|
||||||
|
Badge: badge,
|
||||||
|
}
|
||||||
|
users, count, err := user_model.GetBadgeUsers(ctx, opts)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetBadgeUsers", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Data["Title"] = badge.Description
|
||||||
|
ctx.Data["Badge"] = badge
|
||||||
|
ctx.Data["Users"] = users
|
||||||
|
ctx.Data["Total"] = count
|
||||||
|
ctx.Data["Pages"] = context.NewPagination(int(count), setting.UI.User.RepoPagingNum, page, 5)
|
||||||
|
ctx.HTML(http.StatusOK, tplBadgeUsers)
|
||||||
|
}
|
75
routers/web/explore/badge.go
Normal file
75
routers/web/explore/badge.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package explore
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/templates"
|
||||||
|
"code.gitea.io/gitea/services/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RenderBadgeSearch(ctx *context.Context, opts *user_model.SearchBadgeOptions, tplName templates.TplName) {
|
||||||
|
// Sitemap index for sitemap paths
|
||||||
|
opts.Page = int(ctx.PathParamInt64("idx"))
|
||||||
|
if opts.Page <= 1 {
|
||||||
|
opts.Page = ctx.FormInt("page")
|
||||||
|
}
|
||||||
|
if opts.Page <= 1 {
|
||||||
|
opts.Page = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
badges []*user_model.Badge
|
||||||
|
count int64
|
||||||
|
err error
|
||||||
|
orderBy db.SearchOrderBy
|
||||||
|
)
|
||||||
|
|
||||||
|
// we can not set orderBy to `models.SearchOrderByXxx`, because there may be a JOIN in the statement, different tables may have the same name columns
|
||||||
|
|
||||||
|
sortOrder := ctx.FormString("sort")
|
||||||
|
if sortOrder == "" {
|
||||||
|
sortOrder = setting.UI.ExploreDefaultSort
|
||||||
|
}
|
||||||
|
ctx.Data["SortType"] = sortOrder
|
||||||
|
|
||||||
|
switch sortOrder {
|
||||||
|
case "newest":
|
||||||
|
orderBy = "`badge`.id DESC"
|
||||||
|
case "oldest":
|
||||||
|
orderBy = "`badge`.id ASC"
|
||||||
|
case "reversealphabetically":
|
||||||
|
orderBy = "`badge`.slug DESC"
|
||||||
|
case "alphabetically":
|
||||||
|
orderBy = "`badge`.slug ASC"
|
||||||
|
default:
|
||||||
|
// in case the sortType is not valid, we set it to recent update
|
||||||
|
ctx.Data["SortType"] = "oldest"
|
||||||
|
orderBy = "`badge`.id ASC"
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.Keyword = ctx.FormTrim("q")
|
||||||
|
opts.OrderBy = orderBy
|
||||||
|
if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) {
|
||||||
|
badges, count, err = user_model.SearchBadges(ctx, opts)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("SearchBadges", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Data["Keyword"] = opts.Keyword
|
||||||
|
ctx.Data["Total"] = count
|
||||||
|
ctx.Data["Badges"] = badges
|
||||||
|
|
||||||
|
pager := context.NewPagination(int(count), opts.PageSize, opts.Page, 5)
|
||||||
|
pager.AddParamFromRequest(ctx.Req)
|
||||||
|
ctx.Data["Page"] = pager
|
||||||
|
|
||||||
|
ctx.HTML(http.StatusOK, tplName)
|
||||||
|
}
|
@ -753,6 +753,16 @@ func registerWebRoutes(m *web.Router) {
|
|||||||
m.Post("/{userid}/avatar/delete", admin.DeleteAvatar)
|
m.Post("/{userid}/avatar/delete", admin.DeleteAvatar)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
m.Group("/badges", func() {
|
||||||
|
m.Get("", admin.Badges)
|
||||||
|
m.Combo("/new").Get(admin.NewBadge).Post(web.Bind(forms.AdminCreateBadgeForm{}), admin.NewBadgePost)
|
||||||
|
m.Get("/{badge_slug}", admin.ViewBadge)
|
||||||
|
m.Combo("/{badge_slug}/edit").Get(admin.EditBadge).Post(web.Bind(forms.AdminCreateBadgeForm{}), admin.EditBadgePost)
|
||||||
|
m.Post("/{badge_slug}/delete", admin.DeleteBadge)
|
||||||
|
m.Combo("/{badge_slug}/users").Get(admin.BadgeUsers).Post(admin.BadgeUsersPost)
|
||||||
|
m.Post("/{badge_slug}/users/delete", admin.DeleteBadgeUser)
|
||||||
|
})
|
||||||
|
|
||||||
m.Group("/emails", func() {
|
m.Group("/emails", func() {
|
||||||
m.Get("", admin.Emails)
|
m.Get("", admin.Emails)
|
||||||
m.Post("/activate", admin.ActivateEmail)
|
m.Post("/activate", admin.ActivateEmail)
|
||||||
|
@ -25,6 +25,19 @@ type AdminCreateUserForm struct {
|
|||||||
Visibility structs.VisibleType
|
Visibility structs.VisibleType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AdminCreateBadgeForm form for admin to create badge
|
||||||
|
type AdminCreateBadgeForm struct {
|
||||||
|
Slug string `binding:"Required;Slug"`
|
||||||
|
Description string
|
||||||
|
ImageURL string `binding:"ValidImageUrl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates form fields
|
||||||
|
func (f *AdminCreateBadgeForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
|
||||||
|
ctx := context.GetValidateContext(req)
|
||||||
|
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
|
||||||
|
}
|
||||||
|
|
||||||
// Validate validates form fields
|
// Validate validates form fields
|
||||||
func (f *AdminCreateUserForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
|
func (f *AdminCreateUserForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
|
||||||
ctx := context.GetValidateContext(req)
|
ctx := context.GetValidateContext(req)
|
||||||
|
58
services/user/badge.go
Normal file
58
services/user/badge.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UpdateBadgeDescription changes the description and/or image of a badge
|
||||||
|
func UpdateBadge(ctx context.Context, b *user_model.Badge) error {
|
||||||
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer committer.Close()
|
||||||
|
|
||||||
|
if err := user_model.UpdateBadge(ctx, b); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return committer.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteBadge remove record of badge in the database
|
||||||
|
func DeleteBadge(ctx context.Context, b *user_model.Badge) error {
|
||||||
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer committer.Close()
|
||||||
|
|
||||||
|
if err := user_model.DeleteBadge(ctx, b); err != nil {
|
||||||
|
return fmt.Errorf("DeleteBadge: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := committer.Commit(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_ = committer.Close()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBadgeUsers returns the users that have a specific badge
|
||||||
|
func GetBadgeUsers(ctx context.Context, badge *user_model.Badge, page, pageSize int) ([]*user_model.User, int64, error) {
|
||||||
|
opts := &user_model.GetBadgeUsersOptions{
|
||||||
|
ListOptions: db.ListOptions{
|
||||||
|
Page: page,
|
||||||
|
PageSize: pageSize,
|
||||||
|
},
|
||||||
|
Badge: badge,
|
||||||
|
}
|
||||||
|
return user_model.GetBadgeUsers(ctx, opts)
|
||||||
|
}
|
48
templates/admin/badge/edit.tmpl
Normal file
48
templates/admin/badge/edit.tmpl
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin edit user")}}
|
||||||
|
<div class="admin-setting-content">
|
||||||
|
<h4 class="ui top attached header">
|
||||||
|
{{ctx.Locale.Tr "admin.badges.edit_badge"}}
|
||||||
|
</h4>
|
||||||
|
<div class="ui attached segment">
|
||||||
|
<form class="ui form" action="./edit" method="post">
|
||||||
|
{{template "base/disable_form_autofill"}}
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
|
||||||
|
<div class="non-local field {{if .Err_Slug}}error{{end}}" disabled=disabled>
|
||||||
|
<label for="slug">{{ctx.Locale.Tr "admin.badges.slug"}}</label>
|
||||||
|
<input disabled=disabled id="slug" name="slug" value="{{.Badge.Slug}}">
|
||||||
|
</div>
|
||||||
|
<div class="field {{if .Err_Description}}error{{end}}">
|
||||||
|
<label for="description">{{ctx.Locale.Tr "admin.badges.description"}}</label>
|
||||||
|
<textarea id="description" type="text" name="description" rows="2">{{.Badge.Description}}</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="field {{if .Err_ImageURL}}error{{end}}">
|
||||||
|
<label for="image_url">{{ctx.Locale.Tr "admin.badges.image_url"}}</label>
|
||||||
|
<input id="image_url" type="url" name="image_url" value="{{.Badge.ImageURL}}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<button class="ui primary button">{{ctx.Locale.Tr "admin.badges.update_badge"}}</button>
|
||||||
|
<button class="ui red button show-modal" data-modal="#delete-badge-modal">{{ctx.Locale.Tr "admin.badges.delete_badge"}}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ui g-modal-confirm delete modal" id="delete-badge-modal">
|
||||||
|
<div class="header">
|
||||||
|
{{svg "octicon-trash"}}
|
||||||
|
{{ctx.Locale.Tr "admin.badges.delete_badge"}}
|
||||||
|
</div>
|
||||||
|
<form class="ui form" method="post" action="./delete">
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ctx.Locale.Tr "admin.badges.delete_badge_desc"}}</p>
|
||||||
|
{{$.CsrfTokenHtml}}
|
||||||
|
</div>
|
||||||
|
{{template "base/modal_actions_confirm" .}}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{template "admin/layout_footer" .}}
|
67
templates/admin/badge/list.tmpl
Normal file
67
templates/admin/badge/list.tmpl
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin user")}}
|
||||||
|
<div class="admin-setting-content">
|
||||||
|
<h4 class="ui top attached header">
|
||||||
|
{{ctx.Locale.Tr "admin.badges.badges_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .Total}})
|
||||||
|
<div class="ui right">
|
||||||
|
<a class="ui primary tiny button" href="{{AppSubUrl}}/-/admin/badges/new">{{ctx.Locale.Tr "admin.badges.new_badge"}}</a>
|
||||||
|
</div>
|
||||||
|
</h4>
|
||||||
|
<div class="ui attached segment">
|
||||||
|
<form class="ui form ignore-dirty" id="user-list-search-form">
|
||||||
|
|
||||||
|
<!-- Right Menu -->
|
||||||
|
<div class="ui right floated secondary filter menu">
|
||||||
|
<!-- Sort Menu Item -->
|
||||||
|
<div class="ui dropdown type jump item">
|
||||||
|
<span class="text">
|
||||||
|
{{ctx.Locale.Tr "repo.issues.filter_sort"}}
|
||||||
|
</span>
|
||||||
|
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||||
|
<div class="menu">
|
||||||
|
<button class="item" name="sort" value="oldest">{{ctx.Locale.Tr "repo.issues.filter_sort.oldest"}}</button>
|
||||||
|
<button class="item" name="sort" value="newest">{{ctx.Locale.Tr "repo.issues.filter_sort.latest"}}</button>
|
||||||
|
<button class="item" name="sort" value="alphabetically">{{ctx.Locale.Tr "repo.issues.label.filter_sort.alphabetically"}}</button>
|
||||||
|
<button class="item" name="sort" value="reversealphabetically">{{ctx.Locale.Tr "repo.issues.label.filter_sort.reverse_alphabetically"}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{template "shared/search/combo" dict "Value" .Keyword "Placeholder" (ctx.Locale.Tr "search.badge_kind")}}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="ui attached table segment">
|
||||||
|
<table class="ui very basic striped table unstackable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th data-sortt-asc="oldest" data-sortt-desc="newest" data-sortt-default="true">ID{{SortArrow "oldest" "newest" .SortType false}}</th>
|
||||||
|
<th data-sortt-asc="alphabetically" data-sortt-desc="reversealphabetically">
|
||||||
|
{{ctx.Locale.Tr "admin.badges.slug"}}
|
||||||
|
{{SortArrow "alphabetically" "reversealphabeically" $.SortType true}}
|
||||||
|
</th>
|
||||||
|
<th>{{ctx.Locale.Tr "admin.badges.description"}}</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .Badges}}
|
||||||
|
<tr>
|
||||||
|
<td>{{.ID}}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{$.Link}}/{{.Slug}}">{{.Slug}}</a>
|
||||||
|
</td>
|
||||||
|
<td class="gt-ellipsis tw-max-w-48">{{.Description}}</td>
|
||||||
|
<td>
|
||||||
|
<div class="tw-flex tw-gap-2">
|
||||||
|
<a href="{{$.Link}}/{{.Slug}}" data-tooltip-content="{{ctx.Locale.Tr "admin.badges.details"}}">{{svg "octicon-star"}}</a>
|
||||||
|
<a href="{{$.Link}}/{{.Slug}}/edit" data-tooltip-content="{{ctx.Locale.Tr "edit"}}">{{svg "octicon-pencil"}}</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{template "base/paginate" .}}
|
||||||
|
</div>
|
||||||
|
{{template "admin/layout_footer" .}}
|
30
templates/admin/badge/new.tmpl
Normal file
30
templates/admin/badge/new.tmpl
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin new user")}}
|
||||||
|
<div class="admin-setting-content">
|
||||||
|
<h4 class="ui top attached header">
|
||||||
|
{{ctx.Locale.Tr "admin.badges.new_badge"}}
|
||||||
|
</h4>
|
||||||
|
<div class="ui attached segment">
|
||||||
|
<form class="ui form" action="{{.Link}}" method="post">
|
||||||
|
{{template "base/disable_form_autofill"}}
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
|
||||||
|
<div class="required non-local field {{if .Err_Slug}}error{{end}}">
|
||||||
|
<label for="slug">{{ctx.Locale.Tr "admin.badges.slug"}}</label>
|
||||||
|
<input autofocus required id="slug" name="slug" value="{{.slug}}">
|
||||||
|
</div>
|
||||||
|
<div class="field {{if .Err_Description}}error{{end}}">
|
||||||
|
<label for="description">{{ctx.Locale.Tr "admin.badges.description"}}</label>
|
||||||
|
<textarea id="description" type="text" name="description" rows="2">{{.description}}</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="field {{if .Err_ImageURL}}error{{end}}">
|
||||||
|
<label for="image_url">{{ctx.Locale.Tr "admin.badges.image_url"}}</label>
|
||||||
|
<input id="image_url" type="url" name="image_url" value="{{.image_url}}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<button class="ui primary button">{{ctx.Locale.Tr "admin.badges.new_badge"}}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{template "admin/layout_footer" .}}
|
55
templates/admin/badge/users.tmpl
Normal file
55
templates/admin/badge/users.tmpl
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin user")}}
|
||||||
|
<div class="admin-setting-content">
|
||||||
|
<h4 class="ui top attached header">
|
||||||
|
{{.Title}}
|
||||||
|
</h4>
|
||||||
|
{{if .Users}}
|
||||||
|
<div class="ui attached segment">
|
||||||
|
<div class="flex-list">
|
||||||
|
{{range .Users}}
|
||||||
|
<div class="flex-item tw-items-center">
|
||||||
|
<div class="flex-item-leading">
|
||||||
|
<a href="{{.HomeLink}}">{{ctx.AvatarUtils.Avatar . 32}}</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex-item-main">
|
||||||
|
<div class="flex-item-title">
|
||||||
|
{{template "shared/user/name" .}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-item-trailing">
|
||||||
|
<button class="ui red tiny button inline delete-button" data-url="{{$.Link}}/delete" data-id="{{.ID}}">
|
||||||
|
{{ctx.Locale.Tr "admin.badges.remove_user"}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{template "base/paginate" .}}
|
||||||
|
<div class="ui bottom attached segment">
|
||||||
|
<form class="ui form" id="search-badge-user-form" action="{{.Link}}" method="post">
|
||||||
|
{{.CsrfTokenHtml}}
|
||||||
|
<div id="search-user-box" class="ui search input tw-align-middle">
|
||||||
|
<input class="prompt" name="user" placeholder="{{ctx.Locale.Tr "search.user_kind"}}" autocomplete="off" autofocus required>
|
||||||
|
</div>
|
||||||
|
<button class="ui primary button">{{ctx.Locale.Tr "admin.badges.add_user"}}</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ui g-modal-confirm delete modal">
|
||||||
|
<div class="header">
|
||||||
|
{{svg "octicon-trash"}}
|
||||||
|
{{ctx.Locale.Tr "admin.badges.remove_user"}}
|
||||||
|
</div>
|
||||||
|
<form class="ui form" method="post" id="remove-badge-user-form" action="{{.Link}}">
|
||||||
|
<div class="content">
|
||||||
|
{{$.CsrfTokenHtml}}
|
||||||
|
<p>{{ctx.Locale.Tr "admin.badges.delete_user_desc"}}</p>
|
||||||
|
</div>
|
||||||
|
{{template "base/modal_actions_confirm" .}}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{template "admin/layout_footer" .}}
|
44
templates/admin/badge/view.tmpl
Normal file
44
templates/admin/badge/view.tmpl
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin view user")}}
|
||||||
|
|
||||||
|
<div class="admin-setting-content">
|
||||||
|
<div class="admin-responsive-columns">
|
||||||
|
<div class="tw-flex-1">
|
||||||
|
<h4 class="ui top attached header">
|
||||||
|
{{.Title}}
|
||||||
|
<div class="ui right">
|
||||||
|
<a class="ui primary tiny button" href="{{.Link}}/edit">{{ctx.Locale.Tr "admin.users.edit"}}</a>
|
||||||
|
</div>
|
||||||
|
</h4>
|
||||||
|
<div class="ui attached segment">
|
||||||
|
<div class="flex-list">
|
||||||
|
<div class="flex-item">
|
||||||
|
{{if .Image}}
|
||||||
|
<div class="flex-item-leading">
|
||||||
|
<img width="64" height="64" src="{{.Badge.ImageURL}}" alt="{{.Badge.Description}}" data-tooltip-content="{{.Badge.Description}}">
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<div class="flex-item-main">
|
||||||
|
<div class="flex-item-title">
|
||||||
|
{{.Badge.Slug}}
|
||||||
|
</div>
|
||||||
|
<div class="flex-item-body">
|
||||||
|
{{.Badge.Description}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h4 class="ui top attached header">
|
||||||
|
{{ctx.Locale.Tr "explore.users"}} ({{.UsersTotal}})
|
||||||
|
<div class="ui right">
|
||||||
|
<a class="ui primary tiny button" href="{{.Link}}/users">{{ctx.Locale.Tr "admin.badges.manage_users"}}</a>
|
||||||
|
</div>
|
||||||
|
</h4>
|
||||||
|
<div class="ui attached segment">
|
||||||
|
{{template "explore/user_list" .}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{template "admin/layout_footer" .}}
|
@ -13,7 +13,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
<details class="item toggleable-item" {{if or .PageIsAdminUsers .PageIsAdminEmails .PageIsAdminOrganizations .PageIsAdminAuthentications}}open{{end}}>
|
<details class="item toggleable-item" {{if or .PageIsAdminUsers .PageIsAdminBadges .PageIsAdminEmails .PageIsAdminOrganizations .PageIsAdminAuthentications}}open{{end}}>
|
||||||
<summary>{{ctx.Locale.Tr "admin.identity_access"}}</summary>
|
<summary>{{ctx.Locale.Tr "admin.identity_access"}}</summary>
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
<a class="{{if .PageIsAdminAuthentications}}active {{end}}item" href="{{AppSubUrl}}/-/admin/auths">
|
<a class="{{if .PageIsAdminAuthentications}}active {{end}}item" href="{{AppSubUrl}}/-/admin/auths">
|
||||||
@ -25,6 +25,9 @@
|
|||||||
<a class="{{if .PageIsAdminUsers}}active {{end}}item" href="{{AppSubUrl}}/-/admin/users">
|
<a class="{{if .PageIsAdminUsers}}active {{end}}item" href="{{AppSubUrl}}/-/admin/users">
|
||||||
{{ctx.Locale.Tr "admin.users"}}
|
{{ctx.Locale.Tr "admin.users"}}
|
||||||
</a>
|
</a>
|
||||||
|
<a class="{{if .PageIsAdminBadges}}active {{end}}item" href="{{AppSubUrl}}/-/admin/badges">
|
||||||
|
{{ctx.Locale.Tr "admin.badges"}}
|
||||||
|
</a>
|
||||||
<a class="{{if .PageIsAdminEmails}}active {{end}}item" href="{{AppSubUrl}}/-/admin/emails">
|
<a class="{{if .PageIsAdminEmails}}active {{end}}item" href="{{AppSubUrl}}/-/admin/emails">
|
||||||
{{ctx.Locale.Tr "admin.emails"}}
|
{{ctx.Locale.Tr "admin.emails"}}
|
||||||
</a>
|
</a>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user