summaryrefslogtreecommitdiff
path: root/app/finders/group_descendants_finder.rb
blob: 40d6be03a17ea4b5b86ec4287b56196e11e8d92b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# frozen_string_literal: true

# GroupDescendantsFinder
#
# Used to find and filter all subgroups and projects of a passed parent group
# visible to a specified user.
#
# When passing a `filter` param, the search is performed over all nested levels
# of the `parent_group`. All ancestors for a search result are loaded
#
# Arguments:
#   current_user: The user for which the children should be visible
#   parent_group: The group to find children of
#   params:
#     Supports all params that the `ProjectsFinder` and `GroupProjectsFinder`
#     support.
#
#     filter: string - is aliased to `search` for consistency with the frontend
#     archived: string - `only` or `true`.
#                        `non_archived` is passed to the `ProjectFinder`s if none
#                        was given.
class GroupDescendantsFinder
  attr_reader :current_user, :parent_group, :params

  def initialize(current_user: nil, parent_group:, params: {})
    @current_user = current_user
    @parent_group = parent_group
    @params = params.reverse_merge(non_archived: params[:archived].blank?, not_aimed_for_deletion: true)
  end

  def execute
    # The children array might be extended with the ancestors of projects and
    # subgroups when filtering. In that case, take the maximum so the array does
    # not get limited otherwise, allow paginating through all results.
    #
    all_required_elements = children
    if params[:filter]
      all_required_elements |= ancestors_of_filtered_subgroups
      all_required_elements |= ancestors_of_filtered_projects
    end

    total_count = [all_required_elements.size, paginator.total_count].max

    Kaminari.paginate_array(all_required_elements, total_count: total_count)
  end

  def has_children?
    projects.any? || subgroups.any?
  end

  private

  def children
    @children ||= paginator.paginate(params[:page])
  end

  def paginator
    @paginator ||= Gitlab::MultiCollectionPaginator.new(
      subgroups,
      projects.with_route,
      per_page: params[:per_page]
    )
  end

  def direct_child_groups
    # rubocop: disable CodeReuse/Finder
    GroupsFinder.new(current_user,
                     parent: parent_group,
                     all_available: true).execute
    # rubocop: enable CodeReuse/Finder
  end

  # rubocop: disable CodeReuse/ActiveRecord
  def all_visible_descendant_groups
    # rubocop: disable CodeReuse/Finder
    groups_table = Group.arel_table
    visible_to_user = groups_table[:visibility_level]
                      .in(Gitlab::VisibilityLevel.levels_for_user(current_user))

    if current_user
      authorized_groups = GroupsFinder.new(current_user,
                                           all_available: false)
                            .execute.arel.as('authorized')
      authorized_to_user = groups_table.project(1).from(authorized_groups)
                             .where(authorized_groups[:id].eq(groups_table[:id]))
                             .exists
      visible_to_user = visible_to_user.or(authorized_to_user)
    end

    parent_group.descendants.where(visible_to_user)
    # rubocop: enable CodeReuse/Finder
  end
  # rubocop: enable CodeReuse/ActiveRecord

  def subgroups_matching_filter
    all_visible_descendant_groups
      .search(params[:filter])
  end

  # When filtering we want all to preload all the ancestors upto the specified
  # parent group.
  #
  # - root
  #   - subgroup
  #     - nested-group
  #       - project
  #
  # So when searching 'project', on the 'subgroup' page we want to preload
  # 'nested-group' but not 'subgroup' or 'root'
  # rubocop: disable CodeReuse/ActiveRecord
  def ancestors_of_groups(base_for_ancestors)
    group_ids = base_for_ancestors.except(:select, :sort).select(:id)
    groups = Group.where(id: group_ids)

    if Feature.enabled?(:linear_group_descendants_finder_upto, current_user, default_enabled: :yaml)
      groups.self_and_ancestors(upto: parent_group.id)
    else
      Gitlab::ObjectHierarchy.new(groups).base_and_ancestors(upto: parent_group.id)
    end
  end
  # rubocop: enable CodeReuse/ActiveRecord

  # rubocop: disable CodeReuse/ActiveRecord
  def ancestors_of_filtered_projects
    projects_to_load_ancestors_of = projects.where.not(namespace: parent_group)
    groups_to_load_ancestors_of = Group.where(id: projects_to_load_ancestors_of.select(:namespace_id))
    ancestors_of_groups(groups_to_load_ancestors_of)
      .with_selects_for_list(archived: params[:archived])
  end
  # rubocop: enable CodeReuse/ActiveRecord

  def ancestors_of_filtered_subgroups
    ancestors_of_groups(subgroups)
      .with_selects_for_list(archived: params[:archived])
  end

  def subgroups
    # When filtering subgroups, we want to find all matches within the tree of
    # descendants to show to the user
    groups = if params[:filter]
               subgroups_matching_filter
             else
               direct_child_groups
             end

    groups.with_selects_for_list(archived: params[:archived]).order_by(sort)
  end

  # rubocop: disable CodeReuse/Finder
  def direct_child_projects
    GroupProjectsFinder.new(group: parent_group, current_user: current_user, params: params, options: { only_owned: true })
      .execute
  end
  # rubocop: enable CodeReuse/Finder

  # Finds all projects nested under `parent_group` or any of its descendant
  # groups
  # rubocop: disable CodeReuse/ActiveRecord
  def projects_matching_filter
    # rubocop: disable CodeReuse/Finder
    projects_nested_in_group = Project.where(namespace_id: parent_group.self_and_descendants.as_ids)
    params_with_search = params.merge(search: params[:filter])

    ProjectsFinder.new(params: params_with_search,
                       current_user: current_user,
                       project_ids_relation: projects_nested_in_group).execute
    # rubocop: enable CodeReuse/Finder
  end
  # rubocop: enable CodeReuse/ActiveRecord

  def projects
    projects = if params[:filter]
                 projects_matching_filter
               else
                 direct_child_projects
               end

    projects.with_route.order_by(sort)
  end

  def sort
    params.fetch(:sort, 'name_asc')
  end

  # rubocop: disable CodeReuse/ActiveRecord
  def hierarchy_for_parent
    @hierarchy ||= Gitlab::ObjectHierarchy.new(Group.where(id: parent_group.id))
  end
  # rubocop: enable CodeReuse/ActiveRecord
end