[API] ListIssues add filter for milestones (#10148)

* Refactor Issue Filter Func

* ListIssues add filter for milestones

* as per @lafriks

* documentation ...
release/v1.15
6543 2020-04-30 06:15:39 +02:00 committed by GitHub
parent cbf5dffaf2
commit bfda0f3864
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 102 additions and 29 deletions

View File

@ -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) {

View File

@ -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)
} }

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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
} }

View File

@ -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{})

View File

@ -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
@ -275,6 +310,7 @@ func ListIssues(ctx *context.APIContext) {
IsClosed: isClosed, IsClosed: isClosed,
IssueIDs: issueIDs, IssueIDs: issueIDs,
LabelIDs: labelIDs, LabelIDs: labelIDs,
MilestoneIDs: mileIDs,
IsPull: isPull, IsPull: isPull,
}) })
} }

View File

@ -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{}
@ -203,7 +208,7 @@ func issues(ctx *context.Context, milestoneID int64, isPullOption util.OptionalB
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,

View File

@ -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)",