Commit 25d21e69 authored by Felipe Artur's avatar Felipe Artur

Allow to list project group ancestors on REST API

Adds include_ancestor_groups on groups/:id/projects
API endpoint.

Changelog: added
parent e1a881c1
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
# only_shared: boolean # only_shared: boolean
# limit: integer # limit: integer
# include_subgroups: boolean # include_subgroups: boolean
# include_ancestor_groups: boolean
# params: # params:
# sort: string # sort: string
# visibility_level: int # visibility_level: int
...@@ -113,12 +114,19 @@ class GroupProjectsFinder < ProjectsFinder ...@@ -113,12 +114,19 @@ class GroupProjectsFinder < ProjectsFinder
options.fetch(:include_subgroups, false) options.fetch(:include_subgroups, false)
end end
# ancestor groups are supported only for owned projects not for shared
def include_ancestor_groups?
options.fetch(:include_ancestor_groups, false)
end
def owned_projects def owned_projects
if include_subgroups? return group.projects unless include_subgroups? || include_ancestor_groups?
Project.for_group_and_its_subgroups(group)
else union_relations = []
group.projects union_relations << Project.for_group_and_its_subgroups(group) if include_subgroups?
end union_relations << Project.for_group_and_its_ancestor_groups(group) if include_ancestor_groups?
Project.from_union(union_relations)
end end
def shared_projects def shared_projects
......
...@@ -729,6 +729,7 @@ class Project < ApplicationRecord ...@@ -729,6 +729,7 @@ class Project < ApplicationRecord
scope :joins_import_state, -> { joins("INNER JOIN project_mirror_data import_state ON import_state.project_id = projects.id") } scope :joins_import_state, -> { joins("INNER JOIN project_mirror_data import_state ON import_state.project_id = projects.id") }
scope :for_group, -> (group) { where(group: group) } scope :for_group, -> (group) { where(group: group) }
scope :for_group_and_its_subgroups, ->(group) { where(namespace_id: group.self_and_descendants.select(:id)) } scope :for_group_and_its_subgroups, ->(group) { where(namespace_id: group.self_and_descendants.select(:id)) }
scope :for_group_and_its_ancestor_groups, ->(group) { where(namespace_id: group.self_and_ancestors.select(:id)) }
class << self class << self
# Searches for a list of projects based on the query given in `query`. # Searches for a list of projects based on the query given in `query`.
......
...@@ -293,6 +293,7 @@ module API ...@@ -293,6 +293,7 @@ module API
optional :with_merge_requests_enabled, type: Boolean, default: false, desc: 'Limit by enabled merge requests feature' optional :with_merge_requests_enabled, type: Boolean, default: false, desc: 'Limit by enabled merge requests feature'
optional :with_shared, type: Boolean, default: true, desc: 'Include projects shared to this group' optional :with_shared, type: Boolean, default: true, desc: 'Include projects shared to this group'
optional :include_subgroups, type: Boolean, default: false, desc: 'Includes projects in subgroups of this group' optional :include_subgroups, type: Boolean, default: false, desc: 'Includes projects in subgroups of this group'
optional :include_ancestor_groups, type: Boolean, default: false, desc: 'Includes projects in ancestors of this group'
optional :min_access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'Limit by minimum access level of authenticated user on projects' optional :min_access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'Limit by minimum access level of authenticated user on projects'
use :pagination use :pagination
...@@ -302,7 +303,8 @@ module API ...@@ -302,7 +303,8 @@ module API
get ":id/projects" do get ":id/projects" do
finder_options = { finder_options = {
only_owned: !params[:with_shared], only_owned: !params[:with_shared],
include_subgroups: params[:include_subgroups] include_subgroups: params[:include_subgroups],
include_ancestor_groups: params[:include_ancestor_groups]
} }
projects = find_group_projects(params, finder_options) projects = find_group_projects(params, finder_options)
......
...@@ -9,13 +9,29 @@ RSpec.describe GroupProjectsFinder do ...@@ -9,13 +9,29 @@ RSpec.describe GroupProjectsFinder do
describe 'with a group member current user' do describe 'with a group member current user' do
before do before do
group.add_maintainer(current_user) root_group.add_maintainer(current_user)
end end
context "only shared" do context "only shared" do
let(:options) { { only_shared: true } } let(:options) { { only_shared: true } }
it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1]) } it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1]) }
context 'with ancestor groups projects' do
before do
options[:include_ancestor_groups] = true
end
it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1]) }
end
context 'with subgroups projects' do
before do
options[:include_subgroups] = true
end
it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1]) }
end
end end
context "only owned" do context "only owned" do
...@@ -29,9 +45,46 @@ RSpec.describe GroupProjectsFinder do ...@@ -29,9 +45,46 @@ RSpec.describe GroupProjectsFinder do
it { is_expected.to match_array([private_project, public_project, subgroup_project, subgroup_private_project]) } it { is_expected.to match_array([private_project, public_project, subgroup_project, subgroup_private_project]) }
end end
context 'without subgroups projects' do context 'with ancestor group projects' do
before do
options[:include_ancestor_groups] = true
end
it { is_expected.to match_array([private_project, public_project, root_group_public_project, root_group_private_project, root_group_private_project_2]) }
end
context 'with ancestor groups and subgroups projects' do
before do
options[:include_ancestor_groups] = true
options[:include_subgroups] = true
end
it { is_expected.to match_array([private_project, public_project, root_group_public_project, root_group_private_project, root_group_private_project_2, subgroup_private_project, subgroup_project]) }
end
context 'without subgroups and ancestor group projects' do
it { is_expected.to match_array([private_project, public_project]) } it { is_expected.to match_array([private_project, public_project]) }
end end
context 'when user is member only of a subgroup' do
let(:subgroup_member) { create(:user) }
context 'with ancestor groups and subgroups projects' do
before do
group.add_maintainer(subgroup_member)
options[:include_ancestor_groups] = true
options[:include_subgroups] = true
end
it 'does not return parent group projects' do
finder = described_class.new(group: group, current_user: subgroup_member, params: params, options: options)
projects = finder.execute
expect(projects).to match_array([private_project, public_project, subgroup_project, subgroup_private_project, root_group_public_project])
end
end
end
end end
context "all" do context "all" do
...@@ -90,6 +143,7 @@ RSpec.describe GroupProjectsFinder do ...@@ -90,6 +143,7 @@ RSpec.describe GroupProjectsFinder do
before do before do
private_project.add_maintainer(current_user) private_project.add_maintainer(current_user)
subgroup_private_project.add_maintainer(current_user) subgroup_private_project.add_maintainer(current_user)
root_group_private_project.add_maintainer(current_user)
end end
context 'with subgroups projects' do context 'with subgroups projects' do
...@@ -100,6 +154,23 @@ RSpec.describe GroupProjectsFinder do ...@@ -100,6 +154,23 @@ RSpec.describe GroupProjectsFinder do
it { is_expected.to match_array([private_project, public_project, subgroup_project, subgroup_private_project]) } it { is_expected.to match_array([private_project, public_project, subgroup_project, subgroup_private_project]) }
end end
context 'with ancestor groups projects' do
before do
options[:include_ancestor_groups] = true
end
it { is_expected.to match_array([private_project, public_project, root_group_public_project, root_group_private_project]) }
end
context 'with ancestor groups and subgroups projects' do
before do
options[:include_ancestor_groups] = true
options[:include_subgroups] = true
end
it { is_expected.to match_array([private_project, public_project, root_group_private_project, root_group_public_project, subgroup_private_project, subgroup_project]) }
end
context 'without subgroups projects' do context 'without subgroups projects' do
it { is_expected.to match_array([private_project, public_project]) } it { is_expected.to match_array([private_project, public_project]) }
end end
...@@ -118,6 +189,23 @@ RSpec.describe GroupProjectsFinder do ...@@ -118,6 +189,23 @@ RSpec.describe GroupProjectsFinder do
it { is_expected.to match_array([public_project, subgroup_project]) } it { is_expected.to match_array([public_project, subgroup_project]) }
end end
context 'with ancestor groups projects' do
before do
options[:include_ancestor_groups] = true
end
it { is_expected.to match_array([public_project, root_group_public_project]) }
end
context 'with ancestor groups and subgroups projects' do
before do
options[:include_subgroups] = true
options[:include_ancestor_groups] = true
end
it { is_expected.to match_array([public_project, root_group_public_project, subgroup_project]) }
end
context 'without subgroups projects' do context 'without subgroups projects' do
it { is_expected.to eq([public_project]) } it { is_expected.to eq([public_project]) }
end end
......
...@@ -6240,6 +6240,21 @@ RSpec.describe Project, factory_default: :keep do ...@@ -6240,6 +6240,21 @@ RSpec.describe Project, factory_default: :keep do
end end
end end
describe '.for_group_and_its_ancestor_groups' do
it 'returns projects for group and its ancestors' do
group_1 = create(:group)
project_1 = create(:project, namespace: group_1)
group_2 = create(:group, parent: group_1)
project_2 = create(:project, namespace: group_2)
group_3 = create(:group, parent: group_2)
project_3 = create(:project, namespace: group_2)
group_4 = create(:group, parent: group_3)
create(:project, namespace: group_4)
expect(described_class.for_group_and_its_ancestor_groups(group_3)).to match_array([project_1, project_2, project_3])
end
end
describe '.deployments' do describe '.deployments' do
subject { project.deployments } subject { project.deployments }
......
...@@ -1163,17 +1163,33 @@ RSpec.describe API::Groups do ...@@ -1163,17 +1163,33 @@ RSpec.describe API::Groups do
expect(json_response.length).to eq(3) expect(json_response.length).to eq(3)
end end
it "returns projects including those in subgroups" do context 'when include_subgroups is true' do
subgroup = create(:group, parent: group1) it "returns projects including those in subgroups" do
create(:project, group: subgroup) subgroup = create(:group, parent: group1)
create(:project, group: subgroup) create(:project, group: subgroup)
create(:project, group: subgroup)
get api("/groups/#{group1.id}/projects", user1), params: { include_subgroups: true } get api("/groups/#{group1.id}/projects", user1), params: { include_subgroups: true }
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers expect(response).to include_pagination_headers
expect(json_response).to be_an(Array) expect(json_response).to be_an(Array)
expect(json_response.length).to eq(5) expect(json_response.length).to eq(5)
end
end
context 'when include_ancestor_groups is true' do
it 'returns ancestors groups projects' do
subgroup = create(:group, parent: group1)
subgroup_project = create(:project, group: subgroup)
get api("/groups/#{subgroup.id}/projects", user1), params: { include_ancestor_groups: true }
records = Gitlab::Json.parse(response.body)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(records.map { |r| r['id'] }).to match_array([project1.id, project3.id, subgroup_project.id, archived_project.id])
end
end end
it "does not return a non existing group" do it "does not return a non existing group" do
......
# frozen_string_literal: true # frozen_string_literal: true
RSpec.shared_context 'GroupProjectsFinder context' do RSpec.shared_context 'GroupProjectsFinder context' do
let_it_be(:group) { create(:group) } let_it_be(:root_group) { create(:group) }
let_it_be(:group) { create(:group, parent: root_group) }
let_it_be(:subgroup) { create(:group, parent: group) } let_it_be(:subgroup) { create(:group, parent: group) }
let_it_be(:current_user) { create(:user) } let_it_be(:current_user) { create(:user) }
let(:params) { {} } let(:params) { {} }
...@@ -16,6 +17,9 @@ RSpec.shared_context 'GroupProjectsFinder context' do ...@@ -16,6 +17,9 @@ RSpec.shared_context 'GroupProjectsFinder context' do
let_it_be(:shared_project_3) { create(:project, :internal, path: '5', name: 'c') } let_it_be(:shared_project_3) { create(:project, :internal, path: '5', name: 'c') }
let_it_be(:subgroup_project) { create(:project, :public, path: '6', group: subgroup, name: 'b') } let_it_be(:subgroup_project) { create(:project, :public, path: '6', group: subgroup, name: 'b') }
let_it_be(:subgroup_private_project) { create(:project, :private, path: '7', group: subgroup, name: 'a') } let_it_be(:subgroup_private_project) { create(:project, :private, path: '7', group: subgroup, name: 'a') }
let_it_be(:root_group_public_project) { create(:project, :public, path: '8', group: root_group, name: 'root-public-project') }
let_it_be(:root_group_private_project) { create(:project, :private, path: '9', group: root_group, name: 'root-private-project') }
let_it_be(:root_group_private_project_2) { create(:project, :private, path: '10', group: root_group, name: 'root-private-project-2') }
before do before do
shared_project_1.project_group_links.create!(group_access: Gitlab::Access::MAINTAINER, group: group) shared_project_1.project_group_links.create!(group_access: Gitlab::Access::MAINTAINER, group: group)
......
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