mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-31 19:38:23 +00:00 
			
		
		
		
	[API] ListReleases add filter for draft and pre-releases (#16175)
* invent ctx.QueryOptionalBool * [API] ListReleases add draft and pre-release filter * Add X-Total-Count header * Add a release to fixtures * Add TEST for API ListReleases
This commit is contained in:
		| @@ -7,6 +7,7 @@ package integrations | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"testing" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models" | ||||
| @@ -16,6 +17,58 @@ import ( | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| func TestAPIListReleases(t *testing.T) { | ||||
| 	defer prepareTestEnv(t)() | ||||
|  | ||||
| 	repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) | ||||
| 	user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) | ||||
| 	session := loginUser(t, user2.LowerName) | ||||
| 	token := getTokenForLoggedInUser(t, session) | ||||
|  | ||||
| 	link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/releases", user2.Name, repo.Name)) | ||||
| 	link.RawQuery = url.Values{"token": {token}}.Encode() | ||||
| 	resp := session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK) | ||||
| 	var apiReleases []*api.Release | ||||
| 	DecodeJSON(t, resp, &apiReleases) | ||||
| 	if assert.Len(t, apiReleases, 3) { | ||||
| 		for _, release := range apiReleases { | ||||
| 			switch release.ID { | ||||
| 			case 1: | ||||
| 				assert.False(t, release.IsDraft) | ||||
| 				assert.False(t, release.IsPrerelease) | ||||
| 			case 4: | ||||
| 				assert.True(t, release.IsDraft) | ||||
| 				assert.False(t, release.IsPrerelease) | ||||
| 			case 5: | ||||
| 				assert.False(t, release.IsDraft) | ||||
| 				assert.True(t, release.IsPrerelease) | ||||
| 			default: | ||||
| 				assert.NoError(t, fmt.Errorf("unexpected release: %v", release)) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// test filter | ||||
| 	testFilterByLen := func(auth bool, query url.Values, expectedLength int, msgAndArgs ...string) { | ||||
| 		link.RawQuery = query.Encode() | ||||
| 		if auth { | ||||
| 			query.Set("token", token) | ||||
| 			resp = session.MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK) | ||||
| 		} else { | ||||
| 			resp = MakeRequest(t, NewRequest(t, "GET", link.String()), http.StatusOK) | ||||
| 		} | ||||
| 		DecodeJSON(t, resp, &apiReleases) | ||||
| 		assert.Len(t, apiReleases, expectedLength, msgAndArgs) | ||||
| 	} | ||||
|  | ||||
| 	testFilterByLen(false, url.Values{"draft": {"true"}}, 0, "anon should not see drafts") | ||||
| 	testFilterByLen(true, url.Values{"draft": {"true"}}, 1, "repo owner should see drafts") | ||||
| 	testFilterByLen(true, url.Values{"draft": {"false"}}, 2, "exclude drafts") | ||||
| 	testFilterByLen(true, url.Values{"draft": {"false"}, "pre-release": {"false"}}, 1, "exclude drafts and pre-releases") | ||||
| 	testFilterByLen(true, url.Values{"pre-release": {"true"}}, 1, "only get pre-release") | ||||
| 	testFilterByLen(true, url.Values{"draft": {"true"}, "pre-release": {"true"}}, 0, "there is no pre-release draft") | ||||
| } | ||||
|  | ||||
| func createNewReleaseUsingAPI(t *testing.T, session *TestSession, token string, owner *models.User, repo *models.Repository, name, target, title, desc string) *api.Release { | ||||
| 	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/releases?token=%s", | ||||
| 		owner.Name, repo.Name, token) | ||||
|   | ||||
| @@ -223,7 +223,7 @@ func TestAPIViewRepo(t *testing.T) { | ||||
| 	DecodeJSON(t, resp, &repo) | ||||
| 	assert.EqualValues(t, 1, repo.ID) | ||||
| 	assert.EqualValues(t, "repo1", repo.Name) | ||||
| 	assert.EqualValues(t, 2, repo.Releases) | ||||
| 	assert.EqualValues(t, 3, repo.Releases) | ||||
| 	assert.EqualValues(t, 1, repo.OpenIssues) | ||||
| 	assert.EqualValues(t, 3, repo.OpenPulls) | ||||
|  | ||||
|   | ||||
| @@ -85,7 +85,7 @@ func TestCreateRelease(t *testing.T) { | ||||
| 	session := loginUser(t, "user2") | ||||
| 	createNewRelease(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", false, false) | ||||
|  | ||||
| 	checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", i18n.Tr("en", "repo.release.stable"), 3) | ||||
| 	checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", i18n.Tr("en", "repo.release.stable"), 4) | ||||
| } | ||||
|  | ||||
| func TestCreateReleasePreRelease(t *testing.T) { | ||||
| @@ -94,7 +94,7 @@ func TestCreateReleasePreRelease(t *testing.T) { | ||||
| 	session := loginUser(t, "user2") | ||||
| 	createNewRelease(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", true, false) | ||||
|  | ||||
| 	checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", i18n.Tr("en", "repo.release.prerelease"), 3) | ||||
| 	checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", i18n.Tr("en", "repo.release.prerelease"), 4) | ||||
| } | ||||
|  | ||||
| func TestCreateReleaseDraft(t *testing.T) { | ||||
| @@ -103,7 +103,7 @@ func TestCreateReleaseDraft(t *testing.T) { | ||||
| 	session := loginUser(t, "user2") | ||||
| 	createNewRelease(t, session, "/user2/repo1", "v0.0.1", "v0.0.1", false, true) | ||||
|  | ||||
| 	checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", i18n.Tr("en", "repo.release.draft"), 3) | ||||
| 	checkLatestReleaseAndCount(t, session, "/user2/repo1", "v0.0.1", i18n.Tr("en", "repo.release.draft"), 4) | ||||
| } | ||||
|  | ||||
| func TestCreateReleasePaging(t *testing.T) { | ||||
| @@ -142,7 +142,7 @@ func TestViewReleaseListNoLogin(t *testing.T) { | ||||
|  | ||||
| 	htmlDoc := NewHTMLParser(t, rsp.Body) | ||||
| 	releases := htmlDoc.Find("#release-list li.ui.grid") | ||||
| 	assert.Equal(t, 1, releases.Length()) | ||||
| 	assert.Equal(t, 2, releases.Length()) | ||||
|  | ||||
| 	links := make([]string, 0, 5) | ||||
| 	releases.Each(func(i int, s *goquery.Selection) { | ||||
| @@ -153,7 +153,7 @@ func TestViewReleaseListNoLogin(t *testing.T) { | ||||
| 		links = append(links, link) | ||||
| 	}) | ||||
|  | ||||
| 	assert.EqualValues(t, []string{"/user2/repo1/releases/tag/v1.1"}, links) | ||||
| 	assert.EqualValues(t, []string{"/user2/repo1/releases/tag/v1.0", "/user2/repo1/releases/tag/v1.1"}, links) | ||||
| } | ||||
|  | ||||
| func TestViewReleaseListLogin(t *testing.T) { | ||||
| @@ -169,7 +169,7 @@ func TestViewReleaseListLogin(t *testing.T) { | ||||
|  | ||||
| 	htmlDoc := NewHTMLParser(t, rsp.Body) | ||||
| 	releases := htmlDoc.Find("#release-list li.ui.grid") | ||||
| 	assert.Equal(t, 2, releases.Length()) | ||||
| 	assert.Equal(t, 3, releases.Length()) | ||||
|  | ||||
| 	links := make([]string, 0, 5) | ||||
| 	releases.Each(func(i int, s *goquery.Selection) { | ||||
| @@ -180,8 +180,11 @@ func TestViewReleaseListLogin(t *testing.T) { | ||||
| 		links = append(links, link) | ||||
| 	}) | ||||
|  | ||||
| 	assert.EqualValues(t, []string{"/user2/repo1/releases/tag/draft-release", | ||||
| 		"/user2/repo1/releases/tag/v1.1"}, links) | ||||
| 	assert.EqualValues(t, []string{ | ||||
| 		"/user2/repo1/releases/tag/draft-release", | ||||
| 		"/user2/repo1/releases/tag/v1.0", | ||||
| 		"/user2/repo1/releases/tag/v1.1", | ||||
| 	}, links) | ||||
| } | ||||
|  | ||||
| func TestViewTagsList(t *testing.T) { | ||||
| @@ -197,12 +200,12 @@ func TestViewTagsList(t *testing.T) { | ||||
|  | ||||
| 	htmlDoc := NewHTMLParser(t, rsp.Body) | ||||
| 	tags := htmlDoc.Find(".tag-list tr") | ||||
| 	assert.Equal(t, 2, tags.Length()) | ||||
| 	assert.Equal(t, 3, tags.Length()) | ||||
|  | ||||
| 	tagNames := make([]string, 0, 5) | ||||
| 	tags.Each(func(i int, s *goquery.Selection) { | ||||
| 		tagNames = append(tagNames, s.Find(".tag a.df.ac").Text()) | ||||
| 	}) | ||||
|  | ||||
| 	assert.EqualValues(t, []string{"delete-tag", "v1.1"}, tagNames) | ||||
| 	assert.EqualValues(t, []string{"v1.0", "delete-tag", "v1.1"}, tagNames) | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| - | ||||
|   id: 1 | ||||
| - id: 1 | ||||
|   repo_id: 1 | ||||
|   publisher_id: 2 | ||||
|   tag_name: "v1.1" | ||||
| @@ -13,8 +12,7 @@ | ||||
|   is_tag: false | ||||
|   created_unix: 946684800 | ||||
|  | ||||
| - | ||||
|   id: 2 | ||||
| - id: 2 | ||||
|   repo_id: 40 | ||||
|   publisher_id: 2 | ||||
|   tag_name: "v1.1" | ||||
| @@ -28,8 +26,7 @@ | ||||
|   is_tag: false | ||||
|   created_unix: 946684800 | ||||
|  | ||||
| - | ||||
|   id: 3 | ||||
| - id: 3 | ||||
|   repo_id: 1 | ||||
|   publisher_id: 2 | ||||
|   tag_name: "delete-tag" | ||||
| @@ -43,8 +40,7 @@ | ||||
|   is_tag: true | ||||
|   created_unix: 946684800 | ||||
|  | ||||
| - | ||||
|   id: 4 | ||||
| - id: 4 | ||||
|   repo_id: 1 | ||||
|   publisher_id: 2 | ||||
|   tag_name: "draft-release" | ||||
| @@ -55,3 +51,18 @@ | ||||
|   is_prerelease: false | ||||
|   is_tag: false | ||||
|   created_unix: 1619524806 | ||||
|  | ||||
| - id: 5 | ||||
|   repo_id: 1 | ||||
|   publisher_id: 2 | ||||
|   tag_name: "v1.0" | ||||
|   lower_tag_name: "v1.0" | ||||
|   target: "master" | ||||
|   title: "pre-release" | ||||
|   note: "some text for a pre release" | ||||
|   sha1: "65f1bf27bc3bf70f64657658635e66094edbcb4d" | ||||
|   num_commits: 1 | ||||
|   is_draft: false | ||||
|   is_prerelease: true | ||||
|   is_tag: false | ||||
|   created_unix: 946684800 | ||||
|   | ||||
| @@ -14,6 +14,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/modules/timeutil" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
|  | ||||
| 	"xorm.io/builder" | ||||
| ) | ||||
| @@ -173,6 +174,8 @@ type FindReleasesOptions struct { | ||||
| 	ListOptions | ||||
| 	IncludeDrafts bool | ||||
| 	IncludeTags   bool | ||||
| 	IsPreRelease  util.OptionalBool | ||||
| 	IsDraft       util.OptionalBool | ||||
| 	TagNames      []string | ||||
| } | ||||
|  | ||||
| @@ -189,6 +192,12 @@ func (opts *FindReleasesOptions) toConds(repoID int64) builder.Cond { | ||||
| 	if len(opts.TagNames) > 0 { | ||||
| 		cond = cond.And(builder.In("tag_name", opts.TagNames)) | ||||
| 	} | ||||
| 	if !opts.IsPreRelease.IsNone() { | ||||
| 		cond = cond.And(builder.Eq{"is_prerelease": opts.IsPreRelease.IsTrue()}) | ||||
| 	} | ||||
| 	if !opts.IsDraft.IsNone() { | ||||
| 		cond = cond.And(builder.Eq{"is_draft": opts.IsDraft.IsTrue()}) | ||||
| 	} | ||||
| 	return cond | ||||
| } | ||||
|  | ||||
| @@ -206,6 +215,11 @@ func GetReleasesByRepoID(repoID int64, opts FindReleasesOptions) ([]*Release, er | ||||
| 	return rels, sess.Find(&rels) | ||||
| } | ||||
|  | ||||
| // CountReleasesByRepoID returns a number of releases matching FindReleaseOptions and RepoID. | ||||
| func CountReleasesByRepoID(repoID int64, opts FindReleasesOptions) (int64, error) { | ||||
| 	return x.Where(opts.toConds(repoID)).Count(new(Release)) | ||||
| } | ||||
|  | ||||
| // GetLatestReleaseByRepoID returns the latest release for a repository | ||||
| func GetLatestReleaseByRepoID(repoID int64) (*Release, error) { | ||||
| 	cond := builder.NewCond(). | ||||
|   | ||||
| @@ -27,6 +27,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/templates" | ||||
| 	"code.gitea.io/gitea/modules/translation" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 	"code.gitea.io/gitea/modules/web/middleware" | ||||
| 	"code.gitea.io/gitea/services/auth" | ||||
|  | ||||
| @@ -319,6 +320,11 @@ func (ctx *Context) QueryBool(key string, defaults ...bool) bool { | ||||
| 	return (*Forms)(ctx.Req).MustBool(key, defaults...) | ||||
| } | ||||
|  | ||||
| // QueryOptionalBool returns request form as OptionalBool with default | ||||
| func (ctx *Context) QueryOptionalBool(key string, defaults ...util.OptionalBool) util.OptionalBool { | ||||
| 	return (*Forms)(ctx.Req).MustOptionalBool(key, defaults...) | ||||
| } | ||||
|  | ||||
| // HandleText handles HTTP status code | ||||
| func (ctx *Context) HandleText(status int, title string) { | ||||
| 	if (status/100 == 4) || (status/100 == 5) { | ||||
|   | ||||
| @@ -13,6 +13,7 @@ import ( | ||||
| 	"text/template" | ||||
|  | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| ) | ||||
|  | ||||
| // Forms a new enhancement of http.Request | ||||
| @@ -225,3 +226,16 @@ func (f *Forms) MustBool(key string, defaults ...bool) bool { | ||||
| 	} | ||||
| 	return v | ||||
| } | ||||
|  | ||||
| // MustOptionalBool returns request form as OptionalBool with default | ||||
| func (f *Forms) MustOptionalBool(key string, defaults ...util.OptionalBool) util.OptionalBool { | ||||
| 	value := (*http.Request)(f).FormValue(key) | ||||
| 	if len(value) == 0 { | ||||
| 		return util.OptionalBoolNone | ||||
| 	} | ||||
| 	v, err := strconv.ParseBool((*http.Request)(f).FormValue(key)) | ||||
| 	if len(defaults) > 0 && err != nil { | ||||
| 		return defaults[0] | ||||
| 	} | ||||
| 	return util.OptionalBoolOf(v) | ||||
| } | ||||
|   | ||||
| @@ -5,6 +5,7 @@ | ||||
| package repo | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models" | ||||
| @@ -83,6 +84,14 @@ func ListReleases(ctx *context.APIContext) { | ||||
| 	//   description: name of the repo | ||||
| 	//   type: string | ||||
| 	//   required: true | ||||
| 	// - name: draft | ||||
| 	//   in: query | ||||
| 	//   description: filter (exclude / include) drafts, if you dont have repo write access none will show | ||||
| 	//   type: boolean | ||||
| 	// - name: pre-release | ||||
| 	//   in: query | ||||
| 	//   description: filter (exclude / include) pre-releases | ||||
| 	//   type: boolean | ||||
| 	// - name: per_page | ||||
| 	//   in: query | ||||
| 	//   description: page size of results, deprecated - use limit | ||||
| @@ -100,15 +109,19 @@ func ListReleases(ctx *context.APIContext) { | ||||
| 	//   "200": | ||||
| 	//     "$ref": "#/responses/ReleaseList" | ||||
| 	listOptions := utils.GetListOptions(ctx) | ||||
| 	if ctx.QueryInt("per_page") != 0 { | ||||
| 	if listOptions.PageSize == 0 && ctx.QueryInt("per_page") != 0 { | ||||
| 		listOptions.PageSize = ctx.QueryInt("per_page") | ||||
| 	} | ||||
|  | ||||
| 	releases, err := models.GetReleasesByRepoID(ctx.Repo.Repository.ID, models.FindReleasesOptions{ | ||||
| 	opts := models.FindReleasesOptions{ | ||||
| 		ListOptions:   listOptions, | ||||
| 		IncludeDrafts: ctx.Repo.AccessMode >= models.AccessModeWrite, | ||||
| 		IncludeTags:   false, | ||||
| 	}) | ||||
| 		IsDraft:       ctx.QueryOptionalBool("draft"), | ||||
| 		IsPreRelease:  ctx.QueryOptionalBool("pre-release"), | ||||
| 	} | ||||
|  | ||||
| 	releases, err := models.GetReleasesByRepoID(ctx.Repo.Repository.ID, opts) | ||||
| 	if err != nil { | ||||
| 		ctx.Error(http.StatusInternalServerError, "GetReleasesByRepoID", err) | ||||
| 		return | ||||
| @@ -121,6 +134,16 @@ func ListReleases(ctx *context.APIContext) { | ||||
| 		} | ||||
| 		rels[i] = convert.ToRelease(release) | ||||
| 	} | ||||
|  | ||||
| 	filteredCount, err := models.CountReleasesByRepoID(ctx.Repo.Repository.ID, opts) | ||||
| 	if err != nil { | ||||
| 		ctx.InternalServerError(err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	ctx.SetLinkHeader(int(filteredCount), listOptions.PageSize) | ||||
| 	ctx.Header().Set("X-Total-Count", fmt.Sprint(filteredCount)) | ||||
| 	ctx.Header().Set("Access-Control-Expose-Headers", "X-Total-Count, Link") | ||||
| 	ctx.JSON(http.StatusOK, rels) | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -8076,6 +8076,18 @@ | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           }, | ||||
|           { | ||||
|             "type": "boolean", | ||||
|             "description": "filter (exclude / include) drafts, if you dont have repo write access none will show", | ||||
|             "name": "draft", | ||||
|             "in": "query" | ||||
|           }, | ||||
|           { | ||||
|             "type": "boolean", | ||||
|             "description": "filter (exclude / include) pre-releases", | ||||
|             "name": "pre-release", | ||||
|             "in": "query" | ||||
|           }, | ||||
|           { | ||||
|             "type": "integer", | ||||
|             "description": "page size of results, deprecated - use limit", | ||||
|   | ||||
		Reference in New Issue
	
	Block a user