Skip to content

Commit bb4261a

Browse files
authored
Add issue subscription check to API (#10967)
close #10962 Adds `GET /api/v1​/repos​/{owner}​/{repo}​/issues​/{index}​/subscriptions​/check` -> return a `WachInfo`
1 parent 33176e8 commit bb4261a

File tree

8 files changed

+205
-20
lines changed

8 files changed

+205
-20
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright 2020 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package integrations
6+
7+
import (
8+
"fmt"
9+
"net/http"
10+
"testing"
11+
12+
"code.gitea.io/gitea/models"
13+
api "code.gitea.io/gitea/modules/structs"
14+
15+
"github.com/stretchr/testify/assert"
16+
)
17+
18+
func TestAPIIssueSubscriptions(t *testing.T) {
19+
defer prepareTestEnv(t)()
20+
21+
issue1 := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 1}).(*models.Issue)
22+
issue2 := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 2}).(*models.Issue)
23+
issue3 := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 3}).(*models.Issue)
24+
issue4 := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 4}).(*models.Issue)
25+
issue5 := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 8}).(*models.Issue)
26+
27+
owner := models.AssertExistsAndLoadBean(t, &models.User{ID: issue1.PosterID}).(*models.User)
28+
29+
session := loginUser(t, owner.Name)
30+
token := getTokenForLoggedInUser(t, session)
31+
32+
testSubscription := func(issue *models.Issue, isWatching bool) {
33+
34+
issueRepo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: issue.RepoID}).(*models.Repository)
35+
36+
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/subscriptions/check?token=%s", issueRepo.OwnerName, issueRepo.Name, issue.Index, token)
37+
req := NewRequest(t, "GET", urlStr)
38+
resp := session.MakeRequest(t, req, http.StatusOK)
39+
wi := new(api.WatchInfo)
40+
DecodeJSON(t, resp, wi)
41+
42+
assert.EqualValues(t, isWatching, wi.Subscribed)
43+
assert.EqualValues(t, !isWatching, wi.Ignored)
44+
assert.EqualValues(t, issue.APIURL()+"/subscriptions", wi.URL)
45+
assert.EqualValues(t, issue.CreatedUnix, wi.CreatedAt.Unix())
46+
assert.EqualValues(t, issueRepo.APIURL(), wi.RepositoryURL)
47+
}
48+
49+
testSubscription(issue1, true)
50+
testSubscription(issue2, true)
51+
testSubscription(issue3, true)
52+
testSubscription(issue4, false)
53+
testSubscription(issue5, false)
54+
55+
issue1Repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: issue1.RepoID}).(*models.Repository)
56+
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/subscriptions/%s?token=%s", issue1Repo.OwnerName, issue1Repo.Name, issue1.Index, owner.Name, token)
57+
req := NewRequest(t, "DELETE", urlStr)
58+
session.MakeRequest(t, req, http.StatusCreated)
59+
testSubscription(issue1, false)
60+
61+
issue5Repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: issue5.RepoID}).(*models.Repository)
62+
urlStr = fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/subscriptions/%s?token=%s", issue5Repo.OwnerName, issue5Repo.Name, issue5.Index, owner.Name, token)
63+
req = NewRequest(t, "PUT", urlStr)
64+
session.MakeRequest(t, req, http.StatusCreated)
65+
testSubscription(issue5, true)
66+
}

