Merge group hierarchies when parents are shared

This commit is contained in:
Bob Van Landuyt 2017-09-07 19:08:56 +02:00
parent 530cf2a266
commit 518216c062
8 changed files with 199 additions and 4 deletions

View File

@ -77,6 +77,7 @@ class GroupsController < Groups::ApplicationController
render json: GroupChildSerializer
.new(current_user: current_user)
.with_pagination(request, response)
.hierarchy_base(parent, open_hierarchy: filter[:filter].present)
.represent(@children)
end
end

View File

@ -52,7 +52,7 @@ class GroupChildrenFinder
end
def subgroups_matching_filter
all_subgroups.search(params[:filter])
all_subgroups.search(params[:filter]).include(:parent)
end
def subgroups
@ -75,6 +75,7 @@ class GroupChildrenFinder
def projects_matching_filter
ProjectsFinder.new(current_user: current_user).execute
.search(params[:filter])
.include(:namespace)
.where(namespace: all_subgroups)
end

View File

@ -34,7 +34,6 @@ class GroupProjectsFinder < ProjectsFinder
else
collection_without_user
end
union(projects)
end

View File

@ -1,6 +1,6 @@
module GroupHierarchy
def hierarchy(hierarchy_base = nil)
@hierarchy ||= tree_for_child(self, self, hierarchy_base)
tree_for_child(self, self, hierarchy_base)
end
def parent
@ -16,7 +16,7 @@ module GroupHierarchy
raise ArgumentError.new('specified base is not part of the tree')
end
if child.parent != hierarchy_base
if child.parent && child.parent != hierarchy_base
tree_for_child(child.parent,
{ child.parent => tree },
hierarchy_base)
@ -24,4 +24,47 @@ module GroupHierarchy
tree
end
end
def merge_hierarchy(other_element, hierarchy_base = nil)
GroupHierarchy.merge_hierarchies([self, other_element], hierarchy_base)
end
def self.merge_hierarchies(hierarchies, hierarchy_base = nil)
hierarchies = Array.wrap(hierarchies)
return if hierarchies.empty?
unless hierarchies.all? { |other_base| other_base.is_a?(GroupHierarchy) }
raise ArgumentError.new('element is not a hierarchy')
end
first_hierarchy, *other_hierarchies = hierarchies
merged = first_hierarchy.hierarchy(hierarchy_base)
other_hierarchies.each do |child|
next_hierarchy = child.hierarchy(hierarchy_base)
merged = merge_values(merged, next_hierarchy)
end
merged
end
def self.merge_values(first_child, second_child)
# When the first is an array, we need to go over every element to see if
# we can merge deeper.
if first_child.is_a?(Array)
first_child.map do |element|
if element.is_a?(Hash) && element.keys.any? { |k| second_child.keys.include?(k) }
element.deep_merge(second_child) { |key, first, second| merge_values(first, second) }
else
element
end
end
# If both of them are hashes, we can deep_merge with the same logic
elsif first_child.is_a?(Hash) && second_child.is_a?(Hash)
first_child.deep_merge(second_child) { |key, first, second| merge_values(first, second) }
# One of them is a root node, we just need to put them next to eachother in an array
else
Array.wrap(first_child) + Array.wrap(second_child)
end
end
end

View File

@ -1,5 +1,8 @@
class BaseSerializer
attr_reader :parameters
def initialize(parameters = {})
@parameters = parameters
@request = EntityRequest.new(parameters)
end

View File

@ -1,5 +1,42 @@
class GroupChildSerializer < BaseSerializer
include WithPagination
attr_reader :hierarchy_root
entity GroupChildEntity
def expand_hierarchy(hierarchy_root)
@hierarchy_root = hierarchy_root
self
end
def represent(resource, opts = {}, entity_class = nil)
if hierarchy_root.present?
represent_hierarchies(resource, opts)
else
super(resource, opts)
end
end
def represent_hierarchies(children, opts)
if children.is_a?(GroupHierarchy)
represent_hierarchy(children.hierarchy(hierarchy_root), opts)
else
children.map { |child| represent_hierarchy(child.hierarchy(hierarchy_root), opts) }
end
end
def represent_hierarchy(hierarchy, opts)
serializer = self.class.new(parameters)
result = if hierarchy.is_a?(Hash)
parent = hierarchy.keys.first
serializer.represent(parent, opts)
.merge(children: [serializer.represent_hierarchy(hierarchy[parent], opts)])
else
serializer.represent(hierarchy, opts)
end
result
end
end

