Add issue subscription check to API (#10967)
close #10962 Adds `GET /api/v1/repos/{owner}/{repo}/issues/{index}/subscriptions/check` -> return a `WachInfo`
This commit is contained in:
parent
33176e8d27
commit
bb4261a5ed
8 changed files with 205 additions and 20 deletions
66
integrations/api_issue_subscription_test.go
Normal file
66
integrations/api_issue_subscription_test.go
Normal file
|
@ -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"
|
return repo.APIURL() + "/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()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
iw := new(models.IssueWatch)
|
||||||
var exists bool
|
|
||||||
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": [
|
||||||
|
|
Reference in a new issue