Commit 5ec28dc3 authored by Alexandru Croitor's avatar Alexandru Croitor

Changes to issues api

When issues_controller endpoint was used for search, the parameters
passed to the controller were slightly different then the ones
passed to API. Because the searchbar UI is reused in different
places and builds the parameters passed to request in same way
we need to account for old parameter names.


Add issues_statistics api endpoints

Adds issue_statistics api endpoints for issue lists and returns
counts of issues for all, closed and opened states.


Expose more label attributes based on a param

When requesting issues list through API expose more attributes
for labels, like color, description if with_labels_data param is
being passed, avoiding this way to change response schema for users
that already use API.

https://gitlab.com/gitlab-org/gitlab-ce/issues/57402
parent 6bf5af2a
...@@ -275,6 +275,14 @@ class IssuableFinder ...@@ -275,6 +275,14 @@ class IssuableFinder
params[:assignee_username].present? params[:assignee_username].present?
end end
def assignee_username
if params[:assignee_username].is_a?(Array)
params[:assignee_username].first
else
params[:assignee_username]
end
end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def assignee def assignee
return @assignee if defined?(@assignee) return @assignee if defined?(@assignee)
...@@ -283,7 +291,7 @@ class IssuableFinder ...@@ -283,7 +291,7 @@ class IssuableFinder
if assignee_id? if assignee_id?
User.find_by(id: params[:assignee_id]) User.find_by(id: params[:assignee_id])
elsif assignee_username? elsif assignee_username?
User.find_by_username(params[:assignee_username]) User.find_by_username(assignee_username)
else else
nil nil
end end
......
...@@ -542,9 +542,13 @@ module API ...@@ -542,9 +542,13 @@ 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| expose :labels do |issue, options|
# Avoids an N+1 query since labels are preloaded # Avoids an N+1 query since labels are preloaded
issue.labels.map(&:title).sort if options[:with_labels_data]
::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
...@@ -560,6 +564,8 @@ module API ...@@ -560,6 +564,8 @@ module API
expose :due_date expose :due_date
expose :confidential expose :confidential
expose :discussion_locked expose :discussion_locked
expose(:has_tasks) {|issue, _| !issue.task_list_items.empty? }
expose :task_status, if: -> (issue, _) { !issue.task_list_items.empty? }
expose :web_url do |issue| expose :web_url do |issue|
Gitlab::UrlBuilder.build(issue) Gitlab::UrlBuilder.build(issue)
......
...@@ -18,6 +18,43 @@ module API ...@@ -18,6 +18,43 @@ module API
:title :title
] ]
end end
def issue_finder(args = {})
args = declared_params.merge(args)
args.delete(:id)
args[:milestone_title] ||= args.delete(:milestone)
args[:milestone_title] ||= args.delete(:milestone_title)
args[:label_name] ||= args.delete(:labels)
args[:scope] = args[:scope].underscore if args[:scope]
IssuesFinder.new(current_user, args)
end
def find_issues(args = {})
# rubocop: disable CodeReuse/ActiveRecord
finder = issue_finder(args)
issues = finder.execute.with_api_entity_associations
order_by = declared_params[:sort].present? && %w(asc desc).include?(declared_params[:sort].downcase)
issues = issues.reorder(order_options_with_tie_breaker) if order_by
issues
# rubocop: enable CodeReuse/ActiveRecord
end
def issues_statistics(args = {})
finder = issue_finder(args)
counter = Gitlab::IssuablesCountForState.new(finder)
{
statistics: {
counts: {
all: counter[:all],
closed: counter[:closed],
opened: counter[:opened]
}
}
}
end
end end
end end
end end
...@@ -3,27 +3,12 @@ ...@@ -3,27 +3,12 @@
module API module API
class Issues < Grape::API class Issues < Grape::API
include PaginationParams include PaginationParams
helpers Helpers::IssuesHelpers
helpers ::Gitlab::IssuableMetadata
before { authenticate_non_get! } before { authenticate_non_get! }
helpers ::Gitlab::IssuableMetadata
helpers do helpers do
# rubocop: disable CodeReuse/ActiveRecord
def find_issues(args = {})
args = declared_params.merge(args)
args.delete(:id)
args[:milestone_title] = args.delete(:milestone)
args[:label_name] = args.delete(:labels)
args[:scope] = args[:scope].underscore if args[:scope]
issues = IssuesFinder.new(current_user, args).execute
.with_api_entity_associations
issues.reorder(order_options_with_tie_breaker)
end
# rubocop: enable CodeReuse/ActiveRecord
if Gitlab.ee? if Gitlab.ee?
params :issues_params_ee do params :issues_params_ee do
optional :weight, types: [Integer, String], integer_none_any: true, desc: 'The weight of the issue' optional :weight, types: [Integer, String], integer_none_any: true, desc: 'The weight of the issue'
...@@ -35,13 +20,14 @@ module API ...@@ -35,13 +20,14 @@ module API
end end
params :issues_params do params :issues_params do
optional :labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names' optional :labels, :label_name, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names'
optional :with_labels_data, type: Boolean, desc: 'Return more label data than just lable title', default: false
optional :milestone, type: String, desc: 'Milestone title' optional :milestone, type: String, desc: 'Milestone title'
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',
desc: 'Return issues ordered by `created_at` or `updated_at` fields.' desc: 'Return issues ordered by `created_at` or `updated_at` fields.'
optional :sort, type: String, values: %w[asc desc], default: 'desc', optional :sort, type: String, default: 'desc',
desc: 'Return issues sorted in `asc` or `desc` order.' desc: 'Return issues sorted in `asc` or `desc` order.'
optional :milestone, type: String, desc: 'Return issues for a specific milestone' optional :milestone, :milestone_title, type: String, desc: 'Return issues for a specific milestone'
optional :iids, type: Array[Integer], desc: 'The IID array of issues' optional :iids, type: Array[Integer], desc: 'The IID array of issues'
optional :search, type: String, desc: 'Search issues for text present in the title, description, or any combination of these' optional :search, type: String, desc: 'Search issues for text present in the title, description, or any combination of these'
optional :in, type: String, desc: '`title`, `description`, or a string joining them with comma' optional :in, type: String, desc: '`title`, `description`, or a string joining them with comma'
...@@ -50,12 +36,17 @@ module API ...@@ -50,12 +36,17 @@ module API
optional :updated_after, type: DateTime, desc: 'Return issues updated after the specified time' optional :updated_after, type: DateTime, desc: 'Return issues updated after the specified time'
optional :updated_before, type: DateTime, desc: 'Return issues updated before the specified time' optional :updated_before, type: DateTime, desc: 'Return issues updated before the specified time'
optional :author_id, type: Integer, desc: 'Return issues which are authored by the user with the given ID' optional :author_id, type: Integer, desc: 'Return issues which are authored by the user with the given ID'
optional :author_username, type: String, desc: 'Return issues which are authored by the user with the given username'
optional :assignee_id, types: [Integer, String], integer_none_any: true, optional :assignee_id, types: [Integer, String], integer_none_any: true,
desc: 'Return issues which are assigned to the user with the given ID' desc: 'Return issues which are assigned to the user with the given ID'
optional :assignee_username, type: Array[String],
desc: 'Return issues which are assigned to the user with the given username'
optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all], optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all],
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`'
optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji' optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji'
optional :confidential, type: Boolean, desc: 'Filter confidential or public issues' optional :confidential, type: Boolean, desc: 'Filter confidential or public issues'
optional :state, type: String, values: %w[opened closed all], default: 'all',
desc: 'Return opened, closed, or all issues'
use :pagination use :pagination
use :issues_params_ee if Gitlab.ee? use :issues_params_ee if Gitlab.ee?
...@@ -75,13 +66,25 @@ module API ...@@ -75,13 +66,25 @@ module API
end end
end end
desc "Get currently authenticated user's issues statistics"
params do
use :issues_params
optional :scope, type: String, values: %w[created-by-me assigned-to-me 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`'
end
get '/issues_statistics' do
authenticate! unless params[:scope] == 'all'
stats = issues_statistics
present stats, with: Grape::Presenters::Presenter
end
resource :issues do resource :issues do
desc "Get currently authenticated user's issues" do desc "Get currently authenticated user's issues" do
success Entities::IssueBasic success Entities::IssueBasic
end end
params do params do
optional :state, type: String, values: %w[opened closed all], default: 'all',
desc: 'Return opened, closed, or all issues'
use :issues_params use :issues_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 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`'
...@@ -92,6 +95,7 @@ module API ...@@ -92,6 +95,7 @@ module API
options = { options = {
with: Entities::IssueBasic, with: Entities::IssueBasic,
with_labels_data: declared_params[:with_labels_data],
current_user: current_user, current_user: current_user,
issuable_metadata: issuable_meta_data(issues, 'Issue') issuable_metadata: issuable_meta_data(issues, 'Issue')
} }
...@@ -108,8 +112,6 @@ module API ...@@ -108,8 +112,6 @@ module API
success Entities::IssueBasic success Entities::IssueBasic
end end
params do params do
optional :state, type: String, values: %w[opened closed all], default: 'all',
desc: 'Return opened, closed, or all issues'
use :issues_params use :issues_params
end end
get ":id/issues" do get ":id/issues" do
...@@ -119,12 +121,25 @@ module API ...@@ -119,12 +121,25 @@ module API
options = { options = {
with: Entities::IssueBasic, with: Entities::IssueBasic,
with_labels_data: declared_params[:with_labels_data],
current_user: current_user, current_user: current_user,
issuable_metadata: issuable_meta_data(issues, 'Issue') issuable_metadata: issuable_meta_data(issues, 'Issue')
} }
present issues, options present issues, options
end end
desc 'Get statistics for the list of group issues'
params do
use :issues_params
end
get ":id/issues_statistics" do
group = find_group!(params[:id])
stats = issues_statistics(group_id: group.id, include_subgroups: true)
present stats, with: Grape::Presenters::Presenter
end
end end
params do params do
...@@ -137,8 +152,6 @@ module API ...@@ -137,8 +152,6 @@ module API
success Entities::IssueBasic success Entities::IssueBasic
end end
params do params do
optional :state, type: String, values: %w[opened closed all], default: 'all',
desc: 'Return opened, closed, or all issues'
use :issues_params use :issues_params
end end
get ":id/issues" do get ":id/issues" do
...@@ -148,6 +161,7 @@ module API ...@@ -148,6 +161,7 @@ module API
options = { options = {
with: Entities::IssueBasic, with: Entities::IssueBasic,
with_labels_data: declared_params[:with_labels_data],
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')
...@@ -156,6 +170,18 @@ module API ...@@ -156,6 +170,18 @@ module API
present issues, options present issues, options
end end
desc 'Get statistics for the list of project issues'
params do
use :issues_params
end
get ":id/issues_statistics" do
project = find_project!(params[:id])
stats = issues_statistics(project_id: project.id)
present stats, with: Grape::Presenters::Presenter
end
desc 'Get a single project issue' do desc 'Get a single project issue' do
success Entities::Issue success Entities::Issue
end end
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
"labels": { "labels": {
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "$ref": "../../entities/label.json"
} }
}, },
"milestone": { "milestone": {
...@@ -79,6 +79,8 @@ ...@@ -79,6 +79,8 @@
"due_date": { "type": ["date", "null"] }, "due_date": { "type": ["date", "null"] },
"confidential": { "type": "boolean" }, "confidential": { "type": "boolean" },
"web_url": { "type": "uri" }, "web_url": { "type": "uri" },
"has_tasks": {"type": "boolean"},
"task_status": {"type": "string"},
"time_stats": { "time_stats": {
"time_estimate": { "type": "integer" }, "time_estimate": { "type": "integer" },
"total_time_spent": { "type": "integer" }, "total_time_spent": { "type": "integer" },
...@@ -91,6 +93,6 @@ ...@@ -91,6 +93,6 @@
"state", "created_at", "updated_at", "labels", "state", "created_at", "updated_at", "labels",
"milestone", "assignees", "author", "user_notes_count", "milestone", "assignees", "author", "user_notes_count",
"upvotes", "downvotes", "due_date", "confidential", "upvotes", "downvotes", "due_date", "confidential",
"web_url" "has_tasks", "web_url"
] ]
} }
{
"type": "object",
"required": [
"id",
"name",
"color",
"description",
"text_color"
],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"color": {
"type": "string",
"pattern": "^#[0-9A-Fa-f]{3}{1,2}$"
},
"description": { "type": ["string", "null"] },
"text_color": {
"type": "string",
"pattern": "^#[0-9A-Fa-f]{3}{1,2}$"
}
},
"additionalProperties": false
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
# frozen_string_literal: true
shared_examples 'labeled issues with labels and label_name params' do
shared_examples 'returns label names' do
it 'returns label names' do
expect_paginated_array_response(issue.id)
expect(json_response.first['labels']).to eq([label_c.title, label_b.title, label.title])
end
end
shared_examples 'returns basic label entity' do
it 'returns basic label entity' do
expect_paginated_array_response(issue.id)
expect(json_response.first['labels'].pluck('name')).to eq([label_c.title, label_b.title, label.title])
expect(json_response.first['labels'].first).to match_schema('/public_api/v4/label_basic')
end
end
context 'array of labeled issues when all labels match' do
let(:params) { { labels: "#{label.title},#{label_b.title},#{label_c.title}" } }
it_behaves_like 'returns label names'
end
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] } }
it_behaves_like 'returns label names'
end
context 'array of labeled issues when all labels match the label_name param' do
let(:params) { { label_name: "#{label.title},#{label_b.title},#{label_c.title}" } }
it_behaves_like 'returns label names'
end
context 'array of labeled issues when all labels match with label_name param as array' do
let(:params) { { label_name: [label.title, label_b.title, label_c.title] } }
it_behaves_like 'returns label names'
end
context 'with labels data' 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 } }
it_behaves_like 'returns basic label entity'
end
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 } }
it_behaves_like 'returns basic label entity'
end
context 'array of labeled issues when all labels match the label_name param' do
let(:params) { { label_name: "#{label.title},#{label_b.title},#{label_c.title}", with_labels_data: true } }
it_behaves_like 'returns basic label entity'
end
context 'array of labeled issues when all labels match with label_name param as array' do
let(:params) { { label_name: [label.title, label_b.title, label_c.title], with_labels_data: true } }
it_behaves_like 'returns basic label entity'
end
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