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. | |||
| 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) | |||
| } | |||
| @@ -64,6 +64,23 @@ func getIssueWatch(e Engine, userID, issueID int64) (iw *IssueWatch, exists bool | |||
| 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 | |||
| // but avoids joining with `user` for performance reasons | |||
| // User permissions must be verified elsewhere if required | |||
| @@ -735,6 +735,7 @@ func RegisterRoutes(m *macaron.Macaron) { | |||
| }) | |||
| m.Group("/subscriptions", func() { | |||
| m.Get("", repo.GetIssueSubscribers) | |||
| m.Get("/check", reqToken(), repo.CheckIssueSubscription) | |||
| m.Put("/:user", reqToken(), repo.AddIssueSubscription) | |||
| m.Delete("/:user", reqToken(), repo.DelIssueSubscription) | |||
| }) | |||
| @@ -9,6 +9,7 @@ import ( | |||
| "code.gitea.io/gitea/models" | |||
| "code.gitea.io/gitea/modules/context" | |||
| api "code.gitea.io/gitea/modules/structs" | |||
| "code.gitea.io/gitea/routers/api/v1/utils" | |||
| ) | |||
| @@ -133,6 +134,64 @@ func setIssueSubscription(ctx *context.APIContext, watch bool) { | |||
| 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 | |||
| func GetIssueSubscribers(ctx *context.APIContext) { | |||
| // 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/modules/context" | |||
| "code.gitea.io/gitea/modules/setting" | |||
| api "code.gitea.io/gitea/modules/structs" | |||
| "code.gitea.io/gitea/routers/api/v1/utils" | |||
| ) | |||
| @@ -124,7 +123,7 @@ func IsWatching(ctx *context.APIContext) { | |||
| Reason: nil, | |||
| CreatedAt: ctx.Repo.Repository.CreatedUnix.AsTime(), | |||
| URL: subscriptionURL(ctx.Repo.Repository), | |||
| RepositoryURL: repositoryURL(ctx.Repo.Repository), | |||
| RepositoryURL: ctx.Repo.Repository.APIURL(), | |||
| }) | |||
| } else { | |||
| ctx.NotFound() | |||
| @@ -162,7 +161,7 @@ func Watch(ctx *context.APIContext) { | |||
| Reason: nil, | |||
| CreatedAt: ctx.Repo.Repository.CreatedUnix.AsTime(), | |||
| 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 | |||
| 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) | |||
| var iw *models.IssueWatch | |||
| var exists bool | |||
| iw := new(models.IssueWatch) | |||
| 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 { | |||
| ctx.ServerError("GetIssueWatch", err) | |||
| ctx.InternalServerError(err) | |||
| 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 | |||
| @@ -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}": { | |||
| "put": { | |||
| "consumes": [ | |||