[API] ListIssues add filter for milestones (#10148)
* Refactor Issue Filter Func * ListIssues add filter for milestones * as per @lafriks * documentation ...
This commit is contained in:
parent
cbf5dffaf2
commit
bfda0f3864
10 changed files with 102 additions and 29 deletions
|
@ -33,6 +33,17 @@ func TestAPIListIssues(t *testing.T) {
|
||||||
for _, apiIssue := range apiIssues {
|
for _, apiIssue := range apiIssues {
|
||||||
models.AssertExistsAndLoadBean(t, &models.Issue{ID: apiIssue.ID, RepoID: repo.ID})
|
models.AssertExistsAndLoadBean(t, &models.Issue{ID: apiIssue.ID, RepoID: repo.ID})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// test milestone filter
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/issues?state=all&type=all&milestones=ignore,milestone1,3,4&token=%s",
|
||||||
|
owner.Name, repo.Name, token)
|
||||||
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
DecodeJSON(t, resp, &apiIssues)
|
||||||
|
if assert.Len(t, apiIssues, 2) {
|
||||||
|
assert.EqualValues(t, 3, apiIssues[0].Milestone.ID)
|
||||||
|
assert.EqualValues(t, 1, apiIssues[1].Milestone.ID)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAPICreateIssue(t *testing.T) {
|
func TestAPICreateIssue(t *testing.T) {
|
||||||
|
|
|
@ -1560,6 +1560,7 @@ func (err ErrLabelNotExist) Error() string {
|
||||||
type ErrMilestoneNotExist struct {
|
type ErrMilestoneNotExist struct {
|
||||||
ID int64
|
ID int64
|
||||||
RepoID int64
|
RepoID int64
|
||||||
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsErrMilestoneNotExist checks if an error is a ErrMilestoneNotExist.
|
// IsErrMilestoneNotExist checks if an error is a ErrMilestoneNotExist.
|
||||||
|
@ -1569,6 +1570,9 @@ func IsErrMilestoneNotExist(err error) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err ErrMilestoneNotExist) Error() string {
|
func (err ErrMilestoneNotExist) Error() string {
|
||||||
|
if len(err.Name) > 0 {
|
||||||
|
return fmt.Sprintf("milestone does not exist [name: %s, repo_id: %d]", err.Name, err.RepoID)
|
||||||
|
}
|
||||||
return fmt.Sprintf("milestone does not exist [id: %d, repo_id: %d]", err.ID, err.RepoID)
|
return fmt.Sprintf("milestone does not exist [id: %d, repo_id: %d]", err.ID, err.RepoID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
poster_id: 1
|
poster_id: 1
|
||||||
name: issue3
|
name: issue3
|
||||||
content: content for the third issue
|
content: content for the third issue
|
||||||
|
milestone_id: 3
|
||||||
is_closed: false
|
is_closed: false
|
||||||
is_pull: true
|
is_pull: true
|
||||||
created_unix: 946684820
|
created_unix: 946684820
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
name: milestone3
|
name: milestone3
|
||||||
content: content3
|
content: content3
|
||||||
is_closed: true
|
is_closed: true
|
||||||
num_issues: 0
|
num_issues: 1
|
||||||
|
|
||||||
-
|
-
|
||||||
id: 4
|
id: 4
|
||||||
|
|
|
@ -1058,7 +1058,7 @@ type IssuesOptions struct {
|
||||||
AssigneeID int64
|
AssigneeID int64
|
||||||
PosterID int64
|
PosterID int64
|
||||||
MentionedID int64
|
MentionedID int64
|
||||||
MilestoneID int64
|
MilestoneIDs []int64
|
||||||
IsClosed util.OptionalBool
|
IsClosed util.OptionalBool
|
||||||
IsPull util.OptionalBool
|
IsPull util.OptionalBool
|
||||||
LabelIDs []int64
|
LabelIDs []int64
|
||||||
|
@ -1143,8 +1143,8 @@ func (opts *IssuesOptions) setupSession(sess *xorm.Session) {
|
||||||
And("issue_user.uid = ?", opts.MentionedID)
|
And("issue_user.uid = ?", opts.MentionedID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.MilestoneID > 0 {
|
if len(opts.MilestoneIDs) > 0 {
|
||||||
sess.And("issue.milestone_id=?", opts.MilestoneID)
|
sess.In("issue.milestone_id", opts.MilestoneIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch opts.IsPull {
|
switch opts.IsPull {
|
||||||
|
|
|
@ -109,15 +109,12 @@ func NewMilestone(m *Milestone) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMilestoneByRepoID(e Engine, repoID, id int64) (*Milestone, error) {
|
func getMilestoneByRepoID(e Engine, repoID, id int64) (*Milestone, error) {
|
||||||
m := &Milestone{
|
m := new(Milestone)
|
||||||
ID: id,
|
has, err := e.ID(id).Where("repo_id=?", repoID).Get(m)
|
||||||
RepoID: repoID,
|
|
||||||
}
|
|
||||||
has, err := e.Get(m)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if !has {
|
} else if !has {
|
||||||
return nil, ErrMilestoneNotExist{id, repoID}
|
return nil, ErrMilestoneNotExist{ID: id, RepoID: repoID}
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
@ -127,6 +124,19 @@ func GetMilestoneByRepoID(repoID, id int64) (*Milestone, error) {
|
||||||
return getMilestoneByRepoID(x, repoID, id)
|
return getMilestoneByRepoID(x, repoID, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetMilestoneByRepoIDANDName return a milestone if one exist by name and repo
|
||||||
|
func GetMilestoneByRepoIDANDName(repoID int64, name string) (*Milestone, error) {
|
||||||
|
var mile Milestone
|
||||||
|
has, err := x.Where("repo_id=? AND name=?", repoID, name).Get(&mile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !has {
|
||||||
|
return nil, ErrMilestoneNotExist{Name: name, RepoID: repoID}
|
||||||
|
}
|
||||||
|
return &mile, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetMilestoneByID returns the milestone via id .
|
// GetMilestoneByID returns the milestone via id .
|
||||||
func GetMilestoneByID(id int64) (*Milestone, error) {
|
func GetMilestoneByID(id int64) (*Milestone, error) {
|
||||||
var m Milestone
|
var m Milestone
|
||||||
|
@ -134,7 +144,7 @@ func GetMilestoneByID(id int64) (*Milestone, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if !has {
|
} else if !has {
|
||||||
return nil, ErrMilestoneNotExist{id, 0}
|
return nil, ErrMilestoneNotExist{ID: id, RepoID: 0}
|
||||||
}
|
}
|
||||||
return &m, nil
|
return &m, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -240,14 +240,14 @@ func TestUpdateMilestoneClosedNum(t *testing.T) {
|
||||||
|
|
||||||
issue.IsClosed = true
|
issue.IsClosed = true
|
||||||
issue.ClosedUnix = timeutil.TimeStampNow()
|
issue.ClosedUnix = timeutil.TimeStampNow()
|
||||||
_, err := x.Cols("is_closed", "closed_unix").Update(issue)
|
_, err := x.ID(issue.ID).Cols("is_closed", "closed_unix").Update(issue)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NoError(t, updateMilestoneClosedNum(x, issue.MilestoneID))
|
assert.NoError(t, updateMilestoneClosedNum(x, issue.MilestoneID))
|
||||||
CheckConsistencyFor(t, &Milestone{})
|
CheckConsistencyFor(t, &Milestone{})
|
||||||
|
|
||||||
issue.IsClosed = false
|
issue.IsClosed = false
|
||||||
issue.ClosedUnix = 0
|
issue.ClosedUnix = 0
|
||||||
_, err = x.Cols("is_closed", "closed_unix").Update(issue)
|
_, err = x.ID(issue.ID).Cols("is_closed", "closed_unix").Update(issue)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NoError(t, updateMilestoneClosedNum(x, issue.MilestoneID))
|
assert.NoError(t, updateMilestoneClosedNum(x, issue.MilestoneID))
|
||||||
CheckConsistencyFor(t, &Milestone{})
|
CheckConsistencyFor(t, &Milestone{})
|
||||||
|
|
|
@ -8,6 +8,7 @@ package repo
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -208,6 +209,10 @@ func ListIssues(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: milestones
|
||||||
|
// 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
|
||||||
|
// 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)
|
||||||
|
@ -251,6 +256,36 @@ func ListIssues(ctx *context.APIContext) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var mileIDs []int64
|
||||||
|
if part := strings.Split(ctx.Query("milestones"), ","); len(part) > 0 {
|
||||||
|
for i := range part {
|
||||||
|
// uses names and fall back to ids
|
||||||
|
// non existent milestones are discarded
|
||||||
|
mile, err := models.GetMilestoneByRepoIDANDName(ctx.Repo.Repository.ID, part[i])
|
||||||
|
if err == nil {
|
||||||
|
mileIDs = append(mileIDs, mile.ID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !models.IsErrMilestoneNotExist(err) {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoIDANDName", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := strconv.ParseInt(part[i], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mile, err = models.GetMilestoneByRepoID(ctx.Repo.Repository.ID, id)
|
||||||
|
if err == nil {
|
||||||
|
mileIDs = append(mileIDs, mile.ID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if models.IsErrMilestoneNotExist(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoID", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
listOptions := utils.GetListOptions(ctx)
|
listOptions := utils.GetListOptions(ctx)
|
||||||
if ctx.QueryInt("limit") == 0 {
|
if ctx.QueryInt("limit") == 0 {
|
||||||
listOptions.PageSize = setting.UI.IssuePagingNum
|
listOptions.PageSize = setting.UI.IssuePagingNum
|
||||||
|
@ -270,12 +305,13 @@ func ListIssues(ctx *context.APIContext) {
|
||||||
// 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 {
|
||||||
issues, err = models.Issues(&models.IssuesOptions{
|
issues, err = models.Issues(&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,
|
||||||
IsPull: isPull,
|
MilestoneIDs: mileIDs,
|
||||||
|
IsPull: isPull,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -190,6 +190,11 @@ func issues(ctx *context.Context, milestoneID int64, isPullOption util.OptionalB
|
||||||
}
|
}
|
||||||
pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5)
|
pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5)
|
||||||
|
|
||||||
|
var mileIDs []int64
|
||||||
|
if milestoneID > 0 {
|
||||||
|
mileIDs = []int64{milestoneID}
|
||||||
|
}
|
||||||
|
|
||||||
var issues []*models.Issue
|
var issues []*models.Issue
|
||||||
if forceEmpty {
|
if forceEmpty {
|
||||||
issues = []*models.Issue{}
|
issues = []*models.Issue{}
|
||||||
|
@ -199,16 +204,16 @@ func issues(ctx *context.Context, milestoneID int64, isPullOption util.OptionalB
|
||||||
Page: pager.Paginater.Current(),
|
Page: pager.Paginater.Current(),
|
||||||
PageSize: setting.UI.IssuePagingNum,
|
PageSize: setting.UI.IssuePagingNum,
|
||||||
},
|
},
|
||||||
RepoIDs: []int64{repo.ID},
|
RepoIDs: []int64{repo.ID},
|
||||||
AssigneeID: assigneeID,
|
AssigneeID: assigneeID,
|
||||||
PosterID: posterID,
|
PosterID: posterID,
|
||||||
MentionedID: mentionedID,
|
MentionedID: mentionedID,
|
||||||
MilestoneID: milestoneID,
|
MilestoneIDs: mileIDs,
|
||||||
IsClosed: util.OptionalBoolOf(isShowClosed),
|
IsClosed: util.OptionalBoolOf(isShowClosed),
|
||||||
IsPull: isPullOption,
|
IsPull: isPullOption,
|
||||||
LabelIDs: labelIDs,
|
LabelIDs: labelIDs,
|
||||||
SortType: sortType,
|
SortType: sortType,
|
||||||
IssueIDs: issueIDs,
|
IssueIDs: issueIDs,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("Issues", err)
|
ctx.ServerError("Issues", err)
|
||||||
|
|
|
@ -3768,6 +3768,12 @@
|
||||||
"name": "type",
|
"name": "type",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"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",
|
||||||
|
"name": "milestones",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "page number of results to return (1-based)",
|
"description": "page number of results to return (1-based)",
|
||||||
|
|
Reference in a new issue