forked from gitea/gitea
[API] ListIssues add more filters (#16174)
* [API] ListIssues add more filters: optional filter repo issues by: - since - before - created_by - assigned_by - mentioned_by * Add Tests * Update routers/api/v1/repo/issue.go Co-authored-by: Lanre Adelowo <adelowomailbox@gmail.com> * Apply suggestions from code review Co-authored-by: Lanre Adelowo <adelowomailbox@gmail.com> Co-authored-by: techknowlogick <techknowlogick@gitea.io>
This commit is contained in:
parent
ffbf35b7e9
commit
0e081ff0ce
|
@ -25,9 +25,10 @@ func TestAPIListIssues(t *testing.T) {
|
||||||
|
|
||||||
session := loginUser(t, owner.Name)
|
session := loginUser(t, owner.Name)
|
||||||
token := getTokenForLoggedInUser(t, session)
|
token := getTokenForLoggedInUser(t, session)
|
||||||
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues?state=all&token=%s",
|
link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/issues", owner.Name, repo.Name))
|
||||||
owner.Name, repo.Name, token)
|
|
||||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
link.RawQuery = url.Values{"token": {token}, "state": {"all"}}.Encode()
|
||||||
|
resp := session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
|
||||||
var apiIssues []*api.Issue
|
var apiIssues []*api.Issue
|
||||||
DecodeJSON(t, resp, &apiIssues)
|
DecodeJSON(t, resp, &apiIssues)
|
||||||
assert.Len(t, apiIssues, models.GetCount(t, &models.Issue{RepoID: repo.ID}))
|
assert.Len(t, apiIssues, models.GetCount(t, &models.Issue{RepoID: repo.ID}))
|
||||||
|
@ -36,15 +37,34 @@ func TestAPIListIssues(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// test milestone filter
|
// test milestone filter
|
||||||
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues?state=all&type=all&milestones=ignore,milestone1,3,4&token=%s",
|
link.RawQuery = url.Values{"token": {token}, "state": {"all"}, "type": {"all"}, "milestones": {"ignore,milestone1,3,4"}}.Encode()
|
||||||
owner.Name, repo.Name, token)
|
resp = session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
|
||||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
|
||||||
DecodeJSON(t, resp, &apiIssues)
|
DecodeJSON(t, resp, &apiIssues)
|
||||||
if assert.Len(t, apiIssues, 2) {
|
if assert.Len(t, apiIssues, 2) {
|
||||||
assert.EqualValues(t, 3, apiIssues[0].Milestone.ID)
|
assert.EqualValues(t, 3, apiIssues[0].Milestone.ID)
|
||||||
assert.EqualValues(t, 1, apiIssues[1].Milestone.ID)
|
assert.EqualValues(t, 1, apiIssues[1].Milestone.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
link.RawQuery = url.Values{"token": {token}, "state": {"all"}, "created_by": {"user2"}}.Encode()
|
||||||
|
resp = session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
|
||||||
|
DecodeJSON(t, resp, &apiIssues)
|
||||||
|
if assert.Len(t, apiIssues, 1) {
|
||||||
|
assert.EqualValues(t, 5, apiIssues[0].ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
link.RawQuery = url.Values{"token": {token}, "state": {"all"}, "assigned_by": {"user1"}}.Encode()
|
||||||
|
resp = session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
|
||||||
|
DecodeJSON(t, resp, &apiIssues)
|
||||||
|
if assert.Len(t, apiIssues, 1) {
|
||||||
|
assert.EqualValues(t, 1, apiIssues[0].ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
link.RawQuery = url.Values{"token": {token}, "state": {"all"}, "mentioned_by": {"user4"}}.Encode()
|
||||||
|
resp = session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK)
|
||||||
|
DecodeJSON(t, resp, &apiIssues)
|
||||||
|
if assert.Len(t, apiIssues, 1) {
|
||||||
|
assert.EqualValues(t, 1, apiIssues[0].ID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAPICreateIssue(t *testing.T) {
|
func TestAPICreateIssue(t *testing.T) {
|
||||||
|
|
|
@ -17,4 +17,4 @@
|
||||||
uid: 4
|
uid: 4
|
||||||
issue_id: 1
|
issue_id: 1
|
||||||
is_read: false
|
is_read: false
|
||||||
is_mentioned: false
|
is_mentioned: true
|
||||||
|
|
|
@ -266,6 +266,30 @@ func ListIssues(ctx *context.APIContext) {
|
||||||
// in: query
|
// in: query
|
||||||
// description: comma separated list of milestone names or ids. It uses names and fall back to ids. Fetch only issues that have any of this milestones. Non existent milestones are discarded
|
// description: comma separated list of milestone names or ids. It uses names and fall back to ids. Fetch only issues that have any of this milestones. Non existent milestones are discarded
|
||||||
// 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: created_by
|
||||||
|
// in: query
|
||||||
|
// description: filter (issues / pulls) created to
|
||||||
|
// type: string
|
||||||
|
// - name: assigned_by
|
||||||
|
// in: query
|
||||||
|
// description: filter (issues / pulls) assigned to
|
||||||
|
// type: string
|
||||||
|
// - name: mentioned_by
|
||||||
|
// in: query
|
||||||
|
// description: filter (issues / pulls) mentioning to
|
||||||
|
// type: string
|
||||||
// - name: page
|
// - name: page
|
||||||
// in: query
|
// in: query
|
||||||
// description: page number of results to return (1-based)
|
// description: page number of results to return (1-based)
|
||||||
|
@ -277,6 +301,11 @@ func ListIssues(ctx *context.APIContext) {
|
||||||
// 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") {
|
||||||
|
@ -297,7 +326,6 @@ func ListIssues(ctx *context.APIContext) {
|
||||||
}
|
}
|
||||||
var issueIDs []int64
|
var issueIDs []int64
|
||||||
var labelIDs []int64
|
var labelIDs []int64
|
||||||
var err error
|
|
||||||
if len(keyword) > 0 {
|
if len(keyword) > 0 {
|
||||||
issueIDs, err = issue_indexer.SearchIssuesByKeyword([]int64{ctx.Repo.Repository.ID}, keyword)
|
issueIDs, err = issue_indexer.SearchIssuesByKeyword([]int64{ctx.Repo.Repository.ID}, keyword)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -356,17 +384,36 @@ func ListIssues(ctx *context.APIContext) {
|
||||||
isPull = util.OptionalBoolNone
|
isPull = util.OptionalBoolNone
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: we should be more efficient here
|
||||||
|
createdByID := getUserIDForFilter(ctx, "created_by")
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assignedByID := getUserIDForFilter(ctx, "assigned_by")
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mentionedByID := getUserIDForFilter(ctx, "mentioned_by")
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 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: listOptions,
|
ListOptions: listOptions,
|
||||||
RepoIDs: []int64{ctx.Repo.Repository.ID},
|
RepoIDs: []int64{ctx.Repo.Repository.ID},
|
||||||
IsClosed: isClosed,
|
IsClosed: isClosed,
|
||||||
IssueIDs: issueIDs,
|
IssueIDs: issueIDs,
|
||||||
LabelIDs: labelIDs,
|
LabelIDs: labelIDs,
|
||||||
MilestoneIDs: mileIDs,
|
MilestoneIDs: mileIDs,
|
||||||
IsPull: isPull,
|
IsPull: isPull,
|
||||||
|
UpdatedBeforeUnix: before,
|
||||||
|
UpdatedAfterUnix: since,
|
||||||
|
PosterID: createdByID,
|
||||||
|
AssigneeID: assignedByID,
|
||||||
|
MentionedID: mentionedByID,
|
||||||
}
|
}
|
||||||
|
|
||||||
if issues, err = models.Issues(issuesOpt); err != nil {
|
if issues, err = models.Issues(issuesOpt); err != nil {
|
||||||
|
@ -389,6 +436,26 @@ func ListIssues(ctx *context.APIContext) {
|
||||||
ctx.JSON(http.StatusOK, convert.ToAPIIssueList(issues))
|
ctx.JSON(http.StatusOK, convert.ToAPIIssueList(issues))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getUserIDForFilter(ctx *context.APIContext, queryName string) int64 {
|
||||||
|
userName := ctx.Query(queryName)
|
||||||
|
if len(userName) == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := models.GetUserByName(userName)
|
||||||
|
if models.IsErrUserNotExist(err) {
|
||||||
|
ctx.NotFound(err)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
ctx.InternalServerError(err)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.ID
|
||||||
|
}
|
||||||
|
|
||||||
// GetIssue get an issue of a repository
|
// GetIssue get an issue of a repository
|
||||||
func GetIssue(ctx *context.APIContext) {
|
func GetIssue(ctx *context.APIContext) {
|
||||||
// swagger:operation GET /repos/{owner}/{repo}/issues/{index} issue issueGetIssue
|
// swagger:operation GET /repos/{owner}/{repo}/issues/{index} issue issueGetIssue
|
||||||
|
|
|
@ -4234,6 +4234,38 @@
|
||||||
"name": "milestones",
|
"name": "milestones",
|
||||||
"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": "string",
|
||||||
|
"description": "filter (issues / pulls) created to",
|
||||||
|
"name": "created_by",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "filter (issues / pulls) assigned to",
|
||||||
|
"name": "assigned_by",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "filter (issues / pulls) mentioning to",
|
||||||
|
"name": "mentioned_by",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "page number of results to return (1-based)",
|
"description": "page number of results to return (1-based)",
|
||||||
|
|
Loading…
Reference in New Issue