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
params[:assignee_username].present?
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
def assignee
return @assignee if defined?(@assignee)
......@@ -283,7 +291,7 @@ class IssuableFinder
if assignee_id?
User.find_by(id: params[:assignee_id])
elsif assignee_username?
User.find_by_username(params[:assignee_username])
User.find_by_username(assignee_username)
else
nil
end
......
......@@ -542,10 +542,14 @@ module API
class IssueBasic < ProjectEntity
expose :closed_at
expose :closed_by, using: Entities::UserBasic
expose :labels do |issue|
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 :milestone, using: Entities::Milestone
expose :assignees, :author, using: Entities::UserBasic
......@@ -560,6 +564,8 @@ module API
expose :due_date
expose :confidential
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|
Gitlab::UrlBuilder.build(issue)
......
......@@ -18,6 +18,43 @@ module API
:title
]
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
......@@ -3,27 +3,12 @@
module API
class Issues < Grape::API
include PaginationParams
helpers Helpers::IssuesHelpers
helpers ::Gitlab::IssuableMetadata
before { authenticate_non_get! }
helpers ::Gitlab::IssuableMetadata
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?
params :issues_params_ee do
optional :weight, types: [Integer, String], integer_none_any: true, desc: 'The weight of the issue'
......@@ -35,13 +20,14 @@ module API
end
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 :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',
optional :sort, type: String, default: 'desc',
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 :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'
......@@ -50,12 +36,17 @@ module API
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 :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,
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],
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 :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 :issues_params_ee if Gitlab.ee?
......@@ -75,13 +66,25 @@ module API
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
desc "Get currently authenticated user's issues" do
success Entities::IssueBasic
end
params do
optional :state, type: String, values: %w[opened closed all], default: 'all',
desc: 'Return opened, closed, or all issues'
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`'
......@@ -92,6 +95,7 @@ module API
options = {
with: Entities::IssueBasic,
with_labels_data: declared_params[:with_labels_data],
current_user: current_user,
issuable_metadata: issuable_meta_data(issues, 'Issue')
}
......@@ -108,8 +112,6 @@ module API
success Entities::IssueBasic
end
params do
optional :state, type: String, values: %w[opened closed all], default: 'all',
desc: 'Return opened, closed, or all issues'
use :issues_params
end
get ":id/issues" do
......@@ -119,12 +121,25 @@ module API
options = {
with: Entities::IssueBasic,
with_labels_data: declared_params[:with_labels_data],
current_user: current_user,
issuable_metadata: issuable_meta_data(issues, 'Issue')
}
present issues, options
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
params do
......@@ -137,8 +152,6 @@ module API
success Entities::IssueBasic
end
params do
optional :state, type: String, values: %w[opened closed all], default: 'all',
desc: 'Return opened, closed, or all issues'
use :issues_params
end
get ":id/issues" do
......@@ -148,6 +161,7 @@ module API
options = {
with: Entities::IssueBasic,
with_labels_data: declared_params[:with_labels_data],
current_user: current_user,
project: user_project,
issuable_metadata: issuable_meta_data(issues, 'Issue')
......@@ -156,6 +170,18 @@ module API
present issues, options
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
success Entities::Issue
end
......
......@@ -14,7 +14,7 @@
"labels": {
"type": "array",
"items": {
"type": "string"
"$ref": "../../entities/label.json"
}
},
"milestone": {
......@@ -79,6 +79,8 @@
"due_date": { "type": ["date", "null"] },
"confidential": { "type": "boolean" },
"web_url": { "type": "uri" },
"has_tasks": {"type": "boolean"},
"task_status": {"type": "string"},
"time_stats": {
"time_estimate": { "type": "integer" },
"total_time_spent": { "type": "integer" },
......@@ -91,6 +93,6 @@
"state", "created_at", "updated_at", "labels",
"milestone", "assignees", "author", "user_notes_count",
"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