diff options
author | Jacopo <beschi.jacopo@gmail.com> | 2018-10-29 10:50:18 +0100 |
---|---|---|
committer | Jacopo <beschi.jacopo@gmail.com> | 2018-11-22 10:49:05 +0100 |
commit | f11a54d4a380f17ce17ef42d90be6f0ff9a39c8d (patch) | |
tree | 9250ab32a73b177e81decd42f9baf91dfa659156 | |
parent | f0630090aaea98daef3582bc95efe3a43736a10f (diff) | |
download | gitlab-ce-52371-filter-by-none-any-for-labels-in-issues-mrs-api.tar.gz |
Filter by `None`/`Any` for labels in issues/mrs API52371-filter-by-none-any-for-labels-in-issues-mrs-api
By using the parameters `?labels=None|Any` the user can filter
issues/mrs from the API that has `none/any` label.
Affected endpoints are:
- /api/issues
- /api/projects/:id/issues
- /api/groups/:id/issues
- /api/merge_requests
- /api/projects/:id/merge_requests
- /api/groups/:id/merge_requests
-rw-r--r-- | app/finders/issuable_finder.rb | 11 | ||||
-rw-r--r-- | app/models/concerns/issuable.rb | 1 | ||||
-rw-r--r-- | app/models/label.rb | 9 | ||||
-rw-r--r-- | changelogs/unreleased/52371-filter-by-none-any-for-labels-in-issues-mrs-api.yml | 5 | ||||
-rw-r--r-- | doc/api/issues.md | 6 | ||||
-rw-r--r-- | doc/api/merge_requests.md | 6 | ||||
-rw-r--r-- | spec/finders/issues_finder_spec.rb | 36 | ||||
-rw-r--r-- | spec/requests/api/issues_spec.rb | 115 | ||||
-rw-r--r-- | spec/support/shared_examples/requests/api/merge_requests_list.rb | 17 |
9 files changed, 155 insertions, 51 deletions
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index fdc630cbf72..e04e3a2a7e0 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -210,7 +210,14 @@ class IssuableFinder end def filter_by_no_label? - labels? && params[:label_name].include?(Label::None.title) + downcased = label_names.map(&:downcase) + + # Label::NONE is deprecated and should be removed in 12.0 + downcased.include?(FILTER_NONE) || downcased.include?(Label::NONE) + end + + def filter_by_any_label? + label_names.map(&:downcase).include?(FILTER_ANY) end def labels @@ -465,6 +472,8 @@ class IssuableFinder items = if filter_by_no_label? items.without_label + elsif filter_by_any_label? + items.any_label else items.with_label(label_names, params[:sort]) end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 69c5affe142..5080fe03cc8 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -90,6 +90,7 @@ module Issuable scope :order_milestone_due_asc, -> { left_joins_milestones.reorder('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date ASC') } scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) } + scope :any_label, -> { joins(:label_links).group(:id) } scope :join_project, -> { joins(:project) } scope :inc_notes_with_associations, -> { includes(notes: [:project, :author, :award_emoji]) } scope :references_project, -> { references(:project) } diff --git a/app/models/label.rb b/app/models/label.rb index 165e4a8f3e5..5d2d1afd1d9 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -9,15 +9,10 @@ class Label < ActiveRecord::Base include Sortable include FromUnion - # Represents a "No Label" state used for filtering Issues and Merge - # Requests that have no label assigned. - LabelStruct = Struct.new(:title, :name) - None = LabelStruct.new('No Label', 'No Label') - Any = LabelStruct.new('Any Label', '') - cache_markdown_field :description, pipeline: :single_line - DEFAULT_COLOR = '#428BCA'.freeze + DEFAULT_COLOR = '#428BCA' + NONE = 'no label' default_value_for :color, DEFAULT_COLOR diff --git a/changelogs/unreleased/52371-filter-by-none-any-for-labels-in-issues-mrs-api.yml b/changelogs/unreleased/52371-filter-by-none-any-for-labels-in-issues-mrs-api.yml new file mode 100644 index 00000000000..bb196af3e90 --- /dev/null +++ b/changelogs/unreleased/52371-filter-by-none-any-for-labels-in-issues-mrs-api.yml @@ -0,0 +1,5 @@ +--- +title: Filter by None/Any for labels in issues/mrs API +merge_request: 22622 +author: Jacopo Beschi @jacopo-beschi +type: added diff --git a/doc/api/issues.md b/doc/api/issues.md index 0e97d9ea6f5..9e245f820a3 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -36,7 +36,7 @@ GET /issues?my_reaction_emoji=star | Attribute | Type | Required | Description | | ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `state` | string | no | Return all issues or just those that are `opened` or `closed` | -| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels | +| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. `No+Label` (Deprecated) lists all issues with no labels. | | `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. | | `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ | | `author_id` | integer | no | Return issues created by the given user `id`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ | @@ -149,7 +149,7 @@ GET /groups/:id/issues?my_reaction_emoji=star | ------------------- | ---------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | | `state` | string | no | Return all issues or just those that are `opened` or `closed` | -| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels | +| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. `No+Label` (Deprecated) lists all issues with no labels. | | `iids[]` | Array[integer] | no | Return only the issues having the given `iid` | | `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. | | `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ | @@ -264,7 +264,7 @@ GET /projects/:id/issues?my_reaction_emoji=star | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `iids[]` | Array[integer] | no | Return only the milestone having the given `iid` | | `state` | string | no | Return all issues or just those that are `opened` or `closed` | -| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels | +| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. `No+Label` (Deprecated) lists all issues with no labels. | | `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. | | `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ | | `author_id` | integer | no | Return issues created by the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ | diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 9cb3f0d9c0c..30b31a04475 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -35,7 +35,7 @@ Parameters: | `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` | | `milestone` | string | no | Return merge requests for a specific milestone. `None` returns merge requests with no milestone. `Any` returns merge requests that have an assigned milestone. | | `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request | -| `labels` | string | no | Return merge requests matching a comma separated list of labels | +| `labels` | string | no | Return merge requests matching a comma separated list of labels. `None` lists all merge requests with no labels. `Any` lists all merge requests with at least one label. `No+Label` (Deprecated) lists all merge requests with no labels. | | `created_after` | datetime | no | Return merge requests created on or after the given time | | `created_before` | datetime | no | Return merge requests created on or before the given time | | `updated_after` | datetime | no | Return merge requests updated on or after the given time | @@ -170,7 +170,7 @@ Parameters: | `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` | | `milestone` | string | no | Return merge requests for a specific milestone. `None` returns merge requests with no milestone. `Any` returns merge requests that have an assigned milestone. | | `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request | -| `labels` | string | no | Return merge requests matching a comma separated list of labels | +| `labels` | string | no | Return merge requests matching a comma separated list of labels. `None` lists all merge requests with no labels. `Any` lists all merge requests with at least one label. `No+Label` (Deprecated) lists all merge requests with no labels. | | `created_after` | datetime | no | Return merge requests created on or after the given time | | `created_before` | datetime | no | Return merge requests created on or before the given time | | `updated_after` | datetime | no | Return merge requests updated on or after the given time | @@ -294,7 +294,7 @@ Parameters: | `sort` | string | no | Return merge requests sorted in `asc` or `desc` order. Default is `desc` | | `milestone` | string | no | Return merge requests for a specific milestone. `None` returns merge requests with no milestone. `Any` returns merge requests that have an assigned milestone. | | `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request | -| `labels` | string | no | Return merge requests matching a comma separated list of labels | +| `labels` | string | no | Return merge requests matching a comma separated list of labels. `None` lists all merge requests with no labels. `Any` lists all merge requests with at least one label. `No+Label` (Deprecated) lists all merge requests with no labels. | | `created_after` | datetime | no | Return merge requests created on or after the given time | | `created_before` | datetime | no | Return merge requests created on or before the given time | | `updated_after` | datetime | no | Return merge requests updated on or after the given time | diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index c0488c83bd8..515f6f70b99 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -256,19 +256,51 @@ describe IssuesFinder do create(:label_link, label: label2, target: issue2) end - it 'returns the unique issues with any of those labels' do + it 'returns the unique issues with all those labels' do + expect(issues).to contain_exactly(issue2) + end + end + + context 'filtering by a label that includes any or none in the title' do + let(:params) { { label_name: [label.title, label2.title].join(',') } } + let(:label) { create(:label, title: 'any foo', project: project2) } + let(:label2) { create(:label, title: 'bar none', project: project2) } + + it 'returns the unique issues with all those labels' do + create(:label_link, label: label2, target: issue2) + expect(issues).to contain_exactly(issue2) end end context 'filtering by no label' do - let(:params) { { label_name: Label::None.title } } + let(:params) { { label_name: described_class::FILTER_NONE } } it 'returns issues with no labels' do expect(issues).to contain_exactly(issue1, issue3, issue4) end end + context 'filtering by legacy No+Label' do + let(:params) { { label_name: Label::NONE } } + + it 'returns issues with no labels' do + expect(issues).to contain_exactly(issue1, issue3, issue4) + end + end + + context 'filtering by any label' do + let(:params) { { label_name: described_class::FILTER_ANY } } + + it 'returns issues that have one or more label' do + 2.times do + create(:label_link, label: create(:label, project: project2), target: issue3) + end + + expect(issues).to contain_exactly(issue2, issue3) + end + end + context 'filtering by issue term' do let(:params) { { search: 'git' } } diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 3d532dd83c7..1827da61e2d 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -300,17 +300,31 @@ describe API::Issues do expect(json_response.first['state']).to eq('opened') end - it 'returns unlabeled issues for "No Label" label' do - get api("/issues", user), labels: 'No Label' + it 'returns an empty array if no issue matches labels and state filters' do + get api("/issues", user), labels: label.title, state: :closed + + expect_paginated_array_response(size: 0) + end + + it 'returns an array of issues with any label' do + get api("/issues", user), labels: IssuesFinder::FILTER_ANY expect_paginated_array_response(size: 1) - expect(json_response.first['labels']).to be_empty + expect(json_response.first['id']).to eq(issue.id) end - it 'returns an empty array if no issue matches labels and state filters' do - get api("/issues?labels=#{label.title}&state=closed", user) + it 'returns an array of issues with no label' do + get api("/issues", user), labels: IssuesFinder::FILTER_NONE - expect_paginated_array_response(size: 0) + expect_paginated_array_response(size: 1) + expect(json_response.first['id']).to eq(closed_issue.id) + end + + it 'returns an array of issues with no label when using the legacy No+Label filter' do + get api("/issues", user), labels: "No Label" + + expect_paginated_array_response(size: 1) + expect(json_response.first['id']).to eq(closed_issue.id) end it 'returns an empty array if no issue matches milestone' do @@ -492,58 +506,58 @@ describe API::Issues do end it 'returns group issues without confidential issues for non project members' do - get api("#{base_url}?state=opened", non_member) + get api(base_url, non_member), state: :opened expect_paginated_array_response(size: 1) expect(json_response.first['title']).to eq(group_issue.title) end it 'returns group confidential issues for author' do - get api("#{base_url}?state=opened", author) + get api(base_url, author), state: :opened expect_paginated_array_response(size: 2) end it 'returns group confidential issues for assignee' do - get api("#{base_url}?state=opened", assignee) + get api(base_url, assignee), state: :opened expect_paginated_array_response(size: 2) end it 'returns group issues with confidential issues for project members' do - get api("#{base_url}?state=opened", user) + get api(base_url, user), state: :opened expect_paginated_array_response(size: 2) end it 'returns group confidential issues for admin' do - get api("#{base_url}?state=opened", admin) + get api(base_url, admin), state: :opened expect_paginated_array_response(size: 2) end it 'returns an array of labeled group issues' do - get api("#{base_url}?labels=#{group_label.title}", user) + get api(base_url, user), labels: group_label.title expect_paginated_array_response(size: 1) expect(json_response.first['labels']).to eq([group_label.title]) end it 'returns an array of labeled group issues where all labels match' do - get api("#{base_url}?labels=#{group_label.title},foo,bar", user) + get api(base_url, user), labels: "#{group_label.title},foo,bar" expect_paginated_array_response(size: 0) end it 'returns issues matching given search string for title' do - get api("#{base_url}?search=#{group_issue.title}", user) + get api(base_url, user), search: group_issue.title expect_paginated_array_response(size: 1) expect(json_response.first['id']).to eq(group_issue.id) end it 'returns issues matching given search string for description' do - get api("#{base_url}?search=#{group_issue.description}", user) + get api(base_url, user), search: group_issue.description expect_paginated_array_response(size: 1) expect(json_response.first['id']).to eq(group_issue.id) @@ -556,7 +570,7 @@ describe API::Issues do create(:label_link, label: label_b, target: group_issue) create(:label_link, label: label_c, target: group_issue) - get api("#{base_url}", user), labels: "#{group_label.title},#{label_b.title},#{label_c.title}" + get api(base_url, user), labels: "#{group_label.title},#{label_b.title},#{label_c.title}" expect_paginated_array_response(size: 1) expect(json_response.first['labels']).to eq([label_c.title, label_b.title, group_label.title]) @@ -576,40 +590,55 @@ describe API::Issues do end it 'returns an empty array if no group issue matches labels' do - get api("#{base_url}?labels=foo,bar", user) + get api(base_url, user), labels: 'foo,bar' expect_paginated_array_response(size: 0) end + it 'returns an array of group issues with any label' do + get api(base_url, user), labels: IssuesFinder::FILTER_ANY + + expect_paginated_array_response(size: 1) + expect(json_response.first['id']).to eq(group_issue.id) + end + + it 'returns an array of group issues with no label' do + get api(base_url, user), labels: IssuesFinder::FILTER_NONE + + response_ids = json_response.map { |issue| issue['id'] } + + expect_paginated_array_response(size: 2) + expect(response_ids).to contain_exactly(group_closed_issue.id, group_confidential_issue.id) + end + it 'returns an empty array if no issue matches milestone' do - get api("#{base_url}?milestone=#{group_empty_milestone.title}", user) + get api(base_url, user), milestone: group_empty_milestone.title expect_paginated_array_response(size: 0) end it 'returns an empty array if milestone does not exist' do - get api("#{base_url}?milestone=foo", user) + get api(base_url, user), milestone: 'foo' expect_paginated_array_response(size: 0) end it 'returns an array of issues in given milestone' do - get api("#{base_url}?state=opened&milestone=#{group_milestone.title}", user) + get api(base_url, user), state: :opened, milestone: group_milestone.title expect_paginated_array_response(size: 1) expect(json_response.first['id']).to eq(group_issue.id) end it 'returns an array of issues matching state in milestone' do - get api("#{base_url}?milestone=#{group_milestone.title}"\ - '&state=closed', user) + get api(base_url, user), milestone: group_milestone.title, state: :closed expect_paginated_array_response(size: 1) expect(json_response.first['id']).to eq(group_closed_issue.id) end it 'returns an array of issues with no milestone' do - get api("#{base_url}?milestone=#{no_milestone_title}", user) + get api(base_url, user), milestone: no_milestone_title expect(response).to have_gitlab_http_status(200) @@ -645,7 +674,7 @@ describe API::Issues do end it 'sorts by updated_at ascending when requested' do - get api("#{base_url}?order_by=updated_at&sort=asc", user) + get api(base_url, user), order_by: :updated_at, sort: :asc response_dates = json_response.map { |issue| issue['updated_at'] } @@ -748,7 +777,7 @@ describe API::Issues do end it 'returns an array of labeled project issues' do - get api("#{base_url}/issues?labels=#{label.title}", user) + get api("#{base_url}/issues", user), labels: label.title expect_paginated_array_response(size: 1) expect(json_response.first['labels']).to eq([label.title]) @@ -800,26 +829,42 @@ describe API::Issues do expect_paginated_array_response(size: 0) end + it 'returns an array of project issues with any label' do + get api("#{base_url}/issues", user), labels: IssuesFinder::FILTER_ANY + + expect_paginated_array_response(size: 1) + expect(json_response.first['id']).to eq(issue.id) + end + + it 'returns an array of project issues with no label' do + get api("#{base_url}/issues", user), labels: IssuesFinder::FILTER_NONE + + response_ids = json_response.map { |issue| issue['id'] } + + expect_paginated_array_response(size: 2) + expect(response_ids).to contain_exactly(closed_issue.id, confidential_issue.id) + end + it 'returns an empty array if no project issue matches labels' do - get api("#{base_url}/issues?labels=foo,bar", user) + get api("#{base_url}/issues", user), labels: 'foo,bar' expect_paginated_array_response(size: 0) end it 'returns an empty array if no issue matches milestone' do - get api("#{base_url}/issues?milestone=#{empty_milestone.title}", user) + get api("#{base_url}/issues", user), milestone: empty_milestone.title expect_paginated_array_response(size: 0) end it 'returns an empty array if milestone does not exist' do - get api("#{base_url}/issues?milestone=foo", user) + get api("#{base_url}/issues", user), milestone: :foo expect_paginated_array_response(size: 0) end it 'returns an array of issues in given milestone' do - get api("#{base_url}/issues?milestone=#{milestone.title}", user) + get api("#{base_url}/issues", user), milestone: milestone.title expect_paginated_array_response(size: 2) expect(json_response.first['id']).to eq(issue.id) @@ -827,21 +872,21 @@ describe API::Issues do end it 'returns an array of issues matching state in milestone' do - get api("#{base_url}/issues?milestone=#{milestone.title}&state=closed", user) + get api("#{base_url}/issues", user), milestone: milestone.title, state: :closed expect_paginated_array_response(size: 1) expect(json_response.first['id']).to eq(closed_issue.id) end it 'returns an array of issues with no milestone' do - get api("#{base_url}/issues?milestone=#{no_milestone_title}", user) + get api("#{base_url}/issues", user), milestone: no_milestone_title expect_paginated_array_response(size: 1) expect(json_response.first['id']).to eq(confidential_issue.id) end it 'returns an array of issues with any milestone' do - get api("#{base_url}/issues?milestone=#{any_milestone_title}", user) + get api("#{base_url}/issues", user), milestone: any_milestone_title response_ids = json_response.map { |issue| issue['id'] } @@ -859,7 +904,7 @@ describe API::Issues do end it 'sorts ascending when requested' do - get api("#{base_url}/issues?sort=asc", user) + get api("#{base_url}/issues", user), sort: :asc response_dates = json_response.map { |issue| issue['created_at'] } @@ -868,7 +913,7 @@ describe API::Issues do end it 'sorts by updated_at descending when requested' do - get api("#{base_url}/issues?order_by=updated_at", user) + get api("#{base_url}/issues", user), order_by: :updated_at response_dates = json_response.map { |issue| issue['updated_at'] } @@ -877,7 +922,7 @@ describe API::Issues do end it 'sorts by updated_at ascending when requested' do - get api("#{base_url}/issues?order_by=updated_at&sort=asc", user) + get api("#{base_url}/issues", user), order_by: :updated_at, sort: :asc response_dates = json_response.map { |issue| issue['updated_at'] } diff --git a/spec/support/shared_examples/requests/api/merge_requests_list.rb b/spec/support/shared_examples/requests/api/merge_requests_list.rb index 668a390b5d2..92d4dd598d5 100644 --- a/spec/support/shared_examples/requests/api/merge_requests_list.rb +++ b/spec/support/shared_examples/requests/api/merge_requests_list.rb @@ -186,6 +186,23 @@ shared_examples 'merge requests list' do expect(json_response.length).to eq(0) end + it 'returns an array of merge requests with any label when filtering by any label' do + get api(endpoint_path, user), labels: IssuesFinder::FILTER_ANY + + expect_paginated_array_response + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(merge_request.id) + end + + it 'returns an array of merge requests without a label when filtering by no label' do + get api(endpoint_path, user), labels: IssuesFinder::FILTER_NONE + + response_ids = json_response.map { |merge_request| merge_request['id'] } + + expect_paginated_array_response + expect(response_ids).to contain_exactly(merge_request_closed.id, merge_request_merged.id, merge_request_locked.id) + end + it 'returns an array of labeled merge requests that are merged for a milestone' do bug_label = create(:label, title: 'bug', color: '#FFAABB', project: project) |