close #10962 Adds `GET /api/v1/repos/{owner}/{repo}/issues/{index}/subscriptions/check` -> return a `WachInfo`master
@@ -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": [ | |||