Commit ae38f42f authored by Jan Provaznik's avatar Jan Provaznik

Merge branch 'backend-issue_34239' into 'master'

Expose blocked by issue on IssueEntity

See merge request gitlab-org/gitlab!25723
parents 9fc8ba36 283e5433
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
%section.issuable-discussion.js-vue-notes-event %section.issuable-discussion.js-vue-notes-event
#js-vue-notes{ data: { notes_data: notes_data(@issue).to_json, #js-vue-notes{ data: { notes_data: notes_data(@issue).to_json,
noteable_data: serialize_issuable(@issue), noteable_data: serialize_issuable(@issue, with_blocking_issues: Feature.enabled?(:prevent_closing_blocked_issues, @issue.project)),
noteable_type: 'Issue', noteable_type: 'Issue',
target_type: 'issue', target_type: 'issue',
current_user_data: UserSerializer.new.represent(current_user, {only_path: true}, CurrentUserEntity).to_json } } current_user_data: UserSerializer.new.represent(current_user, {only_path: true}, CurrentUserEntity).to_json } }
...@@ -65,6 +65,20 @@ module EE ...@@ -65,6 +65,20 @@ module EE
project.feature_available?(:multiple_issue_assignees) project.feature_available?(:multiple_issue_assignees)
end end
def blocked?
blocking_issues_ids.any?
end
# Used on EE::IssueEntity to expose blocking issues URLs
def blocked_by_issues(user)
return ::Issue.none unless blocked?
issues =
::IssuesFinder.new(user).execute.where(id: blocking_issues_ids)
issues.preload(project: [:route, { namespace: [:route] }])
end
# override # override
def subscribed_without_subscriptions?(user, *) def subscribed_without_subscriptions?(user, *)
# TODO: this really shouldn't be necessary, because the support # TODO: this really shouldn't be necessary, because the support
...@@ -196,6 +210,10 @@ module EE ...@@ -196,6 +210,10 @@ module EE
private private
def blocking_issues_ids
@blocking_issues_ids ||= ::IssueLink.blocking_issue_ids_for(self)
end
def update_generic_alert_title def update_generic_alert_title
update(title: "#{title} #{iid}") update(title: "#{title} #{iid}")
end end
......
...@@ -20,39 +20,51 @@ class IssueLink < ApplicationRecord ...@@ -20,39 +20,51 @@ class IssueLink < ApplicationRecord
enum link_type: { TYPE_RELATES_TO => 0, TYPE_BLOCKS => 1, TYPE_IS_BLOCKED_BY => 2 } enum link_type: { TYPE_RELATES_TO => 0, TYPE_BLOCKS => 1, TYPE_IS_BLOCKED_BY => 2 }
def self.inverse_link_type(type) class << self
case type def inverse_link_type(type)
when TYPE_BLOCKS case type
TYPE_IS_BLOCKED_BY when TYPE_BLOCKS
when TYPE_IS_BLOCKED_BY TYPE_IS_BLOCKED_BY
TYPE_BLOCKS when TYPE_IS_BLOCKED_BY
else TYPE_BLOCKS
type else
type
end
end
def blocked_issue_ids(issue_ids)
blocked_and_blocking_issues_union(issue_ids).pluck(:blocked_issue_id)
end end
end
def self.blocked_issue_ids(issue_ids) def blocking_issue_ids_for(issue)
from_union([ blocked_and_blocking_issues_union(issue.id).pluck(:blocking_issue_id)
blocked_issues(issue_ids, IssueLink::TYPE_BLOCKS), end
blocked_issues(issue_ids, IssueLink::TYPE_IS_BLOCKED_BY)
]).pluck(:issue_id)
end end
private private
def self.blocked_issues(issue_ids, link_type) class << self
if link_type == IssueLink::TYPE_BLOCKS def blocked_and_blocking_issues_union(issue_ids)
blocked_key = :target_id from_union([
blocking_key = :source_id blocked_or_blocking_issues(issue_ids, IssueLink::TYPE_BLOCKS),
else blocked_or_blocking_issues(issue_ids, IssueLink::TYPE_IS_BLOCKED_BY)
blocked_key = :source_id ])
blocking_key = :target_id
end end
select("#{blocked_key} as issue_id") def blocked_or_blocking_issues(issue_ids, link_type)
.where(link_type: link_type).where(blocked_key => issue_ids) if link_type == IssueLink::TYPE_BLOCKS
.joins("INNER JOIN issues ON issues.id = issue_links.#{blocking_key}") blocked_key = :target_id
.where('issues.state_id' => Issuable::STATE_ID_MAP[:opened]) blocking_key = :source_id
else
blocked_key = :source_id
blocking_key = :target_id
end
select("#{blocked_key} as blocked_issue_id, #{blocking_key} as blocking_issue_id")
.where(link_type: link_type).where(blocked_key => issue_ids)
.joins("INNER JOIN issues ON issues.id = issue_links.#{blocking_key}")
.where('issues.state_id' => Issuable::STATE_ID_MAP[:opened])
end
end end
def check_self_relation def check_self_relation
......
...@@ -5,6 +5,17 @@ module EE ...@@ -5,6 +5,17 @@ module EE
prepended do prepended do
expose :weight, if: ->(issue, _) { issue.supports_weight? } expose :weight, if: ->(issue, _) { issue.supports_weight? }
with_options if: -> (_, options) { options[:with_blocking_issues] } do
expose :blocked?, as: :blocked
expose :blocked_by_issues do |issue|
issues = issue.blocked_by_issues(request.current_user)
serializer_options = options.merge(only: [:id, :web_url])
::IssueEntity.represent(issues, serializer_options)
end
end
end end
end end
end end
...@@ -63,6 +63,20 @@ describe IssueLink do ...@@ -63,6 +63,20 @@ describe IssueLink do
end end
end end
describe '.blocking_issue_ids_for' do
it 'returns blocking issue ids' do
issue = create(:issue)
blocking_issue = create(:issue, project: issue.project)
blocked_by_issue = create(:issue, project: issue.project)
create(:issue_link, source: blocking_issue, target: issue, link_type: IssueLink::TYPE_BLOCKS)
create(:issue_link, source: issue, target: blocked_by_issue, link_type: IssueLink::TYPE_IS_BLOCKED_BY)
blocking_ids = described_class.blocking_issue_ids_for(issue)
expect(blocking_ids).to match_array([blocking_issue.id, blocked_by_issue.id])
end
end
describe '.inverse_link_type' do describe '.inverse_link_type' do
it 'returns reverse type of link' do it 'returns reverse type of link' do
expect(described_class.inverse_link_type('relates_to')).to eq 'relates_to' expect(described_class.inverse_link_type('relates_to')).to eq 'relates_to'
......
...@@ -614,6 +614,51 @@ describe Issue do ...@@ -614,6 +614,51 @@ describe Issue do
end end
end end
describe "#blocked_by_issues" do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:blocking_issue) { create(:issue, project: project) }
let_it_be(:other_project_blocking_issue) { create(:issue) }
let_it_be(:blocked_by_issue) { create(:issue, project: project) }
let_it_be(:confidential_blocked_by_issue) { create(:issue, :confidential, project: project) }
let_it_be(:related_issue) { create(:issue, project: project) }
let_it_be(:closed_blocking_issue) { create(:issue, project: project, state: :closed) }
before_all do
create(:issue_link, source: blocking_issue, target: issue, link_type: IssueLink::TYPE_BLOCKS)
create(:issue_link, source: other_project_blocking_issue, target: issue, link_type: IssueLink::TYPE_BLOCKS)
create(:issue_link, source: issue, target: blocked_by_issue, link_type: IssueLink::TYPE_IS_BLOCKED_BY)
create(:issue_link, source: issue, target: confidential_blocked_by_issue, link_type: IssueLink::TYPE_IS_BLOCKED_BY)
create(:issue_link, source: issue, target: related_issue, link_type: IssueLink::TYPE_RELATES_TO)
create(:issue_link, source: closed_blocking_issue, target: issue, link_type: IssueLink::TYPE_BLOCKS)
end
context 'when user can read issues' do
it 'returns blocked issues' do
project.add_developer(user)
other_project_blocking_issue.project.add_developer(user)
expect(issue.blocked_by_issues(user)).to match_array([blocking_issue, blocked_by_issue, other_project_blocking_issue, confidential_blocked_by_issue])
end
end
context 'when user cannot read issues' do
it 'returns empty array' do
expect(issue.blocked_by_issues(user)).to be_empty
end
end
context 'when user can read some issues' do
it 'returns issues that user can read' do
guest = create(:user)
project.add_guest(guest)
expect(issue.blocked_by_issues(guest)).to match_array([blocking_issue, blocked_by_issue])
end
end
end
it_behaves_like 'having health status' it_behaves_like 'having health status'
describe '#service_desk?' do describe '#service_desk?' do
......
# frozen_string_literal: true
require 'spec_helper'
describe Projects::IssuesController do
let_it_be(:issue) { create(:issue) }
let_it_be(:project) { issue.project }
let_it_be(:user) { issue.author }
let_it_be(:blocking_issue) { create(:issue, project: project) }
let_it_be(:blocked_by_issue) { create(:issue, project: project) }
before do
login_as(user)
end
describe 'GET #show' do
def get_show
get project_issue_path(project, issue)
end
context 'with blocking issues' do
before do
stub_feature_flags(prevent_closing_blocked_issues: true)
get_show # Warm the cache
end
it 'does not cause extra queries when multiple blocking issues are present' do
create(:issue_link, source: blocking_issue, target: issue, link_type: IssueLink::TYPE_BLOCKS)
control = ActiveRecord::QueryRecorder.new { get_show }
other_project_issue = create(:issue)
other_project_issue.project.add_developer(user)
create(:issue_link, source: issue, target: other_project_issue, link_type: IssueLink::TYPE_IS_BLOCKED_BY)
expect { get_show }.not_to exceed_query_limit(control)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe IssueEntity do
let_it_be(:project) { create(:project) }
let_it_be(:resource) { create(:issue, project: project) }
let_it_be(:user) { create(:user) }
let_it_be(:blocking_issue) { create(:issue, project: project) }
let_it_be(:blocked_issue) { create(:issue, project: project) }
let(:request) { double('request', current_user: user) }
before_all do
project.add_developer(user)
create(:issue_link, source: blocking_issue, target: blocked_issue, link_type: IssueLink::TYPE_BLOCKS)
end
subject { described_class.new(resource, request: request).as_json }
context 'when with_blocking_issues option is not present' do
it 'exposes blocking issues' do
expect(subject).not_to include(:blocked)
expect(subject).not_to include(:blocked_by_issues)
end
end
context 'when with_blocking_issues option is present' do
subject { described_class.new(resource, request: request, with_blocking_issues: true).as_json }
it 'exposes blocking issues' do
expect(subject).to include(:blocked)
expect(subject).to include(:blocked_by_issues)
end
it 'exposes only id and web_path' do
response = described_class.new(blocked_issue, request: request, with_blocking_issues: true).as_json
expect(response[:blocked_by_issues].first.keys).to match_array([:id, :web_url])
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