models/issue.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,13 @@ func (issue *Issue) GetIsRead(userID int64) error {
332332

333333
// APIURL returns the absolute APIURL to this issue.
334334
func (issue *Issue) APIURL() string {
335+
if issue.Repo == nil {
336+
err := issue.LoadRepo()
337+
if err != nil {
338+
log.Error("Issue[%d].APIURL(): %v", issue.ID, err)
339+
return ""
340+
}
341+
}
335342
return fmt.Sprintf("%s/issues/%d", issue.Repo.APIURL(), issue.Index)
336343
}
337344

models/issue_watch.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,23 @@ func getIssueWatch(e Engine, userID, issueID int64) (iw *IssueWatch, exists bool
6464
return
6565
}
6666

67+
// CheckIssueWatch check if an user is watching an issue
68+
// it takes participants and repo watch into account
69+
func CheckIssueWatch(user *User, issue *Issue) (bool, error) {
70+
iw, exist, err := getIssueWatch(x, user.ID, issue.ID)
71+
if err != nil {
72+
return false, err
73+
}
74+
if exist {
75+
return iw.IsWatching, nil
76+
}
77+
w, err := getWatch(x, user.ID, issue.RepoID)
78+
if err != nil {
79+
return false, err
80+
}
81+
return isWatchMode(w.Mode) || IsUserParticipantsOfIssue(user, issue), nil
82+
}
83+
6784
// GetIssueWatchersIDs returns IDs of subscribers or explicit unsubscribers to a given issue id
6885
// but avoids joining with `user` for performance reasons
6986
// User permissions must be verified elsewhere if required

routers/api/v1/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,7 @@ func RegisterRoutes(m *macaron.Macaron) {
735735
})
736736
m.Group("/subscriptions", func() {
737737
m.Get("", repo.GetIssueSubscribers)
738+
m.Get("/check", reqToken(), repo.CheckIssueSubscription)
738739
m.Put("/:user", reqToken(), repo.AddIssueSubscription)
739740
m.Delete("/:user", reqToken(), repo.DelIssueSubscription)
740741
})

routers/api/v1/repo/issue_subscription.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"code.gitea.io/gitea/models"
1111
"code.gitea.io/gitea/modules/context"
12+
api "code.gitea.io/gitea/modules/structs"
1213
"code.gitea.io/gitea/routers/api/v1/utils"
1314
)
1415

@@ -133,6 +134,64 @@ func setIssueSubscription(ctx *context.APIContext, watch bool) {
133134
ctx.Status(http.StatusCreated)
134135
}
135136

137+
// CheckIssueSubscription check if user is subscribed to an issue
138+
func CheckIssueSubscription(ctx *context.APIContext) {
139+
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/subscriptions/check issue issueCheckSubscription
140+
// ---
141+
// summary: Check if user is subscribed to an issue
142+
// consumes:
143+
// - application/json
144+
// produces:
145+
// - application/json
146+
// parameters:
147+
// - name: owner
148+
// in: path
149+
// description: owner of the repo
150+
// type: string
151+
// required: true
152+
// - name: repo
153+
// in: path
154+
// description: name of the repo
155+
// type: string
156+
// required: true
157+
// - name: index
158+
// in: path
159+
// description: index of the issue
160+
// type: integer
161+
// format: int64
162+
// required: true
163+
// responses:
164+
// "200":
165+
// "$ref": "#/responses/WatchInfo"
166+
// "404":
167+
// "$ref": "#/responses/notFound"
168+
169+
issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
170+
if err != nil {
171+
if models.IsErrIssueNotExist(err) {
172+
ctx.NotFound()
173+
} else {
174+
ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
175+
}
176+
177+
return
178+
}
179+
180+
watching, err := models.CheckIssueWatch(ctx.User, issue)
181+
if err != nil {
182+
ctx.InternalServerError(err)
183+
return
184+
}
185+
ctx.JSON(http.StatusOK, api.WatchInfo{
186+
Subscribed: watching,
187+
Ignored: !watching,
188+
Reason: nil,
189+
CreatedAt: issue.CreatedUnix.AsTime(),
190+
URL: issue.APIURL() + "/subscriptions",
191+
RepositoryURL: ctx.Repo.Repository.APIURL(),
192+
})
193+
}
194+
136195
// GetIssueSubscribers return subscribers of an issue
137196
func GetIssueSubscribers(ctx *context.APIContext) {
138197
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/subscriptions issue issueSubscriptions

routers/api/v1/user/watch.go

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99

1010
"code.gitea.io/gitea/models"
1111
"code.gitea.io/gitea/modules/context"
12-
"code.gitea.io/gitea/modules/setting"
1312
api "code.gitea.io/gitea/modules/structs"
1413
"code.gitea.io/gitea/routers/api/v1/utils"
1514
)
@@ -124,7 +123,7 @@ func IsWatching(ctx *context.APIContext) {
124123
Reason: nil,
125124
CreatedAt: ctx.Repo.Repository.CreatedUnix.AsTime(),
126125
URL: subscriptionURL(ctx.Repo.Repository),
127-
RepositoryURL: repositoryURL(ctx.Repo.Repository),
126+
RepositoryURL: ctx.Repo.Repository.APIURL(),
128127
})
129128
} else {
130129
ctx.NotFound()
@@ -162,7 +161,7 @@ func Watch(ctx *context.APIContext) {
162161
Reason: nil,
163162
CreatedAt: ctx.Repo.Repository.CreatedUnix.AsTime(),
164163
URL: subscriptionURL(ctx.Repo.Repository),
165-
RepositoryURL: repositoryURL(ctx.Repo.Repository),
164+
RepositoryURL: ctx.Repo.Repository.APIURL(),
166165
})
167166

