aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--models/migrations/migrations.go2
-rw-r--r--models/migrations/v1_22/v287.go46
-rw-r--r--models/migrations/v1_22/v287_test.go57
-rw-r--r--models/user/badge.go85
-rw-r--r--modules/structs/user.go31
-rw-r--r--routers/api/v1/admin/user_badge.go124
-rw-r--r--routers/api/v1/api.go3
-rw-r--r--routers/api/v1/swagger/options.go6
-rw-r--r--templates/swagger/v1_json.tmpl171
9 files changed, 523 insertions, 2 deletions
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index beb1f3bb96..516eb53f62 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -558,6 +558,8 @@ var migrations = []Migration{
NewMigration("Add PreviousDuration to ActionRun", v1_22.AddPreviousDurationToActionRun),
// v286 -> v287
NewMigration("Add support for SHA256 git repositories", v1_22.AdjustDBForSha256),
+ // v287 -> v288
+ NewMigration("Use Slug instead of ID for Badges", v1_22.UseSlugInsteadOfIDForBadges),
}
// GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v1_22/v287.go b/models/migrations/v1_22/v287.go
new file mode 100644
index 0000000000..c8b1593286
--- /dev/null
+++ b/models/migrations/v1_22/v287.go
@@ -0,0 +1,46 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_22 //nolint
+
+import (
+ "xorm.io/xorm"
+)
+
+type BadgeUnique struct {
+ ID int64 `xorm:"pk autoincr"`
+ Slug string `xorm:"UNIQUE"`
+}
+
+func (BadgeUnique) TableName() string {
+ return "badge"
+}
+
+func UseSlugInsteadOfIDForBadges(x *xorm.Engine) error {
+ type Badge struct {
+ Slug string
+ }
+
+ err := x.Sync(new(Badge))
+ if err != nil {
+ return err
+ }
+
+ sess := x.NewSession()
+ defer sess.Close()
+ if err := sess.Begin(); err != nil {
+ return err
+ }
+
+ _, err = sess.Exec("UPDATE `badge` SET `slug` = `id` Where `slug` IS NULL")
+ if err != nil {
+ return err
+ }
+
+ err = sess.Sync(new(BadgeUnique))
+ if err != nil {
+ return err
+ }
+
+ return sess.Commit()
+}
diff --git a/models/migrations/v1_22/v287_test.go b/models/migrations/v1_22/v287_test.go
new file mode 100644
index 0000000000..19c7ae3b91
--- /dev/null
+++ b/models/migrations/v1_22/v287_test.go
@@ -0,0 +1,57 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_22 //nolint
+
+import (
+ "fmt"
+ "testing"
+
+ "code.gitea.io/gitea/models/migrations/base"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func Test_UpdateBadgeColName(t *testing.T) {
+ type Badge struct {
+ ID int64 `xorm:"pk autoincr"`
+ Description string
+ ImageURL string
+ }
+
+ // Prepare and load the testing database
+ x, deferable := base.PrepareTestEnv(t, 0, new(BadgeUnique), new(Badge))
+ defer deferable()
+ if x == nil || t.Failed() {
+ return
+ }
+
+ oldBadges := []Badge{
+ {ID: 1, Description: "Test Badge 1", ImageURL: "https://example.com/badge1.png"},
+ {ID: 2, Description: "Test Badge 2", ImageURL: "https://example.com/badge2.png"},
+ {ID: 3, Description: "Test Badge 3", ImageURL: "https://example.com/badge3.png"},
+ }
+
+ for _, badge := range oldBadges {
+ _, err := x.Insert(&badge)
+ assert.NoError(t, err)
+ }
+
+ if err := UseSlugInsteadOfIDForBadges(x); err != nil {
+ assert.NoError(t, err)
+ return
+ }
+
+ got := []BadgeUnique{}
+ if err := x.Table("badge").Asc("id").Find(&got); !assert.NoError(t, err) {
+ return
+ }
+
+ for i, e := range oldBadges {
+ got := got[i]
+ assert.Equal(t, e.ID, got.ID)
+ assert.Equal(t, fmt.Sprintf("%d", e.ID), got.Slug)
+ }
+
+ // TODO: check if badges have been updated
+}
diff --git a/models/user/badge.go b/models/user/badge.go
index ee52b44cf5..3ff3530a36 100644
--- a/models/user/badge.go
+++ b/models/user/badge.go
@@ -5,13 +5,15 @@ package user
import (
"context"
+ "fmt"
"code.gitea.io/gitea/models/db"
)
// Badge represents a user badge
type Badge struct {
- ID int64 `xorm:"pk autoincr"`
+ ID int64 `xorm:"pk autoincr"`
+ Slug string `xorm:"UNIQUE"`
Description string
ImageURL string
}
@@ -39,3 +41,84 @@ func GetUserBadges(ctx context.Context, u *User) ([]*Badge, int64, error) {
count, err := sess.FindAndCount(&badges)
return badges, count, err
}
+
+// CreateBadge creates a new badge.
+func CreateBadge(ctx context.Context, badge *Badge) error {
+ _, err := db.GetEngine(ctx).Insert(badge)
+ return err
+}
+
+// GetBadge returns a badge
+func GetBadge(ctx context.Context, slug string) (*Badge, error) {
+ badge := new(Badge)
+ has, err := db.GetEngine(ctx).Where("slug=?", slug).Get(badge)
+ if !has {
+ return nil, err
+ }
+ return badge, err
+}
+
+// UpdateBadge updates a badge based on its slug.
+func UpdateBadge(ctx context.Context, badge *Badge) error {
+ _, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Update(badge)
+ return err
+}
+
+// DeleteBadge deletes a badge.
+func DeleteBadge(ctx context.Context, badge *Badge) error {
+ _, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Delete(badge)
+ return err
+}
+
+// AddUserBadge adds a badge to a user.
+func AddUserBadge(ctx context.Context, u *User, badge *Badge) error {
+ return AddUserBadges(ctx, u, []*Badge{badge})
+}
+
+// AddUserBadges adds badges to a user.
+func AddUserBadges(ctx context.Context, u *User, badges []*Badge) error {
+ return db.WithTx(ctx, func(ctx context.Context) error {
+ for _, badge := range badges {
+ // hydrate badge and check if it exists
+ has, err := db.GetEngine(ctx).Where("slug=?", badge.Slug).Get(badge)
+ if err != nil {
+ return err
+ } else if !has {
+ return fmt.Errorf("badge with slug %s doesn't exist", badge.Slug)
+ }
+ if err := db.Insert(ctx, &UserBadge{
+ BadgeID: badge.ID,
+ UserID: u.ID,
+ }); err != nil {
+ return err
+ }
+ }
+ return nil
+ })
+}
+
+// RemoveUserBadge removes a badge from a user.
+func RemoveUserBadge(ctx context.Context, u *User, badge *Badge) error {
+ return RemoveUserBadges(ctx, u, []*Badge{badge})
+}
+
+// RemoveUserBadges removes badges from a user.
+func RemoveUserBadges(ctx context.Context, u *User, badges []*Badge) error {
+ return db.WithTx(ctx, func(ctx context.Context) error {
+ for _, badge := range badges {
+ if _, err := db.GetEngine(ctx).
+ Join("INNER", "badge", "badge.id = `user_badge`.badge_id").
+ Where("`user_badge`.user_id=? AND `badge`.slug=?", u.ID, badge.Slug).
+ Delete(&UserBadge{}); err != nil {
+ return err
+ }
+ }
+ return nil
+ })
+}
+
+// RemoveAllUserBadges removes all badges from a user.
+func RemoveAllUserBadges(ctx context.Context, u *User) error {
+ _, err := db.GetEngine(ctx).Where("user_id=?", u.ID).Delete(&UserBadge{})
+ return err
+}
diff --git a/modules/structs/user.go b/modules/structs/user.go
index 0df67894b0..c43558be5d 100644
--- a/modules/structs/user.go
+++ b/modules/structs/user.go
@@ -1,4 +1,5 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package structs
@@ -108,3 +109,33 @@ type UpdateUserAvatarOption struct {
// image must be base64 encoded
Image string `json:"image" binding:"Required"`
}
+
+// Badge represents a user badge
+// swagger:model
+type Badge struct {
+ ID int64 `json:"id"`
+ Slug string `json:"slug"`
+ Description string `json:"description"`
+ ImageURL string `json:"image_url"`
+}
+
+// UserBadge represents a user badge
+// swagger:model
+type UserBadge struct {
+ ID int64 `json:"id"`
+ BadgeID int64 `json:"badge_id"`
+ UserID int64 `json:"user_id"`
+}
+
+// UserBadgeOption options for link between users and badges
+type UserBadgeOption struct {
+ // example: ["badge1","badge2"]
+ BadgeSlugs []string `json:"badge_slugs" binding:"Required"`
+}
+
+// BadgeList
+// swagger:response BadgeList
+type BadgeList struct {
+ // in:body
+ Body []Badge `json:"body"`
+}
diff --git a/routers/api/v1/admin/user_badge.go b/routers/api/v1/admin/user_badge.go
new file mode 100644
index 0000000000..bacd1f809b
--- /dev/null
+++ b/routers/api/v1/admin/user_badge.go
@@ -0,0 +1,124 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package admin
+
+import (
+ "net/http"
+
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/web"
+ "code.gitea.io/gitea/services/context"
+)
+
+// ListUserBadges lists all badges belonging to a user
+func ListUserBadges(ctx *context.APIContext) {
+ // swagger:operation GET /admin/users/{username}/badges admin adminListUserBadges
+ // ---
+ // summary: List a user's badges
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: username
+ // in: path
+ // description: username of user
+ // type: string
+ // required: true
+ // responses:
+ // "200":
+ // "$ref": "#/responses/BadgeList"
+ // "404":
+ // "$ref": "#/responses/notFound"
+
+ badges, maxResults, err := user_model.GetUserBadges(ctx, ctx.ContextUser)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "GetUserBadges", err)
+ return
+ }
+
+ ctx.SetTotalCountHeader(maxResults)
+ ctx.JSON(http.StatusOK, &badges)
+}
+
+// AddUserBadges add badges to a user
+func AddUserBadges(ctx *context.APIContext) {
+ // swagger:operation POST /admin/users/{username}/badges admin adminAddUserBadges
+ // ---
+ // summary: Add a badge to a user
+ // consumes:
+ // - application/json
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: username
+ // in: path
+ // description: username of user
+ // type: string
+ // required: true
+ // - name: body
+ // in: body
+ // schema:
+ // "$ref": "#/definitions/UserBadgeOption"
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+
+ form := web.GetForm(ctx).(*api.UserBadgeOption)
+ badges := prepareBadgesForReplaceOrAdd(ctx, *form)
+
+ if err := user_model.AddUserBadges(ctx, ctx.ContextUser, badges); err != nil {
+ ctx.Error(http.StatusInternalServerError, "ReplaceUserBadges", err)
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
+
+// DeleteUserBadges delete a badge from a user
+func DeleteUserBadges(ctx *context.APIContext) {
+ // swagger:operation DELETE /admin/users/{username}/badges admin adminDeleteUserBadges
+ // ---
+ // summary: Remove a badge from a user
+ // produces:
+ // - application/json
+ // parameters:
+ // - name: username
+ // in: path
+ // description: username of user
+ // type: string
+ // required: true
+ // - name: body
+ // in: body
+ // schema:
+ // "$ref": "#/definitions/UserBadgeOption"
+ // responses:
+ // "204":
+ // "$ref": "#/responses/empty"
+ // "403":
+ // "$ref": "#/responses/forbidden"
+ // "422":
+ // "$ref": "#/responses/validationError"
+
+ form := web.GetForm(ctx).(*api.UserBadgeOption)
+ badges := prepareBadgesForReplaceOrAdd(ctx, *form)
+
+ if err := user_model.RemoveUserBadges(ctx, ctx.ContextUser, badges); err != nil {
+ ctx.Error(http.StatusInternalServerError, "ReplaceUserBadges", err)
+ return
+ }
+
+ ctx.Status(http.StatusNoContent)
+}
+
+func prepareBadgesForReplaceOrAdd(ctx *context.APIContext, form api.UserBadgeOption) []*user_model.Badge {
+ badges := make([]*user_model.Badge, len(form.BadgeSlugs))
+ for i, badge := range form.BadgeSlugs {
+ badges[i] = &user_model.Badge{
+ Slug: badge,
+ }
+ }
+ return badges
+}
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index 0913571c27..1587d413f5 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -1519,6 +1519,9 @@ func Routes() *web.Route {
m.Post("/orgs", bind(api.CreateOrgOption{}), admin.CreateOrg)
m.Post("/repos", bind(api.CreateRepoOption{}), admin.CreateRepo)
m.Post("/rename", bind(api.RenameUserOption{}), admin.RenameUser)
+ m.Get("/badges", admin.ListUserBadges)
+ m.Post("/badges", bind(api.UserBadgeOption{}), admin.AddUserBadges)
+ m.Delete("/badges", bind(api.UserBadgeOption{}), admin.DeleteUserBadges)
}, context.UserAssignmentAPI())
})
m.Group("/emails", func() {
diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go
index 6f7859df62..e03862d7b9 100644
--- a/routers/api/v1/swagger/options.go
+++ b/routers/api/v1/swagger/options.go
@@ -190,4 +190,10 @@ type swaggerParameterBodies struct {
// in:body
CreateOrUpdateSecretOption api.CreateOrUpdateSecretOption
+
+ // in:body
+ UserBadgeOption api.UserBadgeOption
+
+ // in:body
+ UserBadgeList api.BadgeList
}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index b2bd1bf174..d4c5d9a7ee 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -689,6 +689,109 @@
}
}
},
+ "/admin/users/{username}/badges": {
+ "get": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "admin"
+ ],
+ "summary": "List a user's badges",
+ "operationId": "adminListUserBadges",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "username of user",
+ "name": "username",
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "200": {
+ "$ref": "#/responses/BadgeList"
+ },
+ "404": {
+ "$ref": "#/responses/notFound"
+ }
+ }
+ },
+ "post": {
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "admin"
+ ],
+ "summary": "Add a badge to a user",
+ "operationId": "adminAddUserBadges",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "username of user",
+ "name": "username",
+ "in": "path",
+ "required": true
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "schema": {
+ "$ref": "#/definitions/UserBadgeOption"
+ }
+ }
+ ],
+ "responses": {
+ "204": {
+ "$ref": "#/responses/empty"
+ },
+ "403": {
+ "$ref": "#/responses/forbidden"
+ }
+ }
+ },
+ "delete": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "admin"
+ ],
+ "summary": "Remove a badge from a user",
+ "operationId": "adminDeleteUserBadges",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "username of user",
+ "name": "username",
+ "in": "path",
+ "required": true
+ },
+ {
+ "name": "body",
+ "in": "body",
+ "schema": {
+ "$ref": "#/definitions/UserBadgeOption"
+ }
+ }
+ ],
+ "responses": {
+ "204": {
+ "$ref": "#/responses/empty"
+ },
+ "403": {
+ "$ref": "#/responses/forbidden"
+ },
+ "422": {
+ "$ref": "#/responses/validationError"
+ }
+ }
+ }
+ },
"/admin/users/{username}/keys": {
"post": {
"consumes": [
@@ -17003,6 +17106,45 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
+ "Badge": {
+ "description": "Badge represents a user badge",
+ "type": "object",
+ "properties": {
+ "description": {
+ "type": "string",
+ "x-go-name": "Description"
+ },
+ "id": {
+ "type": "integer",
+ "format": "int64",
+ "x-go-name": "ID"
+ },
+ "image_url": {
+ "type": "string",
+ "x-go-name": "ImageURL"
+ },
+ "slug": {
+ "type": "string",
+ "x-go-name": "Slug"
+ }
+ },
+ "x-go-package": "code.gitea.io/gitea/modules/structs"
+ },
+ "BadgeList": {
+ "description": "BadgeList",
+ "type": "object",
+ "properties": {
+ "body": {
+ "description": "in:body",
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/Badge"
+ },
+ "x-go-name": "Body"
+ }
+ },
+ "x-go-package": "code.gitea.io/gitea/modules/structs"
+ },
"Branch": {
"description": "Branch represents a repository branch",
"type": "object",
@@ -23047,6 +23189,24 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
+ "UserBadgeOption": {
+ "description": "UserBadgeOption options for link between users and badges",
+ "type": "object",
+ "properties": {
+ "badge_slugs": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "x-go-name": "BadgeSlugs",
+ "example": [
+ "badge1",
+ "badge2"
+ ]
+ }
+ },
+ "x-go-package": "code.gitea.io/gitea/modules/structs"
+ },
"UserHeatmapData": {
"description": "UserHeatmapData represents the data needed to create a heatmap",
"type": "object",
@@ -23336,6 +23496,15 @@
}
}
},
+ "BadgeList": {
+ "description": "BadgeList",
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/Badge"
+ }
+ }
+ },
"Branch": {
"description": "Branch",
"schema": {
@@ -24249,7 +24418,7 @@
"parameterBodies": {
"description": "parameterBodies",
"schema": {
- "$ref": "#/definitions/CreateOrUpdateSecretOption"
+ "$ref": "#/definitions/BadgeList"
}
},
"redirect": {