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 @@
# only_shared: boolean
# limit: integer
# include_subgroups: boolean
# include_ancestor_groups: boolean
# params:
# sort: string
# visibility_level: int
......@@ -113,12 +114,19 @@ class GroupProjectsFinder < ProjectsFinder
options.fetch(:include_subgroups, false)
end
def owned_projects
if include_subgroups?
Project.for_group_and_its_subgroups(group)
else
group.projects
# 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
return group.projects unless include_subgroups? || include_ancestor_groups?
union_relations = []
union_relations << Project.for_group_and_its_subgroups(group) if include_subgroups?
union_relations << Project.for_group_and_its_ancestor_groups(group) if include_ancestor_groups?
Project.from_union(union_relations)
end
def shared_projects
......
......@@ -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 :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_ancestor_groups, ->(group) { where(namespace_id: group.self_and_ancestors.select(:id)) }
class << self
# Searches for a list of projects based on the query given in `query`.
......
......@@ -293,6 +293,7 @@ module API
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 :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'
use :pagination
......@@ -302,7 +303,8 @@ module API
get ":id/projects" do
finder_options = {
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)
......
......@@ -9,13 +9,29 @@ RSpec.describe GroupProjectsFinder do
describe 'with a group member current user' do
before do
group.add_maintainer(current_user)
root_group.add_maintainer(current_user)
end
context "only shared" do
let(:options) { { only_shared: true } }
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
context "only owned" do
......@@ -29,9 +45,46 @@ RSpec.describe GroupProjectsFinder do
it { is_expected.to match_array([private_project, public_project, subgroup_project, subgroup_private_project]) }
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]) }
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
context "all" do
......@@ -90,6 +143,7 @@ RSpec.describe GroupProjectsFinder do
before do
private_project.add_maintainer(current_user)
subgroup_private_project.add_maintainer(current_user)
root_group_private_project.add_maintainer(current_user)
end
context 'with subgroups projects' do
......@@ -100,6 +154,23 @@ RSpec.describe GroupProjectsFinder do
it { is_expected.to match_array([private_project, public_project, subgroup_project, subgroup_private_project]) }
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
it { is_expected.to match_array([private_project, public_project]) }
end
......@@ -118,6 +189,23 @@ RSpec.describe GroupProjectsFinder do
it { is_expected.to match_array([public_project, subgroup_project]) }
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
it { is_expected.to eq([public_project]) }
end
......
......@@ -6240,6 +6240,21 @@ RSpec.describe Project, factory_default: :keep do
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
subject { project.deployments }
......
......@@ -1163,6 +1163,7 @@ RSpec.describe API::Groups do
expect(json_response.length).to eq(3)
end
context 'when include_subgroups is true' do
it "returns projects including those in subgroups" do
subgroup = create(:group, parent: group1)
create(:project, group: subgroup)
......@@ -1175,6 +1176,21 @@ RSpec.describe API::Groups do
expect(json_response).to be_an(Array)
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
it "does not return a non existing group" do
get api("/groups/#{non_existing_record_id}/projects", user1)
......
# frozen_string_literal: true
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(:current_user) { create(:user) }
let(:params) { {} }
......@@ -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(: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(: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
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