]> source.dussan.org Git - gitea.git/commitdiff
[API] ListReleases add filter for draft and pre-releases (#16175)
author6543 <6543@obermui.de>
Thu, 17 Jun 2021 08:58:10 +0000 (10:58 +0200)
committerGitHub <noreply@github.com>
Thu, 17 Jun 2021 08:58:10 +0000 (10:58 +0200)
* 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

integrations/api_releases_test.go
integrations/api_repo_test.go
integrations/release_test.go
models/fixtures/release.yml
models/release.go
modules/context/context.go
modules/context/form.go
routers/api/v1/repo/release.go
templates/swagger/v1_json.tmpl

index 26bf752ccae9eb02a6db8f9baeece96701aeb7eb..027b282036f64fc632ecaa61f67b7fb22d797257 100644 (file)
@@ -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)
index cfd3b58d649d644d9e376754efb819228a5b4394..1ca4575508d7ddedde1d41204775cf8de67b764b 100644 (file)
@@ -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)
 
index 365bc04d8738f96f29e8ff4e67a7f43c76b39ede..ac5df315d5972f36583ed69519d7e443611ba2aa 100644 (file)
@@ -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)
 }
index 5e577d3fddbc7c84e32c59d470b8f7cb766a3535..1703f959d2687ce3ac6732bf7932eb201598a541 100644 (file)
@@ -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"
   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
index 13b8f17218c6076feea184895919749b94d05fe1..1ce88a8210c9d515d92c6c58925fa802c924eff1 100644 (file)
@@ -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().
index 492b3f80ded6fbe1263da1147e1270a64d5645f5..7b3fd2899acd9080032ac826f4cd678d255bdea9 100644 (file)
@@ -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) {
index c7b76c614c27aa29fd20d7f3fbbb847dc7e353f2..e3afad0a9046ecd38e2c830989b39e984e87df35 100644 (file)
@@ -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)
+}
index 327a2d790b6be6621bf0120bd41c7bdd35dbee84..1b52de55ffffc7b44cf89710a38b1f2eec924ad8 100644 (file)
@@ -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)
 }
 
index 23e133376778d5962d3dc415b503459fc0e62d80..18b870517ecc3bc34952903fcdf3ee1e8c3b8ab4 100644 (file)
             "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",