diff --git a/models/badges/badge.go b/models/badges/badge.go new file mode 100644 index 0000000000000..e6edc51c63c82 --- /dev/null +++ b/models/badges/badge.go @@ -0,0 +1,128 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package badges + +import ( + "context" + + "gitea.dev/models/db" + "gitea.dev/modules/util" + + "xorm.io/builder" +) + +// Badge represents a reputation badge +type Badge struct { + ID int64 `xorm:"pk autoincr"` + Slug string `xorm:"UNIQUE"` + Description string + ImageURL string +} + +func init() { + db.RegisterModel(new(Badge)) +} + +// 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 entries. +func DeleteBadge(ctx context.Context, badge *Badge) error { + return db.WithTx(ctx, func(ctx context.Context) error { + // First delete all mapping entries for this badge + if _, err := db.GetEngine(ctx).Exec("DELETE FROM user_badge WHERE badge_id = ?", badge.ID); err != nil { + return err + } + if _, err := db.GetEngine(ctx).Exec("DELETE FROM org_badge WHERE badge_id = ?", badge.ID); err != nil { + return err + } + if _, err := db.GetEngine(ctx).Exec("DELETE FROM repo_badge WHERE badge_id = ?", badge.ID); 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 + }) +} + +// 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) +} diff --git a/models/organization/badge.go b/models/organization/badge.go new file mode 100644 index 0000000000000..ef1bc99314a7e --- /dev/null +++ b/models/organization/badge.go @@ -0,0 +1,126 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package organization + +import ( + "context" + + "gitea.dev/models/badges" + "gitea.dev/models/db" + "gitea.dev/modules/util" + + "xorm.io/xorm/schemas" +) + +// OrgBadge represents an organization badge +type OrgBadge struct { + ID int64 `xorm:"pk autoincr"` + BadgeID int64 + OrgID int64 +} + +// TableIndices implements xorm's TableIndices interface +func (n *OrgBadge) TableIndices() []*schemas.Index { + indices := make([]*schemas.Index, 0, 1) + ubUnique := schemas.NewIndex("unique_org_badge", schemas.UniqueType) + ubUnique.AddColumn("org_id", "badge_id") + indices = append(indices, ubUnique) + return indices +} + +func init() { + db.RegisterModel(new(OrgBadge)) +} + +// GetOrgBadges returns the org's badges. +func GetOrgBadges(ctx context.Context, org *Organization) ([]*badges.Badge, int64, error) { + sess := db.GetEngine(ctx). + Select("`badge`.*"). + Join("INNER", "org_badge", "`org_badge`.badge_id=badge.id"). + Where("org_badge.org_id=?", org.ID) + + badgesSlice := make([]*badges.Badge, 0, 8) + count, err := sess.FindAndCount(&badgesSlice) + return badgesSlice, count, err +} + +// AddOrgBadge adds a badge to an organization. +func AddOrgBadge(ctx context.Context, org *Organization, badge *badges.Badge) error { + return db.WithTx(ctx, func(ctx context.Context) error { + // 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 org_id = ?", badge.ID, org.ID).Exist(new(OrgBadge)) + if err != nil { + return err + } + if exists { + return util.NewAlreadyExistErrorf("org badge already exists [org_id: %d, badge_id: %d]", org.ID, badge.ID) + } + + if err := db.Insert(ctx, &OrgBadge{ + BadgeID: badge.ID, + OrgID: org.ID, + }); err != nil { + exists, existErr := db.GetEngine(ctx).Where("badge_id = ? AND org_id = ?", badge.ID, org.ID).Exist(new(OrgBadge)) + if existErr == nil && exists { + return util.NewAlreadyExistErrorf("org badge already exists [org_id: %d, badge_id: %d]", org.ID, badge.ID) + } + return err + } + return nil + }) +} + +// RemoveOrgBadge removes a badge from an organization. +func RemoveOrgBadge(ctx context.Context, org *Organization, badge *badges.Badge) error { + return db.WithTx(ctx, func(ctx context.Context) error { + var userBadges []OrgBadge + if err := db.GetEngine(ctx).Table("org_badge"). + Join("INNER", "badge", "badge.id = `org_badge`.badge_id"). + Where("`org_badge`.org_id = ?", org.ID).In("`badge`.slug", []string{badge.Slug}). + 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("org_badge").In("id", userBadgeIDs).Delete(); err != nil { + return err + } + return nil + }) +} + +// GetBadgeOrgsOptions contains options for getting orgs with a specific badge +type GetBadgeOrgsOptions struct { + db.ListOptions + BadgeSlug string +} + +// GetBadgeOrgs returns the orgs that have a specific badge with pagination support. +func GetBadgeOrgs(ctx context.Context, opts *GetBadgeOrgsOptions) ([]*Organization, int64, error) { + sess := db.GetEngine(ctx). + Select("`user`.*"). + Join("INNER", "org_badge", "`org_badge`.org_id=`user`.id"). + Join("INNER", "badge", "`org_badge`.badge_id=badge.id"). + Where("badge.slug=?", opts.BadgeSlug) + + if opts.Page > 0 { + db.SetSessionPagination(sess, opts) + } + + orgs := make([]*Organization, 0, opts.PageSize) + count, err := sess.FindAndCount(&orgs) + return orgs, count, err +} diff --git a/models/repo/badge.go b/models/repo/badge.go new file mode 100644 index 0000000000000..dca4ebe7f247a --- /dev/null +++ b/models/repo/badge.go @@ -0,0 +1,126 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package repo + +import ( + "context" + + "gitea.dev/models/badges" + "gitea.dev/models/db" + "gitea.dev/modules/util" + + "xorm.io/xorm/schemas" +) + +// RepoBadge represents a repository badge +type RepoBadge struct { //nolint:revive // export stutter + ID int64 `xorm:"pk autoincr"` + BadgeID int64 + RepoID int64 +} + +// TableIndices implements xorm's TableIndices interface +func (n *RepoBadge) TableIndices() []*schemas.Index { + indices := make([]*schemas.Index, 0, 1) + ubUnique := schemas.NewIndex("unique_repo_badge", schemas.UniqueType) + ubUnique.AddColumn("repo_id", "badge_id") + indices = append(indices, ubUnique) + return indices +} + +func init() { + db.RegisterModel(new(RepoBadge)) +} + +// GetRepoBadges returns the repo's badges. +func GetRepoBadges(ctx context.Context, repo *Repository) ([]*badges.Badge, int64, error) { + sess := db.GetEngine(ctx). + Select("`badge`.*"). + Join("INNER", "repo_badge", "`repo_badge`.badge_id=badge.id"). + Where("repo_badge.repo_id=?", repo.ID) + + badgesSlice := make([]*badges.Badge, 0, 8) + count, err := sess.FindAndCount(&badgesSlice) + return badgesSlice, count, err +} + +// AddRepoBadge adds a badge to a repository. +func AddRepoBadge(ctx context.Context, repo *Repository, badge *badges.Badge) error { + return db.WithTx(ctx, func(ctx context.Context) error { + // 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 repo_id = ?", badge.ID, repo.ID).Exist(new(RepoBadge)) + if err != nil { + return err + } + if exists { + return util.NewAlreadyExistErrorf("repo badge already exists [repo_id: %d, badge_id: %d]", repo.ID, badge.ID) + } + + if err := db.Insert(ctx, &RepoBadge{ + BadgeID: badge.ID, + RepoID: repo.ID, + }); err != nil { + exists, existErr := db.GetEngine(ctx).Where("badge_id = ? AND repo_id = ?", badge.ID, repo.ID).Exist(new(RepoBadge)) + if existErr == nil && exists { + return util.NewAlreadyExistErrorf("repo badge already exists [repo_id: %d, badge_id: %d]", repo.ID, badge.ID) + } + return err + } + return nil + }) +} + +// RemoveRepoBadge removes a badge from a repository. +func RemoveRepoBadge(ctx context.Context, repo *Repository, badge *badges.Badge) error { + return db.WithTx(ctx, func(ctx context.Context) error { + var userBadges []RepoBadge + if err := db.GetEngine(ctx).Table("repo_badge"). + Join("INNER", "badge", "badge.id = `repo_badge`.badge_id"). + Where("`repo_badge`.repo_id = ?", repo.ID).In("`badge`.slug", []string{badge.Slug}). + 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("repo_badge").In("id", userBadgeIDs).Delete(); err != nil { + return err + } + return nil + }) +} + +// GetBadgeReposOptions contains options for getting repos with a specific badge +type GetBadgeReposOptions struct { + db.ListOptions + BadgeSlug string +} + +// GetBadgeRepos returns the repos that have a specific badge with pagination support. +func GetBadgeRepos(ctx context.Context, opts *GetBadgeReposOptions) ([]*Repository, int64, error) { + sess := db.GetEngine(ctx). + Select("`repository`.*"). + Join("INNER", "repo_badge", "`repo_badge`.repo_id=`repository`.id"). + Join("INNER", "badge", "`repo_badge`.badge_id=badge.id"). + Where("badge.slug=?", opts.BadgeSlug) + + if opts.Page > 0 { + db.SetSessionPagination(sess, opts) + } + + repos := make([]*Repository, 0, opts.PageSize) + count, err := sess.FindAndCount(&repos) + return repos, count, err +} diff --git a/models/repo/repo.go b/models/repo/repo.go index ee7e62b119613..a01f7b9e0b58e 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -17,6 +17,7 @@ import ( "strings" "sync" + "gitea.dev/models/badges" "gitea.dev/models/db" "gitea.dev/models/unit" user_model "gitea.dev/models/user" @@ -194,8 +195,9 @@ type Repository struct { commonRenderingMetas map[string]string `xorm:"-"` - Units []*RepoUnit `xorm:"-"` - PrimaryLanguage *LanguageStat `xorm:"-"` + Units []*RepoUnit `xorm:"-"` + PrimaryLanguage *LanguageStat `xorm:"-"` + Badges []*badges.Badge `xorm:"-"` IsFork bool `xorm:"INDEX NOT NULL DEFAULT false"` ForkID int64 `xorm:"INDEX"` @@ -365,6 +367,19 @@ func (repo *Repository) HTMLURL(ctxs ...context.Context) string { return httplib.MakeAbsoluteURL(ctx, repo.Link()) } +// LoadBadges loads the badges of the repository +func (repo *Repository) LoadBadges(ctx context.Context) error { + if repo.Badges != nil { + return nil + } + badges, _, err := GetRepoBadges(ctx, repo) + if err != nil { + return err + } + repo.Badges = badges + return nil +} + // CommitLink make link to by commit full ID // note: won't check whether it's an right id func (repo *Repository) CommitLink(commitID string) (result string) { diff --git a/models/repo/repo_list.go b/models/repo/repo_list.go index 57f9e7883328a..bf158d03f5a8a 100644 --- a/models/repo/repo_list.go +++ b/models/repo/repo_list.go @@ -8,6 +8,7 @@ import ( "fmt" "strings" + "gitea.dev/models/badges" "gitea.dev/models/db" "gitea.dev/models/perm" "gitea.dev/models/unit" @@ -794,3 +795,57 @@ func GetOwnerRepositoriesByIDs(ctx context.Context, ownerID int64, repoIDs []int repos := make(RepositoryList, 0, len(repoIDs)) return repos, db.GetEngine(ctx).Where(builder.Eq{"owner_id": ownerID}).In("id", repoIDs).Find(&repos) } + +// LoadBadges loads the badges of the repositories +func (repos RepositoryList) LoadBadges(ctx context.Context) error { + if len(repos) == 0 { + return nil + } + + repoIDs := make([]int64, 0, len(repos)) + for _, repo := range repos { + if repo.Badges == nil { + repoIDs = append(repoIDs, repo.ID) + } + } + if len(repoIDs) == 0 { + return nil + } + + var repoBadges []RepoBadge + if err := db.GetEngine(ctx).Table("repo_badge").In("repo_id", repoIDs).Find(&repoBadges); err != nil { + return err + } + + badgeIDs := make([]int64, 0, len(repoBadges)) + for _, rb := range repoBadges { + badgeIDs = append(badgeIDs, rb.BadgeID) + } + + if len(badgeIDs) == 0 { + return nil + } + + badgesList := make([]*badges.Badge, 0, len(badgeIDs)) + if err := db.GetEngine(ctx).Table("badge").In("id", badgeIDs).Find(&badgesList); err != nil { + return err + } + + badgeMap := make(map[int64]*badges.Badge, len(badgesList)) + for _, b := range badgesList { + badgeMap[b.ID] = b + } + + for _, rb := range repoBadges { + for _, repo := range repos { + if repo.ID == rb.RepoID { + if badge, ok := badgeMap[rb.BadgeID]; ok { + repo.Badges = append(repo.Badges, badge) + } + break + } + } + } + + return nil +} diff --git a/models/user/badge.go b/models/user/badge.go index 0858fd6765663..863a856121261 100644 --- a/models/user/badge.go +++ b/models/user/badge.go @@ -6,22 +6,14 @@ package user import ( "context" + "gitea.dev/models/badges" "gitea.dev/models/db" "gitea.dev/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 +// UserBadge represents a user badge link type UserBadge struct { //nolint:revive // export stutter ID int64 `xorm:"pk autoincr"` BadgeID int64 @@ -38,20 +30,19 @@ func (n *UserBadge) TableIndices() []*schemas.Index { } 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) { +func GetUserBadges(ctx context.Context, u *User) ([]*badges.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 + b := make([]*badges.Badge, 0, 8) + count, err := sess.FindAndCount(&b) + return b, count, err } // GetBadgeUsersOptions contains options for getting users with a specific badge @@ -77,73 +68,15 @@ func GetBadgeUsers(ctx context.Context, opts *GetBadgeUsersOptions) ([]*User, in 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}) +func AddUserBadge(ctx context.Context, u *User, badge *badges.Badge) error { + return AddUserBadges(ctx, u, []*badges.Badge{badge}) } // AddUserBadges adds badges to a user. -func AddUserBadges(ctx context.Context, u *User, badges []*Badge) error { +func AddUserBadges(ctx context.Context, u *User, bdgs []*badges.Badge) error { return db.WithTx(ctx, func(ctx context.Context) error { - for _, badge := range badges { + for _, badge := range bdgs { // hydrate badge and check if it exists has, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Get(badge) if err != nil { @@ -176,19 +109,19 @@ func AddUserBadges(ctx context.Context, u *User, badges []*Badge) error { } // RemoveUserBadge removes a badge from a user. -func RemoveUserBadge(ctx context.Context, u *User, badge *Badge) error { - return RemoveUserBadges(ctx, u, []*Badge{badge}) +func RemoveUserBadge(ctx context.Context, u *User, badge *badges.Badge) error { + return RemoveUserBadges(ctx, u, []*badges.Badge{badge}) } // RemoveUserBadges removes specific badges from a user. -func RemoveUserBadges(ctx context.Context, u *User, badges []*Badge) error { +func RemoveUserBadges(ctx context.Context, u *User, bdgs []*badges.Badge) error { return db.WithTx(ctx, func(ctx context.Context) error { - if len(badges) == 0 { + if len(bdgs) == 0 { return nil } - badgeSlugs := make([]string, 0, len(badges)) - for _, badge := range badges { + badgeSlugs := make([]string, 0, len(bdgs)) + for _, badge := range bdgs { badgeSlugs = append(badgeSlugs, badge.Slug) } var userBadges []UserBadge @@ -211,44 +144,3 @@ func RemoveUserBadges(ctx context.Context, u *User, badges []*Badge) error { return nil }) } - -// 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) -} diff --git a/models/user/badge_test.go b/models/user/badge_test.go index 72d2aedc1e19b..ca6a67aac1d8f 100644 --- a/models/user/badge_test.go +++ b/models/user/badge_test.go @@ -32,13 +32,13 @@ func testGetBadgeNotExist(t *testing.T) { } func testCreateBadgeAlreadyExists(t *testing.T) { - badge := &user_model.Badge{ + badge := &badges.Badge{ Slug: "duplicate-badge-slug", Description: "First", } assert.NoError(t, user_model.CreateBadge(t.Context(), badge)) - err := user_model.CreateBadge(t.Context(), &user_model.Badge{ + err := user_model.CreateBadge(t.Context(), &badges.Badge{ Slug: "duplicate-badge-slug", Description: "Second", }) @@ -48,7 +48,7 @@ func testCreateBadgeAlreadyExists(t *testing.T) { func testGetBadgeUsers(t *testing.T) { // Create a test badge - badge := &user_model.Badge{ + badge := &badges.Badge{ Slug: "test-badge-users", Description: "Test Badge", ImageURL: "test.png", @@ -96,7 +96,7 @@ func testGetBadgeUsers(t *testing.T) { } func testAddAndRemoveUserBadges(t *testing.T) { - badge1 := unittest.AssertExistsAndLoadBean(t, &user_model.Badge{ID: 1}) + badge1 := unittest.AssertExistsAndLoadBean(t, &badges.Badge{ID: 1}) user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) // Add a badge to user and verify that it is returned in the list @@ -125,11 +125,11 @@ func testAddAndRemoveUserBadges(t *testing.T) { // Removing empty or missing badge selections should be a no-op. assert.NoError(t, user_model.RemoveUserBadges(t.Context(), user1, nil)) - assert.NoError(t, user_model.RemoveUserBadges(t.Context(), user1, []*user_model.Badge{{Slug: "does-not-exist"}})) + assert.NoError(t, user_model.RemoveUserBadges(t.Context(), user1, []*badges.Badge{{Slug: "does-not-exist"}})) } func testSearchBadgesOrderingAndKeyword(t *testing.T) { - createdBadges := []*user_model.Badge{ + createdBadges := []*badges.Badge{ {Slug: "badge-sort-b", Description: "Badge Sort B"}, {Slug: "badge-sort-c", Description: "Badge Sort C"}, {Slug: "badge-sort-a", Description: "Badge Sort A"}, @@ -176,7 +176,7 @@ func testSearchBadgesOrderingAndKeyword(t *testing.T) { assert.Equal(t, []string{"badge-sort-case"}, collectBadgeSlugs(caseInsensitive)) } -func collectBadgeSlugs(badges []*user_model.Badge) []string { +func collectBadgeSlugs(badges []*badges.Badge) []string { slugs := make([]string, 0, len(badges)) for _, badge := range badges { slugs = append(slugs, badge.Slug) diff --git a/models/user/user.go b/models/user/user.go index 6d3d85c309db3..5427353872881 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -22,6 +22,7 @@ import ( "unicode" "gitea.dev/models/auth" + "gitea.dev/models/badges" "gitea.dev/models/db" "gitea.dev/modules/auth/openid" "gitea.dev/modules/auth/password/hash" @@ -154,6 +155,8 @@ type User struct { DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"` Theme string `xorm:"NOT NULL DEFAULT ''"` KeepActivityPrivate bool `xorm:"NOT NULL DEFAULT false"` + + Badges []*badges.Badge `xorm:"-"` } // Meta defines the meta information of a user, to be stored in the K/V table diff --git a/options/bindata.dat b/options/bindata.dat new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 9373aa83a90dd..0cbece32e5457 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -2924,6 +2924,22 @@ "admin.badges.add_user": "Add User", "admin.badges.remove_user": "Remove User", "admin.badges.delete_user_desc": "Are you sure you want to remove this user from the badge?", + "admin.badges.repos_with_badge": "Repositories with badge: %s", + "admin.badges.repo_already_has": "Repository already has this badge.", + "admin.badges.repo_add_success": "Badge assigned to repository successfully.", + "admin.badges.repo_remove_success": "Badge removed from repository successfully.", + "admin.badges.manage_repos": "Manage Repositories", + "admin.badges.add_repo": "Add Repository", + "admin.badges.remove_repo": "Remove Repository", + "admin.badges.delete_repo_desc": "Are you sure you want to remove this repository from the badge?", + "admin.badges.orgs_with_badge": "Organizations with badge: %s", + "admin.badges.org_already_has": "Organization already has this badge.", + "admin.badges.org_add_success": "Badge assigned to organization successfully.", + "admin.badges.org_remove_success": "Badge removed from organization successfully.", + "admin.badges.manage_orgs": "Manage Organizations", + "admin.badges.add_org": "Add Organization", + "admin.badges.remove_org": "Remove Organization", + "admin.badges.delete_org_desc": "Are you sure you want to remove this organization from the badge?", "admin.emails": "User Email Addresses", "admin.config": "Configuration", "admin.config_summary": "Summary", diff --git a/routers/api/v1/admin/user_badge.go b/routers/api/v1/admin/user_badge.go index 97b12d84efffe..07d001c122c9d 100644 --- a/routers/api/v1/admin/user_badge.go +++ b/routers/api/v1/admin/user_badge.go @@ -6,6 +6,7 @@ package admin import ( "net/http" + "gitea.dev/models/badges" user_model "gitea.dev/models/user" api "gitea.dev/modules/structs" "gitea.dev/modules/web" @@ -113,12 +114,12 @@ func DeleteUserBadges(ctx *context.APIContext) { ctx.Status(http.StatusNoContent) } -func prepareBadgesForReplaceOrAdd(form api.UserBadgeOption) []*user_model.Badge { - badges := make([]*user_model.Badge, len(form.BadgeSlugs)) +func prepareBadgesForReplaceOrAdd(form api.UserBadgeOption) []*badges.Badge { + badgesList := make([]*badges.Badge, len(form.BadgeSlugs)) for i, badge := range form.BadgeSlugs { - badges[i] = &user_model.Badge{ + badgesList[i] = &badges.Badge{ Slug: badge, } } - return badges + return badgesList } diff --git a/routers/web/admin/badges.go b/routers/web/admin/badges.go index 227e460b10de0..99575778d9ed0 100644 --- a/routers/web/admin/badges.go +++ b/routers/web/admin/badges.go @@ -9,7 +9,10 @@ import ( "net/url" "strings" + "gitea.dev/models/badges" "gitea.dev/models/db" + org_model "gitea.dev/models/organization" + repo_model "gitea.dev/models/repo" user_model "gitea.dev/models/user" "gitea.dev/modules/log" "gitea.dev/modules/setting" @@ -26,6 +29,8 @@ const ( tplBadgeView templates.TplName = "admin/badge/view" tplBadgeEdit templates.TplName = "admin/badge/edit" tplBadgeUsers templates.TplName = "admin/badge/users" + tplBadgeRepos templates.TplName = "admin/badge/repos" + tplBadgeOrgs templates.TplName = "admin/badge/orgs" ) // BadgeSearchDefaultAdminSort is the default sort type for admin view @@ -36,7 +41,7 @@ func Badges(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("admin.badges") ctx.Data["PageIsAdminBadges"] = true - RenderBadgeSearch(ctx, &user_model.SearchBadgeOptions{ + RenderBadgeSearch(ctx, &badges.SearchBadgeOptions{ ListOptions: db.ListOptions{ Page: max(ctx.FormInt("page"), 1), PageSize: setting.UI.Admin.UserPagingNum, @@ -61,13 +66,13 @@ func NewBadgePost(ctx *context.Context) { return } - b := &user_model.Badge{ + b := &badges.Badge{ Slug: form.Slug, Description: form.Description, ImageURL: form.ImageURL, } - if err := user_model.CreateBadge(ctx, b); err != nil { + if err := badges.CreateBadge(ctx, b); err != nil { if errors.Is(err, util.ErrAlreadyExist) { ctx.JSONError(ctx.Tr("admin.badges.slug_been_taken")) } else { @@ -82,8 +87,8 @@ func NewBadgePost(ctx *context.Context) { ctx.JSONRedirect(setting.AppSubURL + "/-/admin/badges/slug/" + url.PathEscape(b.Slug)) } -func prepareBadgeInfo(ctx *context.Context) *user_model.Badge { - b, err := user_model.GetBadge(ctx, ctx.PathParam("badge_slug")) +func prepareBadgeInfo(ctx *context.Context) *badges.Badge { + b, err := badges.GetBadge(ctx, ctx.PathParam("badge_slug")) if err != nil { if errors.Is(err, util.ErrNotExist) { ctx.Redirect(setting.AppSubURL + "/-/admin/badges") @@ -105,7 +110,7 @@ func ViewBadge(ctx *context.Context) { return } - badge := ctx.Data["Badge"].(*user_model.Badge) + badge := ctx.Data["Badge"].(*badges.Badge) opts := &user_model.GetBadgeUsersOptions{ ListOptions: db.ListOptions{ Page: 1, @@ -121,6 +126,40 @@ func ViewBadge(ctx *context.Context) { ctx.Data["Users"] = users ctx.Data["UsersTotal"] = int(count) + repoOpts := &repo_model.GetBadgeReposOptions{ + ListOptions: db.ListOptions{ + Page: 1, + PageSize: setting.UI.Admin.UserPagingNum, + }, + BadgeSlug: badge.Slug, + } + repos, repoCount, err := repo_model.GetBadgeRepos(ctx, repoOpts) + if err != nil { + ctx.ServerError("GetBadgeRepos", err) + return + } + if err := repo_model.RepositoryList(repos).LoadOwners(ctx); err != nil { + ctx.ServerError("LoadOwners", err) + return + } + ctx.Data["Repos"] = repos + ctx.Data["ReposTotal"] = int(repoCount) + + orgOpts := &org_model.GetBadgeOrgsOptions{ + ListOptions: db.ListOptions{ + Page: 1, + PageSize: setting.UI.Admin.UserPagingNum, + }, + BadgeSlug: badge.Slug, + } + orgs, orgCount, err := org_model.GetBadgeOrgs(ctx, orgOpts) + if err != nil { + ctx.ServerError("GetBadgeOrgs", err) + return + } + ctx.Data["Orgs"] = orgs + ctx.Data["OrgsTotal"] = int(orgCount) + ctx.HTML(http.StatusOK, tplBadgeView) } @@ -152,7 +191,7 @@ func EditBadgePost(ctx *context.Context) { b.ImageURL = form.ImageURL b.Description = form.Description - if err := user_model.UpdateBadge(ctx, b); err != nil { + if err := badges.UpdateBadge(ctx, b); err != nil { ctx.ServerError("UpdateBadge", err) return } @@ -165,13 +204,13 @@ func EditBadgePost(ctx *context.Context) { // DeleteBadge response for deleting a badge func DeleteBadge(ctx *context.Context) { - b, err := user_model.GetBadge(ctx, ctx.PathParam("badge_slug")) + b, err := badges.GetBadge(ctx, ctx.PathParam("badge_slug")) if err != nil { ctx.ServerError("GetBadge", err) return } - if err = user_model.DeleteBadge(ctx, b); err != nil { + if err = badges.DeleteBadge(ctx, b); err != nil { ctx.ServerError("DeleteBadge", err) return } @@ -182,13 +221,14 @@ func DeleteBadge(ctx *context.Context) { ctx.Redirect(setting.AppSubURL + "/-/admin/badges") } +//nolint:dupl // these handlers share identical structure by design func BadgeUsers(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("admin.badges.users_with_badge", ctx.PathParam("badge_slug")) ctx.Data["PageIsAdminBadges"] = true page := max(ctx.FormInt("page"), 1) - badge := &user_model.Badge{Slug: ctx.PathParam("badge_slug")} + badge := &badges.Badge{Slug: ctx.PathParam("badge_slug")} opts := &user_model.GetBadgeUsersOptions{ ListOptions: db.ListOptions{ Page: page, @@ -224,7 +264,7 @@ func BadgeUsersPost(ctx *context.Context) { return } - if err = user_model.AddUserBadge(ctx, u, &user_model.Badge{Slug: ctx.PathParam("badge_slug")}); err != nil { + if err = user_model.AddUserBadge(ctx, u, &badges.Badge{Slug: ctx.PathParam("badge_slug")}); err != nil { if errors.Is(err, util.ErrNotExist) { ctx.Flash.Error(ctx.Tr("admin.badges.not_found")) ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) @@ -256,7 +296,7 @@ func DeleteBadgeUser(ctx *context.Context) { return } } - if err := user_model.RemoveUserBadge(ctx, user, &user_model.Badge{Slug: ctx.PathParam("badge_slug")}); err == nil { + if err := user_model.RemoveUserBadge(ctx, user, &badges.Badge{Slug: ctx.PathParam("badge_slug")}); err == nil { ctx.Flash.Success(ctx.Tr("admin.badges.user_remove_success")) } else { ctx.ServerError("RemoveUserBadge", err) @@ -266,12 +306,189 @@ func DeleteBadgeUser(ctx *context.Context) { ctx.JSONRedirect(badgeUsersURL) } -func RenderBadgeSearch(ctx *context.Context, opts *user_model.SearchBadgeOptions, tplName templates.TplName) { +func BadgeRepos(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("admin.badges.repos_with_badge", ctx.PathParam("badge_slug")) + ctx.Data["PageIsAdminBadges"] = true + + page := max(ctx.FormInt("page"), 1) + + badge := &badges.Badge{Slug: ctx.PathParam("badge_slug")} + opts := &repo_model.GetBadgeReposOptions{ + ListOptions: db.ListOptions{ + Page: page, + PageSize: setting.UI.Admin.UserPagingNum, + }, + BadgeSlug: badge.Slug, + } + repos, count, err := repo_model.GetBadgeRepos(ctx, opts) + if err != nil { + ctx.ServerError("GetBadgeRepos", err) + return + } + if err := repo_model.RepositoryList(repos).LoadOwners(ctx); err != nil { + ctx.ServerError("LoadOwners", err) + return + } + + ctx.Data["Repos"] = repos + ctx.Data["Total"] = count + ctx.Data["Page"] = context.NewPagination(count, setting.UI.Admin.UserPagingNum, page, 5) + + ctx.HTML(http.StatusOK, tplBadgeRepos) +} + +func BadgeReposPost(ctx *context.Context) { + repoFullName := strings.ToLower(ctx.FormString("repo")) + parts := strings.Split(repoFullName, "/") + if len(parts) != 2 { + ctx.Flash.Error(ctx.Tr("form.repo_not_exist")) + ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) + return + } + ownerName := parts[0] + repoName := parts[1] + + repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, ownerName, repoName) + if err != nil { + if repo_model.IsErrRepoNotExist(err) { + ctx.Flash.Error(ctx.Tr("form.repo_not_exist")) + ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) + } else { + ctx.ServerError("GetRepositoryByOwnerAndName", err) + } + return + } + + if err = repo_model.AddRepoBadge(ctx, repo, &badges.Badge{Slug: ctx.PathParam("badge_slug")}); err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.Flash.Error(ctx.Tr("admin.badges.not_found")) + ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) + } else if errors.Is(err, util.ErrAlreadyExist) { + ctx.Flash.Error(ctx.Tr("admin.badges.repo_already_has")) + ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) + } else { + ctx.ServerError("AddRepoBadge", err) + } + return + } + + ctx.Flash.Success(ctx.Tr("admin.badges.repo_add_success")) + ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) +} + +func DeleteBadgeRepo(ctx *context.Context) { + badgeReposURL := setting.AppSubURL + "/-/admin/badges/slug/" + url.PathEscape(ctx.PathParam("badge_slug")) + "/repos" + + repo, err := repo_model.GetRepositoryByID(ctx, ctx.FormInt64("id")) + if err != nil { + if repo_model.IsErrRepoNotExist(err) { + ctx.Flash.Error(ctx.Tr("form.repo_not_exist")) + ctx.JSONRedirect(badgeReposURL) + return + } else { + ctx.ServerError("GetRepositoryByID", err) + return + } + } + if err := repo_model.RemoveRepoBadge(ctx, repo, &badges.Badge{Slug: ctx.PathParam("badge_slug")}); err == nil { + ctx.Flash.Success(ctx.Tr("admin.badges.repo_remove_success")) + } else { + ctx.ServerError("RemoveRepoBadge", err) + return + } + + ctx.JSONRedirect(badgeReposURL) +} + +//nolint:dupl // these handlers share identical structure by design +func BadgeOrgs(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("admin.badges.orgs_with_badge", ctx.PathParam("badge_slug")) + ctx.Data["PageIsAdminBadges"] = true + + page := max(ctx.FormInt("page"), 1) + + badge := &badges.Badge{Slug: ctx.PathParam("badge_slug")} + opts := &org_model.GetBadgeOrgsOptions{ + ListOptions: db.ListOptions{ + Page: page, + PageSize: setting.UI.Admin.UserPagingNum, + }, + BadgeSlug: badge.Slug, + } + orgs, count, err := org_model.GetBadgeOrgs(ctx, opts) + if err != nil { + ctx.ServerError("GetBadgeOrgs", err) + return + } + + ctx.Data["Orgs"] = orgs + ctx.Data["Total"] = count + ctx.Data["Page"] = context.NewPagination(count, setting.UI.Admin.UserPagingNum, page, 5) + + ctx.HTML(http.StatusOK, tplBadgeOrgs) +} + +func BadgeOrgsPost(ctx *context.Context) { + name := strings.ToLower(ctx.FormString("org")) + + org, err := org_model.GetOrgByName(ctx, name) + if err != nil { + if org_model.IsErrOrgNotExist(err) { + ctx.Flash.Error(ctx.Tr("form.org_not_exist")) + ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) + } else { + ctx.ServerError("GetOrgByName", err) + } + return + } + + if err = org_model.AddOrgBadge(ctx, org, &badges.Badge{Slug: ctx.PathParam("badge_slug")}); err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.Flash.Error(ctx.Tr("admin.badges.not_found")) + ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) + } else if errors.Is(err, util.ErrAlreadyExist) { + ctx.Flash.Error(ctx.Tr("admin.badges.org_already_has")) + ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) + } else { + ctx.ServerError("AddOrgBadge", err) + } + return + } + + ctx.Flash.Success(ctx.Tr("admin.badges.org_add_success")) + ctx.Redirect(setting.AppSubURL + ctx.Req.URL.EscapedPath()) +} + +func DeleteBadgeOrg(ctx *context.Context) { + badgeOrgsURL := setting.AppSubURL + "/-/admin/badges/slug/" + url.PathEscape(ctx.PathParam("badge_slug")) + "/orgs" + + org, err := org_model.GetOrgByID(ctx, ctx.FormInt64("id")) + if err != nil { + if org_model.IsErrOrgNotExist(err) { + ctx.Flash.Error(ctx.Tr("form.org_not_exist")) + ctx.JSONRedirect(badgeOrgsURL) + return + } else { + ctx.ServerError("GetOrgByID", err) + return + } + } + if err := org_model.RemoveOrgBadge(ctx, org, &badges.Badge{Slug: ctx.PathParam("badge_slug")}); err == nil { + ctx.Flash.Success(ctx.Tr("admin.badges.org_remove_success")) + } else { + ctx.ServerError("RemoveOrgBadge", err) + return + } + + ctx.JSONRedirect(badgeOrgsURL) +} + +func RenderBadgeSearch(ctx *context.Context, opts *badges.SearchBadgeOptions, tplName templates.TplName) { var ( - badges []*user_model.Badge - count int64 - err error - orderBy db.SearchOrderBy + badgesList []*badges.Badge + count int64 + err error + orderBy db.SearchOrderBy ) sortOrder := ctx.FormString("sort") @@ -298,7 +515,7 @@ func RenderBadgeSearch(ctx *context.Context, opts *user_model.SearchBadgeOptions opts.Keyword = ctx.FormTrim("q") opts.OrderBy = orderBy if len(opts.Keyword) == 0 || isKeywordValid(opts.Keyword) { - badges, count, err = user_model.SearchBadges(ctx, opts) + badgesList, count, err = badges.SearchBadges(ctx, opts) if err != nil { ctx.ServerError("SearchBadges", err) return @@ -307,7 +524,7 @@ func RenderBadgeSearch(ctx *context.Context, opts *user_model.SearchBadgeOptions ctx.Data["Keyword"] = opts.Keyword ctx.Data["Total"] = count - ctx.Data["Badges"] = badges + ctx.Data["Badges"] = badgesList pager := context.NewPagination(count, opts.PageSize, opts.Page, 5) pager.AddParamFromRequest(ctx.Req) diff --git a/routers/web/explore/repo.go b/routers/web/explore/repo.go index 2399aa588061e..2c5cace40fe4c 100644 --- a/routers/web/explore/repo.go +++ b/routers/web/explore/repo.go @@ -132,6 +132,10 @@ func RenderRepoSearch(ctx *context.Context, opts *RepoSearchOptions) { return } + if err := repo_model.RepositoryList(repos).LoadBadges(ctx); err != nil { + log.Error("Failed loading badges: %v", err) + } + ctx.Data["Keyword"] = keyword ctx.Data["Total"] = count ctx.Data["Repos"] = repos diff --git a/routers/web/explore/user.go b/routers/web/explore/user.go index 64e2a92d68542..58a5679ca2ddf 100644 --- a/routers/web/explore/user.go +++ b/routers/web/explore/user.go @@ -8,6 +8,7 @@ import ( "net/http" "gitea.dev/models/db" + org_model "gitea.dev/models/organization" user_model "gitea.dev/models/user" "gitea.dev/modules/container" "gitea.dev/modules/log" @@ -95,6 +96,17 @@ func RenderUserSearch(ctx *context.Context, opts user_model.SearchUserOptions, t ctx.ServerError("SearchUsers", err) return } + for _, u := range users { + if u.IsOrganization() { + if badges, _, err := org_model.GetOrgBadges(ctx, (*org_model.Organization)(u)); err == nil { + u.Badges = badges + } + } else { + if badges, _, err := user_model.GetUserBadges(ctx, u); err == nil { + u.Badges = badges + } + } + } } if isSitemap { m := sitemap.NewSitemap() diff --git a/routers/web/org/home.go b/routers/web/org/home.go index 7c52455d31db5..d503dfb38109a 100644 --- a/routers/web/org/home.go +++ b/routers/web/org/home.go @@ -52,6 +52,13 @@ func home(ctx *context.Context, viewRepositories bool) { ctx.Data["PageIsUserProfile"] = true ctx.Data["Title"] = org.DisplayName() + badges, _, err := organization.GetOrgBadges(ctx, org) + if err != nil { + ctx.ServerError("GetOrgBadges", err) + return + } + ctx.Data["Badges"] = badges + var orderBy db.SearchOrderBy sortOrder := ctx.FormString("sort") if _, ok := repo_model.OrderByFlatMap[sortOrder]; !ok { diff --git a/routers/web/web.go b/routers/web/web.go index 0bfae44e869ad..921b1a6a459b9 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -808,6 +808,10 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { m.Post("/slug/{badge_slug}/delete", admin.DeleteBadge) m.Combo("/slug/{badge_slug}/users").Get(admin.BadgeUsers).Post(admin.BadgeUsersPost) m.Post("/slug/{badge_slug}/users/delete", admin.DeleteBadgeUser) + m.Combo("/slug/{badge_slug}/repos").Get(admin.BadgeRepos).Post(admin.BadgeReposPost) + m.Post("/slug/{badge_slug}/repos/delete", admin.DeleteBadgeRepo) + m.Combo("/slug/{badge_slug}/orgs").Get(admin.BadgeOrgs).Post(admin.BadgeOrgsPost) + m.Post("/slug/{badge_slug}/orgs/delete", admin.DeleteBadgeOrg) }) m.Group("/emails", func() { diff --git a/services/context/repo.go b/services/context/repo.go index 81df87a67f4be..54113672d80f5 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -678,6 +678,11 @@ func repoAssignmentPrepareTemplateData(ctx *Context, data *repoAssignmentPrepare return } } + + if err := repo_model.RepositoryList([]*repo_model.Repository{repo}).LoadBadges(ctx); err != nil { + ctx.ServerError("LoadBadges", err) + return + } } func repoAssignmentIsHomeOrSettings(ctx *Context, data *repoAssignmentPrepareDataStruct) bool { diff --git a/templates/admin/badge/orgs.tmpl b/templates/admin/badge/orgs.tmpl new file mode 100644 index 0000000000000..ad3b28996e752 --- /dev/null +++ b/templates/admin/badge/orgs.tmpl @@ -0,0 +1,40 @@ +{{template "admin/layout_head" (dict "pageClass" "admin badge")}} +
+

