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. | ||||
| 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": [ | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 6543
				6543