From f88a2eae9777e0be612647bc17227c1ca13616ba Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Mon, 23 Nov 2020 21:49:36 +0100 Subject: [PATCH] [API] Add more filters to issues search (#13514) * Add time filter for issue search * Add limit option for paggination * Add Filter for: Created by User, Assigned to User, Mentioning User * update swagger * Add Tests for limit, before & since --- integrations/api_issue_test.go | 29 +++++++++++++--- models/issue.go | 9 +++++ routers/api/v1/repo/issue.go | 61 ++++++++++++++++++++++++++++++++-- templates/swagger/v1_json.tmpl | 40 +++++++++++++++++++++- 4 files changed, 130 insertions(+), 9 deletions(-) diff --git a/integrations/api_issue_test.go b/integrations/api_issue_test.go index 9311d50c5cbf..81e5c44873b1 100644 --- a/integrations/api_issue_test.go +++ b/integrations/api_issue_test.go @@ -9,6 +9,7 @@ import ( "net/http" "net/url" "testing" + "time" "code.gitea.io/gitea/models" api "code.gitea.io/gitea/modules/structs" @@ -152,17 +153,27 @@ func TestAPISearchIssues(t *testing.T) { resp := session.MakeRequest(t, req, http.StatusOK) var apiIssues []*api.Issue DecodeJSON(t, resp, &apiIssues) - assert.Len(t, apiIssues, 10) - query := url.Values{} - query.Add("token", token) + query := url.Values{"token": {token}} link.RawQuery = query.Encode() req = NewRequest(t, "GET", link.String()) resp = session.MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) assert.Len(t, apiIssues, 10) + since := "2000-01-01T00%3A50%3A01%2B00%3A00" // 946687801 + before := time.Unix(999307200, 0).Format(time.RFC3339) + query.Add("since", since) + query.Add("before", before) + link.RawQuery = query.Encode() + req = NewRequest(t, "GET", link.String()) + resp = session.MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiIssues) + assert.Len(t, apiIssues, 8) + query.Del("since") + query.Del("before") + query.Add("state", "closed") link.RawQuery = query.Encode() req = NewRequest(t, "GET", link.String()) @@ -175,14 +186,22 @@ func TestAPISearchIssues(t *testing.T) { req = NewRequest(t, "GET", link.String()) resp = session.MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) + assert.EqualValues(t, "12", resp.Header().Get("X-Total-Count")) assert.Len(t, apiIssues, 10) //there are more but 10 is page item limit - query.Add("page", "2") + query.Add("limit", "20") link.RawQuery = query.Encode() req = NewRequest(t, "GET", link.String()) resp = session.MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &apiIssues) - assert.Len(t, apiIssues, 2) + assert.Len(t, apiIssues, 12) + + query = url.Values{"assigned": {"true"}, "state": {"all"}} + link.RawQuery = query.Encode() + req = NewRequest(t, "GET", link.String()) + resp = session.MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiIssues) + assert.Len(t, apiIssues, 1) } func TestAPISearchIssuesWithLabels(t *testing.T) { diff --git a/models/issue.go b/models/issue.go index ee75623f5302..8c135faa8df3 100644 --- a/models/issue.go +++ b/models/issue.go @@ -1100,6 +1100,8 @@ type IssuesOptions struct { ExcludedLabelNames []string SortType string IssueIDs []int64 + UpdatedAfterUnix int64 + UpdatedBeforeUnix int64 // prioritize issues from this repo PriorityRepoID int64 } @@ -1178,6 +1180,13 @@ func (opts *IssuesOptions) setupSession(sess *xorm.Session) { sess.In("issue.milestone_id", opts.MilestoneIDs) } + if opts.UpdatedAfterUnix != 0 { + sess.And(builder.Gte{"issue.updated_unix": opts.UpdatedAfterUnix}) + } + if opts.UpdatedBeforeUnix != 0 { + sess.And(builder.Lte{"issue.updated_unix": opts.UpdatedBeforeUnix}) + } + if opts.ProjectID > 0 { sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id"). And("project_issue.project_id=?", opts.ProjectID) diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index 0dbf2741ad4b..c58e0bb6ce51 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -55,14 +55,48 @@ func SearchIssues(ctx *context.APIContext) { // in: query // description: filter by type (issues / pulls) if set // type: string + // - name: since + // in: query + // description: Only show notifications updated after the given time. This is a timestamp in RFC 3339 format + // type: string + // format: date-time + // required: false + // - name: before + // in: query + // description: Only show notifications updated before the given time. This is a timestamp in RFC 3339 format + // type: string + // format: date-time + // required: false + // - name: assigned + // in: query + // description: filter (issues / pulls) assigned to you, default is false + // type: boolean + // - name: created + // in: query + // description: filter (issues / pulls) created by you, default is false + // type: boolean + // - name: mentioned + // in: query + // description: filter (issues / pulls) mentioning you, default is false + // type: boolean // - name: page // in: query - // description: page number of requested issues + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results // type: integer // responses: // "200": // "$ref": "#/responses/IssueList" + before, since, err := utils.GetQueryBeforeSince(ctx) + if err != nil { + ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err) + return + } + var isClosed util.OptionalBool switch ctx.Query("state") { case "closed": @@ -119,7 +153,6 @@ func SearchIssues(ctx *context.APIContext) { } var issueIDs []int64 var labelIDs []int64 - var err error if len(keyword) > 0 && len(repoIDs) > 0 { if issueIDs, err = issue_indexer.SearchIssuesByKeyword(repoIDs, keyword); err != nil { ctx.Error(http.StatusInternalServerError, "SearchIssuesByKeyword", err) @@ -143,13 +176,22 @@ func SearchIssues(ctx *context.APIContext) { includedLabelNames = strings.Split(labels, ",") } + // this api is also used in UI, + // so the default limit is set to fit UI needs + limit := ctx.QueryInt("limit") + if limit == 0 { + limit = setting.UI.IssuePagingNum + } else if limit > setting.API.MaxResponseItems { + limit = setting.API.MaxResponseItems + } + // Only fetch the issues if we either don't have a keyword or the search returned issues // This would otherwise return all issues if no issues were found by the search. if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 { issuesOpt := &models.IssuesOptions{ ListOptions: models.ListOptions{ Page: ctx.QueryInt("page"), - PageSize: setting.UI.IssuePagingNum, + PageSize: limit, }, RepoIDs: repoIDs, IsClosed: isClosed, @@ -158,6 +200,19 @@ func SearchIssues(ctx *context.APIContext) { SortType: "priorityrepo", PriorityRepoID: ctx.QueryInt64("priority_repo_id"), IsPull: isPull, + UpdatedBeforeUnix: before, + UpdatedAfterUnix: since, + } + + // Filter for: Created by User, Assigned to User, Mentioning User + if ctx.QueryBool("created") { + issuesOpt.PosterID = ctx.User.ID + } + if ctx.QueryBool("assigned") { + issuesOpt.AssigneeID = ctx.User.ID + } + if ctx.QueryBool("mentioned") { + issuesOpt.MentionedID = ctx.User.ID } if issues, err = models.Issues(issuesOpt); err != nil { diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 9d775da7d6b8..8bcfc43d73f7 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -1879,11 +1879,49 @@ "name": "type", "in": "query" }, + { + "type": "string", + "format": "date-time", + "description": "Only show notifications updated after the given time. This is a timestamp in RFC 3339 format", + "name": "since", + "in": "query" + }, + { + "type": "string", + "format": "date-time", + "description": "Only show notifications updated before the given time. This is a timestamp in RFC 3339 format", + "name": "before", + "in": "query" + }, + { + "type": "boolean", + "description": "filter (issues / pulls) assigned to you, default is false", + "name": "assigned", + "in": "query" + }, + { + "type": "boolean", + "description": "filter (issues / pulls) created by you, default is false", + "name": "created", + "in": "query" + }, + { + "type": "boolean", + "description": "filter (issues / pulls) mentioning you, default is false", + "name": "mentioned", + "in": "query" + }, { "type": "integer", - "description": "page number of requested issues", + "description": "page number of results to return (1-based)", "name": "page", "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" } ], "responses": {