Commit 9ff6edf6 authored by Alexandru Croitor's avatar Alexandru Croitor

Review updates and cleanup

* Cleaned issues and issues_statistics docs
* Renamed param with_labels_data to with_labels_details
* Added spec for N+1 check when retrieving labels from issue
* Refactoed CheckAssigneesCount validation class
parent f117c032
...@@ -39,7 +39,7 @@ GET /issues?confidential=true ...@@ -39,7 +39,7 @@ GET /issues?confidential=true
| ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `state` | string | no | Return `all` issues or just those that are `opened` or `closed` | | `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. `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. Predefined names are case-insensitive. | | `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. Predefined names are case-insensitive. |
| `with_labels_data` | Boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:text_color`. Default is `false`. | | `with_labels_details`| Boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:text_color`. Default is `false`. |
| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. | | `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)_ | | `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`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ | | `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ |
...@@ -170,7 +170,7 @@ GET /groups/:id/issues?confidential=true ...@@ -170,7 +170,7 @@ GET /groups/:id/issues?confidential=true
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | | `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` | | `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. `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. Predefined names are case-insensitive. | | `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. Predefined names are case-insensitive. |
| `with_labels_data` | Boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:text_color`. Default is `false`. | | `with_labels_details`| Boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:text_color`. Default is `false`. |
| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` | | `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. | | `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)_ | | `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)_ |
...@@ -301,7 +301,7 @@ GET /projects/:id/issues?confidential=true ...@@ -301,7 +301,7 @@ GET /projects/:id/issues?confidential=true
| `iids[]` | Array[integer] | no | Return only the milestone having the given `iid` | | `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` | | `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. `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. Predefined names are case-insensitive. | | `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. Predefined names are case-insensitive. |
| `with_labels_data` | Boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:text_color`. Default is `false`. | | `with_labels_details`| Boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:text_color`. Default is `false`. |
| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. | | `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)_ | | `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`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ | | `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ |
......
This diff is collapsed.
...@@ -542,10 +542,15 @@ module API ...@@ -542,10 +542,15 @@ module API
class IssueBasic < ProjectEntity class IssueBasic < ProjectEntity
expose :closed_at expose :closed_at
expose :closed_by, using: Entities::UserBasic expose :closed_by, using: Entities::UserBasic
expose :labels do |issue|
# Avoids an N+1 query since labels are preloaded expose :labels do |issue, options|
issue.labels.map(&:title).sort if options[:with_labels_details]
::API::Entities::LabelBasic.represent(issue.labels.sort_by(&:title))
else
issue.labels.map(&:title).sort
end
end end
expose :milestone, using: Entities::Milestone expose :milestone, using: Entities::Milestone
expose :assignees, :author, using: Entities::UserBasic expose :assignees, :author, using: Entities::UserBasic
...@@ -573,15 +578,6 @@ module API ...@@ -573,15 +578,6 @@ module API
class Issue < IssueBasic class Issue < IssueBasic
include ::API::Helpers::RelatedResourcesHelpers include ::API::Helpers::RelatedResourcesHelpers
expose :labels do |issue, options|
# Avoids an N+1 query since labels are preloaded
if options[:with_labels_data]
::API::Entities::LabelBasic.represent(issue.labels.sort_by(&:title))
else
issue.labels.map(&:title).sort
end
end
expose(:has_tasks) do |issue, _| expose(:has_tasks) do |issue, _|
!issue.task_list_items.empty? !issue.task_list_items.empty?
end end
......
...@@ -31,12 +31,10 @@ module API ...@@ -31,12 +31,10 @@ module API
end end
def find_issues(args = {}) def find_issues(args = {})
# rubocop: disable CodeReuse/ActiveRecord
finder = issue_finder(args) finder = issue_finder(args)
issues = finder.execute.with_api_entity_associations issues = finder.execute.with_api_entity_associations
issues.reorder(order_options_with_tie_breaker) issues.reorder(order_options_with_tie_breaker) # rubocop: disable CodeReuse/ActiveRecord
# rubocop: enable CodeReuse/ActiveRecord
end end
def issues_statistics(args = {}) def issues_statistics(args = {})
......
...@@ -51,7 +51,7 @@ module API ...@@ -51,7 +51,7 @@ module API
end end
params :issues_params do params :issues_params do
optional :with_labels_data, type: Boolean, desc: 'Return more label data than just lable title', default: false optional :with_labels_details, type: Boolean, desc: 'Return more label data than just lable title', default: false
optional :state, type: String, values: %w[opened closed all], default: 'all', optional :state, type: String, values: %w[opened closed all], default: 'all',
desc: 'Return opened, closed, or all issues' desc: 'Return opened, closed, or all issues'
optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at', optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at',
...@@ -80,15 +80,13 @@ module API ...@@ -80,15 +80,13 @@ module API
desc "Get currently authenticated user's issues statistics" desc "Get currently authenticated user's issues statistics"
params do params do
use :issues_stats_params use :issues_stats_params
optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all], default: 'created_by_me', optional :scope, type: String, values: %w[created_by_me assigned_to_me all], default: 'created_by_me',
desc: 'Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`' desc: 'Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`'
end end
get '/issues_statistics' do get '/issues_statistics' do
authenticate! unless params[:scope] == 'all' authenticate! unless params[:scope] == 'all'
stats = issues_statistics present issues_statistics, with: Grape::Presenters::Presenter
present stats, with: Grape::Presenters::Presenter
end end
resource :issues do resource :issues do
...@@ -106,7 +104,7 @@ module API ...@@ -106,7 +104,7 @@ module API
options = { options = {
with: Entities::Issue, with: Entities::Issue,
with_labels_data: declared_params[:with_labels_data], with_labels_details: declared_params[:with_labels_details],
current_user: current_user, current_user: current_user,
issuable_metadata: issuable_meta_data(issues, 'Issue') issuable_metadata: issuable_meta_data(issues, 'Issue')
} }
...@@ -132,7 +130,7 @@ module API ...@@ -132,7 +130,7 @@ module API
options = { options = {
with: Entities::Issue, with: Entities::Issue,
with_labels_data: declared_params[:with_labels_data], with_labels_details: declared_params[:with_labels_details],
current_user: current_user, current_user: current_user,
issuable_metadata: issuable_meta_data(issues, 'Issue') issuable_metadata: issuable_meta_data(issues, 'Issue')
} }
...@@ -147,9 +145,7 @@ module API ...@@ -147,9 +145,7 @@ module API
get ":id/issues_statistics" do get ":id/issues_statistics" do
group = find_group!(params[:id]) group = find_group!(params[:id])
stats = issues_statistics(group_id: group.id, include_subgroups: true) present issues_statistics(group_id: group.id, include_subgroups: true), with: Grape::Presenters::Presenter
present stats, with: Grape::Presenters::Presenter
end end
end end
...@@ -172,7 +168,7 @@ module API ...@@ -172,7 +168,7 @@ module API
options = { options = {
with: Entities::Issue, with: Entities::Issue,
with_labels_data: declared_params[:with_labels_data], with_labels_details: declared_params[:with_labels_details],
current_user: current_user, current_user: current_user,
project: user_project, project: user_project,
issuable_metadata: issuable_meta_data(issues, 'Issue') issuable_metadata: issuable_meta_data(issues, 'Issue')
...@@ -188,9 +184,7 @@ module API ...@@ -188,9 +184,7 @@ module API
get ":id/issues_statistics" do get ":id/issues_statistics" do
project = find_project!(params[:id]) project = find_project!(params[:id])
stats = issues_statistics(project_id: project.id) present issues_statistics(project_id: project.id), with: Grape::Presenters::Presenter
present stats, with: Grape::Presenters::Presenter
end end
desc 'Get a single project issue' do desc 'Get a single project issue' do
......
...@@ -6,12 +6,8 @@ module API ...@@ -6,12 +6,8 @@ module API
def self.coerce def self.coerce
lambda do |value| lambda do |value|
case value case value
when String when String, Array
[value] Array.wrap(value)
when Array
value
when CheckAssigneesCount
value
else else
[] []
end end
...@@ -19,11 +15,11 @@ module API ...@@ -19,11 +15,11 @@ module API
end end
def validate_param!(attr_name, params) def validate_param!(attr_name, params)
unless param_allowed?(attr_name, params) return if param_allowed?(attr_name, params)
raise Grape::Exceptions::Validation,
params: [@scope.full_name(attr_name)], raise Grape::Exceptions::Validation,
message: "allows one value, but found #{params[attr_name].size}: #{params[attr_name].join(", ")}" params: [@scope.full_name(attr_name)],
end message: "allows one value, but found #{params[attr_name].size}: #{params[attr_name].join(", ")}"
end end
private private
......
...@@ -122,25 +122,25 @@ describe API::Issues do ...@@ -122,25 +122,25 @@ describe API::Issues do
expect_paginated_array_response([issue.id, closed_issue.id]) expect_paginated_array_response([issue.id, closed_issue.id])
end end
it 'returns authentication error without any scope' do context 'issues_statistics' do
get api('/issues_statistics') it 'returns authentication error without any scope' do
get api('/issues_statistics')
expect(response).to have_http_status(401) expect(response).to have_http_status(401)
end end
it 'returns authentication error when scope is assigned-to-me' do it 'returns authentication error when scope is assigned_to_me' do
get api('/issues_statistics'), params: { scope: 'assigned-to-me' } get api('/issues_statistics'), params: { scope: 'assigned_to_me' }
expect(response).to have_http_status(401) expect(response).to have_http_status(401)
end end
it 'returns authentication error when scope is created-by-me' do it 'returns authentication error when scope is created_by_me' do
get api('/issues_statistics'), params: { scope: 'created-by-me' } get api('/issues_statistics'), params: { scope: 'created_by_me' }
expect(response).to have_http_status(401) expect(response).to have_http_status(401)
end end
context 'issues_statistics' do
context 'no state is treated as all state' do context 'no state is treated as all state' do
let(:params) { {} } let(:params) { {} }
let(:counts) { { all: 2, closed: 1, opened: 1 } } let(:counts) { { all: 2, closed: 1, opened: 1 } }
...@@ -386,6 +386,31 @@ describe API::Issues do ...@@ -386,6 +386,31 @@ describe API::Issues do
end end
context 'filter by labels or label_name param' do context 'filter by labels or label_name param' do
context 'N+1' do
let(:label_b) { create(:label, title: 'foo', project: project) }
let(:label_c) { create(:label, title: 'bar', project: project) }
before do
create(:label_link, label: label_b, target: issue)
create(:label_link, label: label_c, target: issue)
end
it 'tests N+1' do
control = ActiveRecord::QueryRecorder.new do
get api('/issues', user), params: { labels: [label.title, label_b.title, label_c.title] }
end
label_d = create(:label, title: 'dar', project: project)
label_e = create(:label, title: 'ear', project: project)
create(:label_link, label: label_d, target: issue)
create(:label_link, label: label_e, target: issue)
expect do
get api('/issues', user), params: { labels: [label.title, label_b.title, label_c.title] }
end.not_to exceed_query_limit(control)
expect(issue.labels.count).to eq(5)
end
end
it 'returns an array of labeled issues' do it 'returns an array of labeled issues' do
get api('/issues', user), params: { labels: label.title } get api('/issues', user), params: { labels: label.title }
......
...@@ -28,15 +28,15 @@ shared_examples 'labeled issues with labels and label_name params' do ...@@ -28,15 +28,15 @@ shared_examples 'labeled issues with labels and label_name params' do
it_behaves_like 'returns label names' it_behaves_like 'returns label names'
end end
context 'when with_labels_data provided' do context 'when with_labels_details provided' do
context 'array of labeled issues when all labels match' do context 'array of labeled issues when all labels match' do
let(:params) { { labels: "#{label.title},#{label_b.title},#{label_c.title}", with_labels_data: true } } let(:params) { { labels: "#{label.title},#{label_b.title},#{label_c.title}", with_labels_details: true } }
it_behaves_like 'returns basic label entity' it_behaves_like 'returns basic label entity'
end end
context 'array of labeled issues when all labels match with labels param as array' do context 'array of labeled issues when all labels match with labels param as array' do
let(:params) { { labels: [label.title, label_b.title, label_c.title], with_labels_data: true } } let(:params) { { labels: [label.title, label_b.title, label_c.title], with_labels_details: true } }
it_behaves_like 'returns basic label entity' it_behaves_like 'returns basic label entity'
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment