mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-26 12:27:06 +00:00 
			
		
		
		
	[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:
		| @@ -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 New Issue
	
	Block a user
	 6543
					6543