168167
}
@@ -197,10 +196,5 @@ func Unwatch(ctx *context.APIContext) {
197196

198197
// subscriptionURL returns the URL of the subscription API endpoint of a repo
199198
func subscriptionURL(repo *models.Repository) string {
200-
return repositoryURL(repo) + "/subscription"
201-
}
202-
203-
// repositoryURL returns the URL of the API endpoint of a repo
204-
func repositoryURL(repo *models.Repository) string {
205-
return setting.AppURL + "api/v1/" + repo.FullName()
199+
return repo.APIURL() + "/subscription"
206200
}

routers/repo/issue.go

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -749,21 +749,15 @@ func ViewIssue(ctx *context.Context) {
749749

750750
ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title)
751751

752-
var iw *models.IssueWatch
753-
var exists bool
752+
iw := new(models.IssueWatch)
754753
if ctx.User != nil {
755-
iw, exists, err = models.GetIssueWatch(ctx.User.ID, issue.ID)
754+
iw.UserID = ctx.User.ID
755+
iw.IssueID = issue.ID
756+
iw.IsWatching, err = models.CheckIssueWatch(ctx.User, issue)
756757
if err != nil {
757-
ctx.ServerError("GetIssueWatch", err)
758+
ctx.InternalServerError(err)
758759
return
759760
}
760-
if !exists {
761-
iw = &models.IssueWatch{
762-
UserID: ctx.User.ID,
763-
IssueID: issue.ID,
764-
IsWatching: models.IsWatching(ctx.User.ID, ctx.Repo.Repository.ID) || models.IsUserParticipantsOfIssue(ctx.User, issue),
765-
}
766-
}
767761
}
768762
ctx.Data["IssueWatch"] = iw
769763

templates/swagger/v1_json.tmpl

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5217,6 +5217,53 @@
52175217
}
52185218
}
52195219
},
5220+
"/repos/{owner}/{repo}/issues/{index}/subscriptions/check": {
5221+
"get": {
5222+
"consumes": [
5223+
"application/json"
5224+
],
5225+
"produces": [
5226+
"application/json"
5227+
],
5228+
"tags": [
5229+
"issue"
5230+
],
5231+
"summary": "Check if user is subscribed to an issue",
5232+
"operationId": "issueCheckSubscription",
5233+
"parameters": [
5234+
{
5235+
"type": "string",
5236+
"description": "owner of the repo",
5237+
"name": "owner",
5238+
"in": "path",
5239+
"required": true
5240+
},
5241+
{
5242+
"type": "string",
5243+
"description": "name of the repo",
5244+
"name": "repo",
5245+
"in": "path",
5246+
"required": true
5247+
},
5248+
{
5249+
"type": "integer",
5250+
"format": "int64",
5251+
"description": "index of the issue",
5252+
"name": "index",
5253+
"in": "path",
5254+
"required": true
5255+
}
5256+
],
5257+
"responses": {
5258+
"200": {
5259+
"$ref": "#/responses/WatchInfo"
5260+
},
5261+
"404": {
5262+
"$ref": "#/responses/notFound"
5263+
}
5264+
}
5265+
}
5266+
},
52205267
"/repos/{owner}/{repo}/issues/{index}/subscriptions/{user}": {
52215268
"put": {
52225269
"consumes": [

0 commit comments

Comments
 (0)