[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
This commit is contained in:
6543 2020-11-23 21:49:36 +01:00 committed by GitHub
parent 78204a7a71
commit f88a2eae97
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 130 additions and 9 deletions

View file

@ -9,6 +9,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"testing" "testing"
"time"
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
@ -152,17 +153,27 @@ func TestAPISearchIssues(t *testing.T) {
resp := session.MakeRequest(t, req, http.StatusOK) resp := session.MakeRequest(t, req, http.StatusOK)
var apiIssues []*api.Issue var apiIssues []*api.Issue
DecodeJSON(t, resp, &apiIssues) DecodeJSON(t, resp, &apiIssues)
assert.Len(t, apiIssues, 10) assert.Len(t, apiIssues, 10)
query := url.Values{} query := url.Values{"token": {token}}
query.Add("token", token)
link.RawQuery = query.Encode() link.RawQuery = query.Encode()
req = NewRequest(t, "GET", link.String()) req = NewRequest(t, "GET", link.String())
resp = session.MakeRequest(t, req, http.StatusOK) resp = session.MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiIssues) DecodeJSON(t, resp, &apiIssues)
assert.Len(t, apiIssues, 10) 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") query.Add("state", "closed")
link.RawQuery = query.Encode() link.RawQuery = query.Encode()
req = NewRequest(t, "GET", link.String()) req = NewRequest(t, "GET", link.String())
@ -175,14 +186,22 @@ func TestAPISearchIssues(t *testing.T) {
req = NewRequest(t, "GET", link.String()) req = NewRequest(t, "GET", link.String())
resp = session.MakeRequest(t, req, http.StatusOK) resp = session.MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiIssues) 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 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() link.RawQuery = query.Encode()
req = NewRequest(t, "GET", link.String()) req = NewRequest(t, "GET", link.String())
resp = session.MakeRequest(t, req, http.StatusOK) resp = session.MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiIssues) 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) { func TestAPISearchIssuesWithLabels(t *testing.T) {

View file

@ -1100,6 +1100,8 @@ type IssuesOptions struct {
ExcludedLabelNames []string ExcludedLabelNames []string
SortType string SortType string
IssueIDs []int64 IssueIDs []int64
UpdatedAfterUnix int64
UpdatedBeforeUnix int64
// prioritize issues from this repo // prioritize issues from this repo
PriorityRepoID int64 PriorityRepoID int64
} }
@ -1178,6 +1180,13 @@ func (opts *IssuesOptions) setupSession(sess *xorm.Session) {
sess.In("issue.milestone_id", opts.MilestoneIDs) 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 { if opts.ProjectID > 0 {
sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id"). sess.Join("INNER", "project_issue", "issue.id = project_issue.issue_id").
And("project_issue.project_id=?", opts.ProjectID) And("project_issue.project_id=?", opts.ProjectID)

View file

@ -55,14 +55,48 @@ func SearchIssues(ctx *context.APIContext) {
// in: query // in: query
// description: filter by type (issues / pulls) if set // description: filter by type (issues / pulls) if set
// type: string // 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 // - name: page
// in: query // 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 // type: integer
// responses: // responses:
// "200": // "200":
// "$ref": "#/responses/IssueList" // "$ref": "#/responses/IssueList"
before, since, err := utils.GetQueryBeforeSince(ctx)
if err != nil {
ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
return
}
var isClosed util.OptionalBool var isClosed util.OptionalBool
switch ctx.Query("state") { switch ctx.Query("state") {
case "closed": case "closed":
@ -119,7 +153,6 @@ func SearchIssues(ctx *context.APIContext) {
} }
var issueIDs []int64 var issueIDs []int64
var labelIDs []int64 var labelIDs []int64
var err error
if len(keyword) > 0 && len(repoIDs) > 0 { if len(keyword) > 0 && len(repoIDs) > 0 {
if issueIDs, err = issue_indexer.SearchIssuesByKeyword(repoIDs, keyword); err != nil { if issueIDs, err = issue_indexer.SearchIssuesByKeyword(repoIDs, keyword); err != nil {
ctx.Error(http.StatusInternalServerError, "SearchIssuesByKeyword", err) ctx.Error(http.StatusInternalServerError, "SearchIssuesByKeyword", err)
@ -143,13 +176,22 @@ func SearchIssues(ctx *context.APIContext) {
includedLabelNames = strings.Split(labels, ",") 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 // 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. // This would otherwise return all issues if no issues were found by the search.
if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 { if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 {
issuesOpt := &models.IssuesOptions{ issuesOpt := &models.IssuesOptions{
ListOptions: models.ListOptions{ ListOptions: models.ListOptions{
Page: ctx.QueryInt("page"), Page: ctx.QueryInt("page"),
PageSize: setting.UI.IssuePagingNum, PageSize: limit,
}, },
RepoIDs: repoIDs, RepoIDs: repoIDs,
IsClosed: isClosed, IsClosed: isClosed,
@ -158,6 +200,19 @@ func SearchIssues(ctx *context.APIContext) {
SortType: "priorityrepo", SortType: "priorityrepo",
PriorityRepoID: ctx.QueryInt64("priority_repo_id"), PriorityRepoID: ctx.QueryInt64("priority_repo_id"),
IsPull: isPull, 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 { if issues, err = models.Issues(issuesOpt); err != nil {

View file

@ -1879,11 +1879,49 @@
"name": "type", "name": "type",
"in": "query" "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", "type": "integer",
"description": "page number of requested issues", "description": "page number of results to return (1-based)",
"name": "page", "name": "page",
"in": "query" "in": "query"
},
{
"type": "integer",
"description": "page size of results",
"name": "limit",
"in": "query"
} }
], ],
"responses": { "responses": {