[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
					
				| 
						 | 
				
			
			@ -33,6 +33,17 @@ func TestAPIListIssues(t *testing.T) {
 | 
			
		|||
	for _, apiIssue := range apiIssues {
 | 
			
		||||
		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) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1560,6 +1560,7 @@ func (err ErrLabelNotExist) Error() string {
 | 
			
		|||
type ErrMilestoneNotExist struct {
 | 
			
		||||
	ID     int64
 | 
			
		||||
	RepoID int64
 | 
			
		||||
	Name   string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsErrMilestoneNotExist checks if an error is a ErrMilestoneNotExist.
 | 
			
		||||
| 
						 | 
				
			
			@ -1569,6 +1570,9 @@ func IsErrMilestoneNotExist(err error) bool {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -32,6 +32,7 @@
 | 
			
		|||
  poster_id: 1
 | 
			
		||||
  name: issue3
 | 
			
		||||
  content: content for the third issue
 | 
			
		||||
  milestone_id: 3
 | 
			
		||||
  is_closed: false
 | 
			
		||||
  is_pull: true
 | 
			
		||||
  created_unix: 946684820
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,7 +20,7 @@
 | 
			
		|||
  name: milestone3
 | 
			
		||||
  content: content3
 | 
			
		||||
  is_closed: true
 | 
			
		||||
  num_issues: 0
 | 
			
		||||
  num_issues: 1
 | 
			
		||||
 | 
			
		||||
-
 | 
			
		||||
  id: 4
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1058,7 +1058,7 @@ type IssuesOptions struct {
 | 
			
		|||
	AssigneeID         int64
 | 
			
		||||
	PosterID           int64
 | 
			
		||||
	MentionedID        int64
 | 
			
		||||
	MilestoneID        int64
 | 
			
		||||
	MilestoneIDs       []int64
 | 
			
		||||
	IsClosed           util.OptionalBool
 | 
			
		||||
	IsPull             util.OptionalBool
 | 
			
		||||
	LabelIDs           []int64
 | 
			
		||||
| 
						 | 
				
			
			@ -1143,8 +1143,8 @@ func (opts *IssuesOptions) setupSession(sess *xorm.Session) {
 | 
			
		|||
			And("issue_user.uid = ?", opts.MentionedID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if opts.MilestoneID > 0 {
 | 
			
		||||
		sess.And("issue.milestone_id=?", opts.MilestoneID)
 | 
			
		||||
	if len(opts.MilestoneIDs) > 0 {
 | 
			
		||||
		sess.In("issue.milestone_id", opts.MilestoneIDs)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch opts.IsPull {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -109,15 +109,12 @@ func NewMilestone(m *Milestone) (err error) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
func getMilestoneByRepoID(e Engine, repoID, id int64) (*Milestone, error) {
 | 
			
		||||
	m := &Milestone{
 | 
			
		||||
		ID:     id,
 | 
			
		||||
		RepoID: repoID,
 | 
			
		||||
	}
 | 
			
		||||
	has, err := e.Get(m)
 | 
			
		||||
	m := new(Milestone)
 | 
			
		||||
	has, err := e.ID(id).Where("repo_id=?", repoID).Get(m)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	} else if !has {
 | 
			
		||||
		return nil, ErrMilestoneNotExist{id, repoID}
 | 
			
		||||
		return nil, ErrMilestoneNotExist{ID: id, RepoID: repoID}
 | 
			
		||||
	}
 | 
			
		||||
	return m, nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -127,6 +124,19 @@ func GetMilestoneByRepoID(repoID, id int64) (*Milestone, error) {
 | 
			
		|||
	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 .
 | 
			
		||||
func GetMilestoneByID(id int64) (*Milestone, error) {
 | 
			
		||||
	var m Milestone
 | 
			
		||||
| 
						 | 
				
			
			@ -134,7 +144,7 @@ func GetMilestoneByID(id int64) (*Milestone, error) {
 | 
			
		|||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	} else if !has {
 | 
			
		||||
		return nil, ErrMilestoneNotExist{id, 0}
 | 
			
		||||
		return nil, ErrMilestoneNotExist{ID: id, RepoID: 0}
 | 
			
		||||
	}
 | 
			
		||||
	return &m, nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -240,14 +240,14 @@ func TestUpdateMilestoneClosedNum(t *testing.T) {
 | 
			
		|||
 | 
			
		||||
	issue.IsClosed = true
 | 
			
		||||
	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, updateMilestoneClosedNum(x, issue.MilestoneID))
 | 
			
		||||
	CheckConsistencyFor(t, &Milestone{})
 | 
			
		||||
 | 
			
		||||
	issue.IsClosed = false
 | 
			
		||||
	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, updateMilestoneClosedNum(x, issue.MilestoneID))
 | 
			
		||||
	CheckConsistencyFor(t, &Milestone{})
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,6 +8,7 @@ package repo
 | 
			
		|||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -208,6 +209,10 @@ func ListIssues(ctx *context.APIContext) {
 | 
			
		|||
	//   in: query
 | 
			
		||||
	//   description: filter by type (issues / pulls) if set
 | 
			
		||||
	//   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
 | 
			
		||||
	//   in: query
 | 
			
		||||
	//   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)
 | 
			
		||||
	if ctx.QueryInt("limit") == 0 {
 | 
			
		||||
		listOptions.PageSize = setting.UI.IssuePagingNum
 | 
			
		||||
| 
						 | 
				
			
			@ -275,6 +310,7 @@ func ListIssues(ctx *context.APIContext) {
 | 
			
		|||
			IsClosed:     isClosed,
 | 
			
		||||
			IssueIDs:     issueIDs,
 | 
			
		||||
			LabelIDs:     labelIDs,
 | 
			
		||||
			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)
 | 
			
		||||
 | 
			
		||||
	var mileIDs []int64
 | 
			
		||||
	if milestoneID > 0 {
 | 
			
		||||
		mileIDs = []int64{milestoneID}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var issues []*models.Issue
 | 
			
		||||
	if forceEmpty {
 | 
			
		||||
		issues = []*models.Issue{}
 | 
			
		||||
| 
						 | 
				
			
			@ -203,7 +208,7 @@ func issues(ctx *context.Context, milestoneID int64, isPullOption util.OptionalB
 | 
			
		|||
			AssigneeID:   assigneeID,
 | 
			
		||||
			PosterID:     posterID,
 | 
			
		||||
			MentionedID:  mentionedID,
 | 
			
		||||
			MilestoneID: milestoneID,
 | 
			
		||||
			MilestoneIDs: mileIDs,
 | 
			
		||||
			IsClosed:     util.OptionalBoolOf(isShowClosed),
 | 
			
		||||
			IsPull:       isPullOption,
 | 
			
		||||
			LabelIDs:     labelIDs,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3768,6 +3768,12 @@
 | 
			
		|||
            "name": "type",
 | 
			
		||||
            "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",
 | 
			
		||||
            "description": "page number of results to return (1-based)",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue