* 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 & sincetags/v1.15.0-dev
@@ -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.Add("token", token) | |||||
query := url.Values{"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 { | ||||
@@ -1880,10 +1880,48 @@ | |||||
"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": { | ||||