View File

@ -29,6 +29,40 @@ describe GroupHierarchy, :nested_groups do
expect(subsub_group.parent).to eq(subgroup)
end
end
describe '#merge_hierarchy' do
it 'combines hierarchies' do
other_subgroup = create(:group, parent: parent)
expected_hierarchy = { parent => [{ subgroup => subsub_group }, other_subgroup] }
expect(subsub_group.merge_hierarchy(other_subgroup)).to eq(expected_hierarchy)
end
end
describe '.merge_hierarchies' do
it 'combines hierarchies until the top' do
other_subgroup = create(:group, parent: parent)
other_subsub_group = create(:group, parent: subgroup)
groups = [other_subgroup, subsub_group, other_subsub_group]
expected_hierarchy = { parent => [other_subgroup, { subgroup => [subsub_group, other_subsub_group] }] }
expect(described_class.merge_hierarchies(groups)).to eq(expected_hierarchy)
end
it 'combines upto a given parent' do
other_subgroup = create(:group, parent: parent)
other_subsub_group = create(:group, parent: subgroup)
groups = [other_subgroup, subsub_group, other_subsub_group]
expected_hierarchy = [other_subgroup, { subgroup => [subsub_group, other_subsub_group] }]
expect(described_class.merge_hierarchies(groups, parent)).to eq(expected_hierarchy)
end
end
end
context 'for a project' do
@ -57,5 +91,39 @@ describe GroupHierarchy, :nested_groups do
expect(project.parent).to eq(subsub_group)
end
end
describe '#merge_hierarchy' do
it 'combines hierarchies' do
project = create(:project, namespace: parent)
expected_hierarchy = { parent => [{ subgroup => subsub_group }, project] }
expect(subsub_group.merge_hierarchy(project)).to eq(expected_hierarchy)
end
end
describe '.merge_hierarchies' do
it 'combines hierarchies until the top' do
other_project = create(:project, namespace: parent)
other_subgroup_project = create(:project, namespace: subgroup)
elements = [other_project, subsub_group, other_subgroup_project]
expected_hierarchy = { parent => [other_project, { subgroup => [subsub_group, other_subgroup_project] }] }
expect(described_class.merge_hierarchies(elements)).to eq(expected_hierarchy)
end
it 'combines upto a given parent' do
other_project = create(:project, namespace: parent)
other_subgroup_project = create(:project, namespace: subgroup)
elements = [other_project, subsub_group, other_subgroup_project]
expected_hierarchy = [other_project, { subgroup => [subsub_group, other_subgroup_project] }]
expect(described_class.merge_hierarchies(elements, parent)).to eq(expected_hierarchy)
end
end
end
end

View File

@ -0,0 +1,43 @@
require 'spec_helper'
describe GroupChildSerializer do
let(:request) { double('request') }
let(:user) { create(:user) }
subject(:serializer) { described_class.new(current_user: user) }
describe '#represent' do
context 'for groups' do
it 'can render a single group' do
expect(serializer.represent(build(:group))).to be_kind_of(Hash)
end
it 'can render a collection of groups' do
expect(serializer.represent(build_list(:group, 2))).to be_kind_of(Array)
end
end
context 'with a hierarchy' do
let(:parent) { create(:group) }
subject(:serializer) do
described_class.new(current_user: user).expand_hierarchy(parent)
end
it 'expands the subgroups' do
subgroup = create(:group, parent: parent)
subsub_group = create(:group, parent: subgroup)
json = serializer.represent(subsub_group)
subsub_group_json = json[:children].first
expect(json[:id]).to eq(subgroup.id)
expect(subsub_group_json).not_to be_nil
expect(subsub_group_json[:id]).to eq(subsub_group.id)
end
it 'can expand multiple trees' do
end
end
end
end