summaryrefslogtreecommitdiff
path: root/app/finders/group_descendants_finder.rb
blob: d921e59d16f8056fd6a87dc53eaeec380f59aa43 (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
class GroupDescendantsFinder
  include Gitlab::Allowable

  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: true)
  end

  def execute
    # The children array might be extended with the ancestors of projects when
    # filtering. In that case, take the maximum so the aray does not get limited
    # Otherwise, allow paginating through the search results
    #
    total_count = [children.size, subgroup_count + project_count].max
    Kaminari.paginate_array(children, total_count: total_count)
  end

  def subgroup_count
    @subgroup_count ||= subgroups.count
  end

  def project_count
    @project_count ||= projects.count
  end

  private

  def children
    return @children if @children

    projects_count = <<~PROJECTCOUNT
                   (SELECT COUNT(projects.id) AS preloaded_project_count
                    FROM projects WHERE projects.namespace_id = namespaces.id)
                   PROJECTCOUNT
    subgroup_count = <<~SUBGROUPCOUNT
                     (SELECT COUNT(children.id) AS preloaded_subgroup_count
                      FROM namespaces children
                      WHERE children.parent_id = namespaces.id)
                     SUBGROUPCOUNT
    member_count = <<~MEMBERCOUNT
                   (SELECT COUNT(members.user_id) AS preloaded_member_count
                    FROM members
                    WHERE members.source_type = 'Namespace'
                    AND members.source_id = namespaces.id
                    AND members.requested_at IS NULL)
                   MEMBERCOUNT
    group_selects = [
      'namespaces.*',
      projects_count,
      subgroup_count,
      member_count
    ]

    subgroups_with_counts = subgroups.with_route.page(params[:page]).per(per_page).select(group_selects)
    group_page_count = subgroups_with_counts.total_pages
    subgroup_page = subgroups_with_counts.current_page

    paginated_projects = projects.with_route.page(subgroup_page - group_page_count)
                           .per(per_page - subgroups_with_counts.size)

    if params[:filter]
      ancestors_for_project_search = ancestors_for_groups(Group.where(id: paginated_projects.select(:namespace_id)))
      subgroups_with_counts = ancestors_for_project_search.with_route.select(group_selects) | subgroups_with_counts
    end

    @children = subgroups_with_counts + paginated_projects
  end

  def direct_child_groups
    GroupsFinder.new(current_user,
                     parent: parent_group,
                     all_available: true).execute
  end

  def all_visible_descendant_groups
    groups_table = Group.arel_table
    visible_for_user = if current_user
                         groups_table[:id].in(
                           Arel::Nodes::SqlLiteral.new(GroupsFinder.new(current_user, all_available: true).execute.select(:id).to_sql)
                         )
                       else
                         groups_table[:visibility_level].eq(Gitlab::VisibilityLevel::PUBLIC)
                       end

    Gitlab::GroupHierarchy.new(Group.where(id: parent_group))
      .base_and_descendants
      .where(visible_for_user)
  end

  def subgroups_matching_filter
    all_visible_descendant_groups
      .where.not(id: parent_group)
      .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'
  def ancestors_for_groups(base_for_ancestors)
    ancestors_for_parent = Gitlab::GroupHierarchy.new(Group.where(id: parent_group))
                             .base_and_ancestors
    Gitlab::GroupHierarchy.new(base_for_ancestors)
      .base_and_ancestors.where.not(id: ancestors_for_parent)
  end

  def subgroups
    return Group.none unless Group.supports_nested_groups?
    return Group.none unless can?(current_user, :read_group, parent_group)

    # When filtering subgroups, we want to find all matches withing the tree of
    # descendants to show to the user
    groups = if params[:filter]
               ancestors_for_groups(subgroups_matching_filter)
             else
               direct_child_groups
             end
    groups.order_by(sort)
  end

  def projects_for_user
    Project.public_or_visible_to_user(current_user).non_archived
  end

  def direct_child_projects
    projects_for_user.where(namespace: parent_group)
  end

  def projects_matching_filter
    projects_for_user.search(params[:filter])
      .where(namespace: all_visible_descendant_groups)
  end

  def projects
    return Project.none unless can?(current_user, :read_group, parent_group)

    projects = if params[:filter]
                 projects_matching_filter
               else
                 direct_child_projects
               end
    projects.order_by(sort)
  end

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

  def per_page
    params.fetch(:per_page, Kaminari.config.default_per_page)
  end
end