+ {{.Title}} +

+
+
+ + +
+
+ {{if .Orgs}} +
+
+ {{range .Orgs}} +
+ +
+
+ {{template "shared/user/name" .}} +
+
+ +
+ {{end}} +
+
+ {{end}} + {{template "base/paginate" .}} +
+ +{{template "admin/layout_footer" .}} diff --git a/templates/admin/badge/repos.tmpl b/templates/admin/badge/repos.tmpl new file mode 100644 index 0000000000000..410f37693aa9a --- /dev/null +++ b/templates/admin/badge/repos.tmpl @@ -0,0 +1,40 @@ +{{template "admin/layout_head" (dict "pageClass" "admin badge")}} +
+

+ {{.Title}} +

+
+
+ + +
+
+ {{if .Repos}} +
+ +
+ {{end}} + {{template "base/paginate" .}} +
+ +{{template "admin/layout_footer" .}} diff --git a/templates/admin/badge/view.tmpl b/templates/admin/badge/view.tmpl index 26d3d8dbe632f..aa7d568fffb78 100644 --- a/templates/admin/badge/view.tmpl +++ b/templates/admin/badge/view.tmpl @@ -39,6 +39,26 @@
{{template "explore/user_list" .}}
+ +

+ {{ctx.Locale.Tr "explore.repos"}} ({{.ReposTotal}}) +
+ {{ctx.Locale.Tr "admin.badges.manage_repos"}} +
+

