close #10962 Adds `GET /api/v1/repos/{owner}/{repo}/issues/{index}/subscriptions/check` -> return a `WachInfo`tags/v1.21.12.1
| @@ -0,0 +1,66 @@ | |||||
| // Copyright 2020 The Gitea Authors. All rights reserved. | |||||
| // Use of this source code is governed by a MIT-style | |||||
| // license that can be found in the LICENSE file. | |||||
| package integrations | |||||
| import ( | |||||
| "fmt" | |||||
| "net/http" | |||||
| "testing" | |||||
| "code.gitea.io/gitea/models" | |||||
| api "code.gitea.io/gitea/modules/structs" | |||||
| "github.com/stretchr/testify/assert" | |||||
| ) | |||||
| func TestAPIIssueSubscriptions(t *testing.T) { | |||||
| defer prepareTestEnv(t)() | |||||
| issue1 := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 1}).(*models.Issue) | |||||
| issue2 := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 2}).(*models.Issue) | |||||
| issue3 := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 3}).(*models.Issue) | |||||
| issue4 := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 4}).(*models.Issue) | |||||
| issue5 := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 8}).(*models.Issue) | |||||
| owner := models.AssertExistsAndLoadBean(t, &models.User{ID: issue1.PosterID}).(*models.User) | |||||
| session := loginUser(t, owner.Name) | |||||
| token := getTokenForLoggedInUser(t, session) | |||||
| testSubscription := func(issue *models.Issue, isWatching bool) { | |||||
| issueRepo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: issue.RepoID}).(*models.Repository) | |||||
| urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/subscriptions/check?token=%s", issueRepo.OwnerName, issueRepo.Name, issue.Index, token) | |||||
| req := NewRequest(t, "GET", urlStr) | |||||
| resp := session.MakeRequest(t, req, http.StatusOK) | |||||
| wi := new(api.WatchInfo) | |||||
| DecodeJSON(t, resp, wi) | |||||
| assert.EqualValues(t, isWatching, wi.Subscribed) | |||||
| assert.EqualValues(t, !isWatching, wi.Ignored) | |||||
| assert.EqualValues(t, issue.APIURL()+"/subscriptions", wi.URL) | |||||
| assert.EqualValues(t, issue.CreatedUnix, wi.CreatedAt.Unix()) | |||||
| assert.EqualValues(t, issueRepo.APIURL(), wi.RepositoryURL) | |||||
| } | |||||
| testSubscription(issue1, true) | |||||
| testSubscription(issue2, true) | |||||
| testSubscription(issue3, true) | |||||
| testSubscription(issue4, false) | |||||
| testSubscription(issue5, false) | |||||
| issue1Repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: issue1.RepoID}).(*models.Repository) | |||||
| urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/subscriptions/%s?token=%s", issue1Repo.OwnerName, issue1Repo.Name, issue1.Index, owner.Name, token) | |||||
| req := NewRequest(t, "DELETE", urlStr) | |||||
| session.MakeRequest(t, req, http.StatusCreated) | |||||
| testSubscription(issue1, false) | |||||
| issue5Repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: issue5.RepoID}).(*models.Repository) | |||||
| urlStr = fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/subscriptions/%s?token=%s", issue5Repo.OwnerName, issue5Repo.Name, issue5.Index, owner.Name, token) | |||||
| req = NewRequest(t, "PUT", urlStr) | |||||
| session.MakeRequest(t, req, http.StatusCreated) | |||||
| testSubscription(issue5, true) | |||||
| } | |||||
| @@ -332,6 +332,13 @@ func (issue *Issue) GetIsRead(userID int64) error { | |||||
| // APIURL returns the absolute APIURL to this issue. | // APIURL returns the absolute APIURL to this issue. | ||||
| func (issue *Issue) APIURL() string { | func (issue *Issue) APIURL() string { | ||||
| if issue.Repo == nil { | |||||
| err := issue.LoadRepo() | |||||
| if err != nil { | |||||
| log.Error("Issue[%d].APIURL(): %v", issue.ID, err) | |||||
| return "" | |||||
| } | |||||
| } | |||||
| return fmt.Sprintf("%s/issues/%d", issue.Repo.APIURL(), issue.Index) | return fmt.Sprintf("%s/issues/%d", issue.Repo.APIURL(), issue.Index) | ||||
| } | } | ||||
| @@ -64,6 +64,23 @@ func getIssueWatch(e Engine, userID, issueID int64) (iw *IssueWatch, exists bool | |||||
| return | return | ||||
| } | } | ||||
| // CheckIssueWatch check if an user is watching an issue | |||||
| // it takes participants and repo watch into account | |||||
| func CheckIssueWatch(user *User, issue *Issue) (bool, error) { | |||||
| iw, exist, err := getIssueWatch(x, user.ID, issue.ID) | |||||
| if err != nil { | |||||
| return false, err | |||||
| } | |||||
| if exist { | |||||
| return iw.IsWatching, nil | |||||
| } | |||||
| w, err := getWatch(x, user.ID, issue.RepoID) | |||||
| if err != nil { | |||||
| return false, err | |||||
| } | |||||
| return isWatchMode(w.Mode) || IsUserParticipantsOfIssue(user, issue), nil | |||||
| } | |||||
| // GetIssueWatchersIDs returns IDs of subscribers or explicit unsubscribers to a given issue id | // GetIssueWatchersIDs returns IDs of subscribers or explicit unsubscribers to a given issue id | ||||
| // but avoids joining with `user` for performance reasons | // but avoids joining with `user` for performance reasons | ||||
| // User permissions must be verified elsewhere if required | // User permissions must be verified elsewhere if required | ||||
| @@ -735,6 +735,7 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||
| }) | }) | ||||
| m.Group("/subscriptions", func() { | m.Group("/subscriptions", func() { | ||||
| m.Get("", repo.GetIssueSubscribers) | m.Get("", repo.GetIssueSubscribers) | ||||
| m.Get("/check", reqToken(), repo.CheckIssueSubscription) | |||||
| m.Put("/:user", reqToken(), repo.AddIssueSubscription) | m.Put("/:user", reqToken(), repo.AddIssueSubscription) | ||||
| m.Delete("/:user", reqToken(), repo.DelIssueSubscription) | m.Delete("/:user", reqToken(), repo.DelIssueSubscription) | ||||
| }) | }) | ||||
| @@ -9,6 +9,7 @@ import ( | |||||
| "code.gitea.io/gitea/models" | "code.gitea.io/gitea/models" | ||||
| "code.gitea.io/gitea/modules/context" | "code.gitea.io/gitea/modules/context" | ||||
| api "code.gitea.io/gitea/modules/structs" | |||||
| "code.gitea.io/gitea/routers/api/v1/utils" | "code.gitea.io/gitea/routers/api/v1/utils" | ||||
| ) | ) | ||||
| @@ -133,6 +134,64 @@ func setIssueSubscription(ctx *context.APIContext, watch bool) { | |||||
| ctx.Status(http.StatusCreated) | ctx.Status(http.StatusCreated) | ||||
| } | } | ||||
| // CheckIssueSubscription check if user is subscribed to an issue | |||||
| func CheckIssueSubscription(ctx *context.APIContext) { | |||||
| // swagger:operation GET /repos/{owner}/{repo}/issues/{index}/subscriptions/check issue issueCheckSubscription | |||||
| // --- | |||||
| // summary: Check if user is subscribed to an issue | |||||
| // consumes: | |||||
| // - application/json | |||||
| // produces: | |||||
| // - application/json | |||||
| // parameters: | |||||
| // - name: owner | |||||
| // in: path | |||||
| // description: owner of the repo | |||||
| // type: string | |||||
| // required: true | |||||
| // - name: repo | |||||
| // in: path | |||||
| // description: name of the repo | |||||
| // type: string | |||||
| // required: true | |||||
| // - name: index | |||||
| // in: path | |||||
| // description: index of the issue | |||||
| // type: integer | |||||
| // format: int64 | |||||
| // required: true | |||||
| // responses: | |||||
| // "200": | |||||
| // "$ref": "#/responses/WatchInfo" | |||||
| // "404": | |||||
| // "$ref": "#/responses/notFound" | |||||
| issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) | |||||
| if err != nil { | |||||
| if models.IsErrIssueNotExist(err) { | |||||
| ctx.NotFound() | |||||
| } else { | |||||
| ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err) | |||||
| } | |||||
| return | |||||
| } | |||||
| watching, err := models.CheckIssueWatch(ctx.User, issue) | |||||
| if err != nil { | |||||
| ctx.InternalServerError(err) | |||||
| return | |||||
| } | |||||
| ctx.JSON(http.StatusOK, api.WatchInfo{ | |||||
| Subscribed: watching, | |||||
| Ignored: !watching, | |||||
| Reason: nil, | |||||
| CreatedAt: issue.CreatedUnix.AsTime(), | |||||
| URL: issue.APIURL() + "/subscriptions", | |||||
| RepositoryURL: ctx.Repo.Repository.APIURL(), | |||||
| }) | |||||
| } | |||||
| // GetIssueSubscribers return subscribers of an issue | // GetIssueSubscribers return subscribers of an issue | ||||
| func GetIssueSubscribers(ctx *context.APIContext) { | func GetIssueSubscribers(ctx *context.APIContext) { | ||||
| // swagger:operation GET /repos/{owner}/{repo}/issues/{index}/subscriptions issue issueSubscriptions | // swagger:operation GET /repos/{owner}/{repo}/issues/{index}/subscriptions issue issueSubscriptions | ||||
| @@ -9,7 +9,6 @@ import ( | |||||
| "code.gitea.io/gitea/models" | "code.gitea.io/gitea/models" | ||||
| "code.gitea.io/gitea/modules/context" | "code.gitea.io/gitea/modules/context" | ||||
| "code.gitea.io/gitea/modules/setting" | |||||
| api "code.gitea.io/gitea/modules/structs" | api "code.gitea.io/gitea/modules/structs" | ||||
| "code.gitea.io/gitea/routers/api/v1/utils" | "code.gitea.io/gitea/routers/api/v1/utils" | ||||
| ) | ) | ||||
| @@ -124,7 +123,7 @@ func IsWatching(ctx *context.APIContext) { | |||||
| Reason: nil, | Reason: nil, | ||||
| CreatedAt: ctx.Repo.Repository.CreatedUnix.AsTime(), | CreatedAt: ctx.Repo.Repository.CreatedUnix.AsTime(), | ||||
| URL: subscriptionURL(ctx.Repo.Repository), | URL: subscriptionURL(ctx.Repo.Repository), | ||||
| RepositoryURL: repositoryURL(ctx.Repo.Repository), | |||||
| RepositoryURL: ctx.Repo.Repository.APIURL(), | |||||
| }) | }) | ||||
| } else { | } else { | ||||
| ctx.NotFound() | ctx.NotFound() | ||||
| @@ -162,7 +161,7 @@ func Watch(ctx *context.APIContext) { | |||||
| Reason: nil, | Reason: nil, | ||||
| CreatedAt: ctx.Repo.Repository.CreatedUnix.AsTime(), | CreatedAt: ctx.Repo.Repository.CreatedUnix.AsTime(), | ||||
| URL: subscriptionURL(ctx.Repo.Repository), | URL: subscriptionURL(ctx.Repo.Repository), | ||||
| RepositoryURL: repositoryURL(ctx.Repo.Repository), | |||||
| RepositoryURL: ctx.Repo.Repository.APIURL(), | |||||
| }) | }) | ||||
| } | } | ||||
| @@ -197,10 +196,5 @@ func Unwatch(ctx *context.APIContext) { | |||||
| // subscriptionURL returns the URL of the subscription API endpoint of a repo | // subscriptionURL returns the URL of the subscription API endpoint of a repo | ||||
| func subscriptionURL(repo *models.Repository) string { | func subscriptionURL(repo *models.Repository) string { | ||||
| return repositoryURL(repo) + "/subscription" | |||||
| } | |||||
| // repositoryURL returns the URL of the API endpoint of a repo | |||||
| func repositoryURL(repo *models.Repository) string { | |||||
| return setting.AppURL + "api/v1/" + repo.FullName() | |||||
| return repo.APIURL() + "/subscription" | |||||
| } | } | ||||
| @@ -749,21 +749,15 @@ func ViewIssue(ctx *context.Context) { | |||||
| ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title) | ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title) | ||||
| var iw *models.IssueWatch | |||||
| var exists bool | |||||
| iw := new(models.IssueWatch) | |||||
| if ctx.User != nil { | if ctx.User != nil { | ||||
| iw, exists, err = models.GetIssueWatch(ctx.User.ID, issue.ID) | |||||
| iw.UserID = ctx.User.ID | |||||
| iw.IssueID = issue.ID | |||||
| iw.IsWatching, err = models.CheckIssueWatch(ctx.User, issue) | |||||
| if err != nil { | if err != nil { | ||||
| ctx.ServerError("GetIssueWatch", err) | |||||
| ctx.InternalServerError(err) | |||||
| return | return | ||||
| } | } | ||||
| if !exists { | |||||
| iw = &models.IssueWatch{ | |||||
| UserID: ctx.User.ID, | |||||
| IssueID: issue.ID, | |||||
| IsWatching: models.IsWatching(ctx.User.ID, ctx.Repo.Repository.ID) || models.IsUserParticipantsOfIssue(ctx.User, issue), | |||||
| } | |||||
| } | |||||
| } | } | ||||
| ctx.Data["IssueWatch"] = iw | ctx.Data["IssueWatch"] = iw | ||||
| @@ -5217,6 +5217,53 @@ | |||||
| } | } | ||||
| } | } | ||||
| }, | }, | ||||
| "/repos/{owner}/{repo}/issues/{index}/subscriptions/check": { | |||||
| "get": { | |||||
| "consumes": [ | |||||
| "application/json" | |||||
| ], | |||||
| "produces": [ | |||||
| "application/json" | |||||
| ], | |||||
| "tags": [ | |||||
| "issue" | |||||
| ], | |||||
| "summary": "Check if user is subscribed to an issue", | |||||
| "operationId": "issueCheckSubscription", | |||||
| "parameters": [ | |||||
| { | |||||
| "type": "string", | |||||
| "description": "owner of the repo", | |||||
| "name": "owner", | |||||
| "in": "path", | |||||
| "required": true | |||||
| }, | |||||
| { | |||||
| "type": "string", | |||||
| "description": "name of the repo", | |||||
| "name": "repo", | |||||
| "in": "path", | |||||
| "required": true | |||||
| }, | |||||
| { | |||||
| "type": "integer", | |||||
| "format": "int64", | |||||
| "description": "index of the issue", | |||||
| "name": "index", | |||||
| "in": "path", | |||||
| "required": true | |||||
| } | |||||
| ], | |||||
| "responses": { | |||||
| "200": { | |||||
| "$ref": "#/responses/WatchInfo" | |||||
| }, | |||||
| "404": { | |||||
| "$ref": "#/responses/notFound" | |||||
| } | |||||
| } | |||||
| } | |||||
| }, | |||||
| "/repos/{owner}/{repo}/issues/{index}/subscriptions/{user}": { | "/repos/{owner}/{repo}/issues/{index}/subscriptions/{user}": { | ||||
| "put": { | "put": { | ||||
| "consumes": [ | "consumes": [ | ||||