[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:
parent
78204a7a71
commit
f88a2eae97
4 changed files with 130 additions and 9 deletions
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
Reference in a new issue