Commit 23660f3a authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'ce-57402-add-issues-statistics-api-endpoints' into 'master'

Add issues_statistics api endpoints

See merge request gitlab-org/gitlab-ce!27366
parents f1456594 9ff6edf6
---
title: Add issues_statistics api endpoints and extend issues search api
merge_request: 27366
author:
type: added
This diff is collapsed.
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|
if options[:with_labels_details]
::API::Entities::LabelBasic.represent(issue.labels.sort_by(&:title))
else
issue.labels.map(&:title).sort 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,6 +578,14 @@ module API ...@@ -573,6 +578,14 @@ module API
class Issue < IssueBasic class Issue < IssueBasic
include ::API::Helpers::RelatedResourcesHelpers include ::API::Helpers::RelatedResourcesHelpers
expose(:has_tasks) do |issue, _|
!issue.task_list_items.empty?
end
expose :task_status, if: -> (issue, _) do
!issue.task_list_items.empty?
end
expose :_links do expose :_links do
expose :self do |issue| expose :self do |issue|
expose_url(api_v4_project_issue_path(id: issue.project_id, issue_iid: issue.iid)) expose_url(api_v4_project_issue_path(id: issue.project_id, issue_iid: issue.iid))
......
...@@ -18,6 +18,39 @@ module API ...@@ -18,6 +18,39 @@ 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[:label_name] ||= args.delete(:labels)
args[:scope] = args[:scope].underscore if args[:scope]
IssuesFinder.new(current_user, args)
end
def find_issues(args = {})
finder = issue_finder(args)
issues = finder.execute.with_api_entity_associations
issues.reorder(order_options_with_tie_breaker) # rubocop: disable 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'
...@@ -34,13 +19,9 @@ module API ...@@ -34,13 +19,9 @@ module API
end end
end end
params :issues_params do params :issues_stats_params do
optional :labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names' optional :labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names'
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',
desc: 'Return issues ordered by `created_at` or `updated_at` fields.'
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return issues sorted in `asc` or `desc` order.'
optional :milestone, type: String, desc: 'Return issues for a specific milestone' optional :milestone, 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'
...@@ -49,18 +30,39 @@ module API ...@@ -49,18 +30,39 @@ module API
optional :created_before, type: DateTime, desc: 'Return issues created before the specified time' optional :created_before, type: DateTime, desc: 'Return issues created before the specified time'
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'
mutually_exclusive :author_id, :author_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], check_assignees_count: true,
coerce_with: Validations::CheckAssigneesCount.coerce,
desc: 'Return issues which are assigned to the user with the given username'
mutually_exclusive :assignee_id, :assignee_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'
use :pagination
use :issues_params_ee if Gitlab.ee? use :issues_params_ee if Gitlab.ee?
end end
params :issues_params do
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',
desc: 'Return opened, closed, or all issues'
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.'
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return issues sorted in `asc` or `desc` order.'
use :issues_stats_params
use :pagination
end
params :issue_params do params :issue_params do
optional :description, type: String, desc: 'The description of an issue' optional :description, type: String, desc: 'The description of an issue'
optional :assignee_ids, type: Array[Integer], desc: 'The array of user IDs to assign issue' optional :assignee_ids, type: Array[Integer], desc: 'The array of user IDs to assign issue'
...@@ -75,13 +77,23 @@ module API ...@@ -75,13 +77,23 @@ module API
end end
end end
desc "Get currently authenticated user's issues statistics"
params do
use :issues_stats_params
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`'
end
get '/issues_statistics' do
authenticate! unless params[:scope] == 'all'
present issues_statistics, 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::Issue
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`'
...@@ -91,7 +103,8 @@ module API ...@@ -91,7 +103,8 @@ module API
issues = paginate(find_issues) issues = paginate(find_issues)
options = { options = {
with: Entities::IssueBasic, with: Entities::Issue,
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')
} }
...@@ -105,11 +118,9 @@ module API ...@@ -105,11 +118,9 @@ module API
end end
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a list of group issues' do desc 'Get a list of group issues' do
success Entities::IssueBasic success Entities::Issue
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
...@@ -118,13 +129,24 @@ module API ...@@ -118,13 +129,24 @@ module API
issues = paginate(find_issues(group_id: group.id, include_subgroups: true)) issues = paginate(find_issues(group_id: group.id, include_subgroups: true))
options = { options = {
with: Entities::IssueBasic, with: Entities::Issue,
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')
} }
present issues, options present issues, options
end end
desc 'Get statistics for the list of group issues'
params do
use :issues_stats_params
end
get ":id/issues_statistics" do
group = find_group!(params[:id])
present issues_statistics(group_id: group.id, include_subgroups: true), with: Grape::Presenters::Presenter
end
end end
params do params do
...@@ -134,11 +156,9 @@ module API ...@@ -134,11 +156,9 @@ module API
include TimeTrackingEndpoints include TimeTrackingEndpoints
desc 'Get a list of project issues' do desc 'Get a list of project issues' do
success Entities::IssueBasic success Entities::Issue
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
...@@ -147,7 +167,8 @@ module API ...@@ -147,7 +167,8 @@ module API
issues = paginate(find_issues(project_id: project.id)) issues = paginate(find_issues(project_id: project.id))
options = { options = {
with: Entities::IssueBasic, with: Entities::Issue,
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')
...@@ -156,6 +177,16 @@ module API ...@@ -156,6 +177,16 @@ module API
present issues, options present issues, options
end end
desc 'Get statistics for the list of project issues'
params do
use :issues_stats_params
end
get ":id/issues_statistics" do
project = find_project!(params[:id])
present issues_statistics(project_id: project.id), 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
......
# frozen_string_literal: true
module API
module Validations
class CheckAssigneesCount < Grape::Validations::Base
def self.coerce
lambda do |value|
case value
when String, Array
Array.wrap(value)
else
[]
end
end
end
def validate_param!(attr_name, params)
return if param_allowed?(attr_name, params)
raise Grape::Exceptions::Validation,
params: [@scope.full_name(attr_name)],
message: "allows one value, but found #{params[attr_name].size}: #{params[attr_name].join(", ")}"
end
private
def param_allowed?(attr_name, params)
params[attr_name].size <= 1
end
end
end
end
{
"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 'when with_labels_details provided' do
context 'array of labeled issues when all labels match' do
let(:params) { { labels: "#{label.title},#{label_b.title},#{label_c.title}", with_labels_details: 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_details: 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