+
+ {{template "shared/repo/list" .}} +
+ +

+ {{ctx.Locale.Tr "explore.organizations"}} ({{.OrgsTotal}}) +
+ {{ctx.Locale.Tr "admin.badges.manage_orgs"}} +
+

+
+ {{template "explore/user_list" dict "Users" .Orgs}} +
{{template "admin/layout_footer" .}} diff --git a/templates/explore/user_list.tmpl b/templates/explore/user_list.tmpl index e9248bd549556..623a8058dad06 100644 --- a/templates/explore/user_list.tmpl +++ b/templates/explore/user_list.tmpl @@ -7,6 +7,7 @@
{{template "shared/user/name" .}} + {{template "shared/badges" .Badges}} {{if .Visibility.IsPrivate}} {{ctx.Locale.Tr "repo.desc.private"}} {{end}} diff --git a/templates/org/header.tmpl b/templates/org/header.tmpl index 4f9e54b610924..5dafea92984ac 100644 --- a/templates/org/header.tmpl +++ b/templates/org/header.tmpl @@ -4,6 +4,7 @@
{{.Org.DisplayName}} + {{template "shared/badges" .Badges}} {{if .Org.Visibility.IsLimited}}{{ctx.Locale.Tr "org.settings.visibility.limited_shortname"}}{{end}} {{if .Org.Visibility.IsPrivate}}{{ctx.Locale.Tr "org.settings.visibility.private_shortname"}}{{end}}
diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl index c522bf38655ab..1965f0609a4c8 100644 --- a/templates/repo/header.tmpl +++ b/templates/repo/header.tmpl @@ -7,6 +7,7 @@ {{template "repo/icon" .}}
{{.Owner.Name}}/{{.Name}} + {{template "shared/badges" .Badges}}
{{if .IsArchived}} diff --git a/templates/shared/badges.tmpl b/templates/shared/badges.tmpl new file mode 100644 index 0000000000000..e5ec84fb9faef --- /dev/null +++ b/templates/shared/badges.tmpl @@ -0,0 +1,13 @@ +{{if .}} + + {{range .}} + {{if .ImageURL}} + {{.Description}} + {{else}} + + {{.Slug}} + + {{end}} + {{end}} + +{{end}} diff --git a/templates/shared/repo/list.tmpl b/templates/shared/repo/list.tmpl index 0b84aa0812ee2..18f7a3d19b089 100644 --- a/templates/shared/repo/list.tmpl +++ b/templates/shared/repo/list.tmpl @@ -16,6 +16,7 @@ {{end}} {{.Name}} + {{template "shared/badges" .Badges}} {{if .IsArchived}} {{ctx.Locale.Tr "repo.desc.archived"}} {{end}} diff --git a/templates/shared/user/profile_big_avatar.tmpl b/templates/shared/user/profile_big_avatar.tmpl index b70013c7f0eaa..3983ba0d2761c 100644 --- a/templates/shared/user/profile_big_avatar.tmpl +++ b/templates/shared/user/profile_big_avatar.tmpl @@ -101,15 +101,7 @@ {{if .Badges}}
  • - {{range .Badges}} - - {{if .ImageURL}} - {{.Description}} - {{else}} - {{.Slug}} - {{end}} - - {{end}} + {{template "shared/badges" .Badges}}
  • {{end}}