Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-09-20 02:18:09 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-09-20 02:18:09 +0300
commit6ed4ec3e0b1340f96b7c043ef51d1b33bbe85fde (patch)
treedc4d20fe6064752c0bd323187252c77e0a89144b /app/finders
parent9868dae7fc0655bd7ce4a6887d4e6d487690eeed (diff)
Add latest changes from gitlab-org/gitlab@15-4-stable-eev15.4.0-rc42
Diffstat (limited to 'app/finders')
-rw-r--r--app/finders/context_commits_finder.rb11
-rw-r--r--app/finders/crm/organizations_finder.rb16
-rw-r--r--app/finders/database/batched_background_migrations_finder.rb27
-rw-r--r--app/finders/deployments_finder.rb47
-rw-r--r--app/finders/environments/environments_finder.rb7
-rw-r--r--app/finders/group_members_finder.rb2
-rw-r--r--app/finders/groups/accepting_group_transfers_finder.rb69
-rw-r--r--app/finders/groups/accepting_project_transfers_finder.rb4
-rw-r--r--app/finders/groups/base.rb17
-rw-r--r--app/finders/groups/user_groups_finder.rb12
-rw-r--r--app/finders/groups_finder.rb36
-rw-r--r--app/finders/incident_management/timeline_events_finder.rb2
-rw-r--r--app/finders/issuable_finder.rb29
-rw-r--r--app/finders/issues_finder.rb4
-rw-r--r--app/finders/merge_requests/by_approvals_finder.rb4
-rw-r--r--app/finders/merge_requests_finder.rb28
-rw-r--r--app/finders/merge_requests_finder/params.rb6
-rw-r--r--app/finders/projects_finder.rb4
-rw-r--r--app/finders/user_groups_counter.rb6
19 files changed, 251 insertions, 80 deletions
diff --git a/app/finders/context_commits_finder.rb b/app/finders/context_commits_finder.rb
index d623854ada4..4a45817cc61 100644
--- a/app/finders/context_commits_finder.rb
+++ b/app/finders/context_commits_finder.rb
@@ -5,8 +5,10 @@ class ContextCommitsFinder
@project = project
@merge_request = merge_request
@search = params[:search]
+ @author = params[:author]
+ @committed_before = params[:committed_before]
+ @committed_after = params[:committed_after]
@limit = (params[:limit] || 40).to_i
- @offset = (params[:offset] || 0).to_i
end
def execute
@@ -16,13 +18,13 @@ class ContextCommitsFinder
private
- attr_reader :project, :merge_request, :search, :limit, :offset
+ attr_reader :project, :merge_request, :search, :author, :committed_before, :committed_after, :limit
def init_collection
if search.present?
search_commits
else
- project.repository.commits(merge_request.target_branch, { limit: limit, offset: offset })
+ project.repository.commits(merge_request.target_branch, { limit: limit })
end
end
@@ -41,7 +43,8 @@ class ContextCommitsFinder
commits = [commit_by_sha] if commit_by_sha
end
else
- commits = project.repository.find_commits_by_message(search, merge_request.target_branch, nil, 20)
+ commits = project.repository.list_commits_by(search, merge_request.target_branch,
+ author: author, before: committed_before, after: committed_after, limit: limit)
end
commits
diff --git a/app/finders/crm/organizations_finder.rb b/app/finders/crm/organizations_finder.rb
index 5a8ab148ef3..69f72235c71 100644
--- a/app/finders/crm/organizations_finder.rb
+++ b/app/finders/crm/organizations_finder.rb
@@ -16,6 +16,11 @@ module Crm
attr_reader :params, :current_user
+ def self.counts_by_state(current_user, params = {})
+ params = params.merge(sort: nil)
+ new(current_user, params).execute.counts_by_state
+ end
+
def initialize(current_user, params = {})
@current_user = current_user
@params = params
@@ -28,11 +33,20 @@ module Crm
organizations = by_ids(organizations)
organizations = by_search(organizations)
organizations = by_state(organizations)
- organizations.sort_by_name
+ sort_organizations(organizations)
end
private
+ def sort_organizations(organizations)
+ return organizations.sort_by_name unless @params.key?(:sort)
+ return organizations if @params[:sort].nil?
+
+ field = @params[:sort][:field]
+ direction = @params[:sort][:direction]
+ organizations.sort_by_field(field, direction)
+ end
+
def root_group
strong_memoize(:root_group) do
group = params[:group]&.root_ancestor
diff --git a/app/finders/database/batched_background_migrations_finder.rb b/app/finders/database/batched_background_migrations_finder.rb
new file mode 100644
index 00000000000..866acd47238
--- /dev/null
+++ b/app/finders/database/batched_background_migrations_finder.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Database
+ class BatchedBackgroundMigrationsFinder
+ RETURNED_MIGRATIONS = 20
+
+ def initialize(connection:)
+ @connection = connection
+ end
+
+ def execute
+ batched_migration_class.ordered_by_created_at_desc.for_gitlab_schema(schema).limit(RETURNED_MIGRATIONS)
+ end
+
+ private
+
+ attr_accessor :connection
+
+ def batched_migration_class
+ Gitlab::Database::BackgroundMigration::BatchedMigration
+ end
+
+ def schema
+ Gitlab::Database.gitlab_schemas_for_connection(connection)
+ end
+ end
+end
diff --git a/app/finders/deployments_finder.rb b/app/finders/deployments_finder.rb
index 04b82ee04ec..5b2139cb941 100644
--- a/app/finders/deployments_finder.rb
+++ b/app/finders/deployments_finder.rb
@@ -9,8 +9,8 @@
# updated_before: DateTime
# finished_after: DateTime
# finished_before: DateTime
-# environment: String
-# status: String (see Deployment.statuses)
+# environment: String (name) or Integer (ID)
+# status: String or Array<String> (see Deployment.statuses)
# order_by: String (see ALLOWED_SORT_VALUES constant)
# sort: String (asc | desc)
class DeploymentsFinder
@@ -33,6 +33,7 @@ class DeploymentsFinder
def initialize(params = {})
@params = params
+ @params[:status] = Array(@params[:status]).map(&:to_s) if @params[:status]
validate!
end
@@ -68,16 +69,25 @@ class DeploymentsFinder
raise error if raise_for_inefficient_updated_at_query?
end
- if (filter_by_finished_at? && !order_by_finished_at?) || (!filter_by_finished_at? && order_by_finished_at?)
- raise InefficientQueryError, '`finished_at` filter and `finished_at` sorting must be paired'
+ if filter_by_finished_at? && !order_by_finished_at?
+ raise InefficientQueryError, '`finished_at` filter requires `finished_at` sort.'
+ end
+
+ if order_by_finished_at? && !(filter_by_finished_at? || filter_by_finished_statuses?)
+ raise InefficientQueryError,
+ '`finished_at` sort requires `finished_at` filter or a filter with at least one of the finished statuses.'
end
if filter_by_finished_at? && !filter_by_successful_deployment?
raise InefficientQueryError, '`finished_at` filter must be combined with `success` status filter.'
end
- if params[:environment].present? && !params[:project].present?
- raise InefficientQueryError, '`environment` filter must be combined with `project` scope.'
+ if filter_by_environment_name? && !params[:project].present?
+ raise InefficientQueryError, '`environment` name filter must be combined with `project` scope.'
+ end
+
+ if filter_by_finished_statuses? && filter_by_upcoming_statuses?
+ raise InefficientQueryError, 'finished statuses and upcoming statuses must be separately queried.'
end
end
@@ -86,6 +96,8 @@ class DeploymentsFinder
params[:project].deployments
elsif params[:group].present?
::Deployment.for_projects(params[:group].all_projects)
+ elsif filter_by_environment_id?
+ ::Deployment.for_environment(params[:environment])
else
::Deployment.none
end
@@ -112,7 +124,7 @@ class DeploymentsFinder
end
def by_environment(items)
- if params[:project].present? && params[:environment].present?
+ if params[:project].present? && filter_by_environment_name?
items.for_environment_name(params[:project], params[:environment])
else
items
@@ -122,7 +134,7 @@ class DeploymentsFinder
def by_status(items)
return items unless params[:status].present?
- unless Deployment.statuses.key?(params[:status])
+ unless Deployment.statuses.keys.intersection(params[:status]) == params[:status]
raise ArgumentError, "The deployment status #{params[:status]} is invalid"
end
@@ -165,7 +177,23 @@ class DeploymentsFinder
end
def filter_by_successful_deployment?
- params[:status].to_s == 'success'
+ params[:status].present? && params[:status].count == 1 && params[:status].first.to_s == 'success'
+ end
+
+ def filter_by_finished_statuses?
+ params[:status].present? && Deployment::FINISHED_STATUSES.map(&:to_s).intersection(params[:status]).any?
+ end
+
+ def filter_by_upcoming_statuses?
+ params[:status].present? && Deployment::UPCOMING_STATUSES.map(&:to_s).intersection(params[:status]).any?
+ end
+
+ def filter_by_environment_name?
+ params[:environment].present? && params[:environment].is_a?(String)
+ end
+
+ def filter_by_environment_id?
+ params[:environment].present? && params[:environment].is_a?(Integer)
end
def order_by_updated_at?
@@ -183,6 +211,7 @@ class DeploymentsFinder
environment: [],
deployable: {
job_artifacts: [],
+ user: [],
pipeline: {
project: {
route: [],
diff --git a/app/finders/environments/environments_finder.rb b/app/finders/environments/environments_finder.rb
index 46c49f096c6..f2dcba04349 100644
--- a/app/finders/environments/environments_finder.rb
+++ b/app/finders/environments/environments_finder.rb
@@ -14,6 +14,7 @@ module Environments
def execute
environments = project.environments
+ environments = by_type(environments)
environments = by_name(environments)
environments = by_search(environments)
environments = by_ids(environments)
@@ -24,6 +25,12 @@ module Environments
private
+ def by_type(environments)
+ return environments unless params[:type].present?
+
+ environments.for_type(params[:type])
+ end
+
def by_name(environments)
if params[:name].present?
environments.for_name(params[:name])
diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb
index 048e25046da..4688d561897 100644
--- a/app/finders/group_members_finder.rb
+++ b/app/finders/group_members_finder.rb
@@ -47,7 +47,7 @@ class GroupMembersFinder < UnionFinder
related_groups << Group.by_id(group.id) if include_relations&.include?(:direct)
related_groups << group.ancestors if include_relations&.include?(:inherited)
related_groups << group.descendants if include_relations&.include?(:descendants)
- related_groups << group.shared_with_groups.public_or_visible_to_user(user) if include_relations&.include?(:shared_from_groups)
+ related_groups << Group.shared_into_ancestors(group).public_or_visible_to_user(user) if include_relations&.include?(:shared_from_groups)
find_union(related_groups, Group)
end
diff --git a/app/finders/groups/accepting_group_transfers_finder.rb b/app/finders/groups/accepting_group_transfers_finder.rb
new file mode 100644
index 00000000000..df67f940d20
--- /dev/null
+++ b/app/finders/groups/accepting_group_transfers_finder.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+module Groups
+ class AcceptingGroupTransfersFinder < Base
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(current_user, group_to_be_transferred, params = {})
+ @current_user = current_user
+ @group_to_be_transferred = group_to_be_transferred
+ @params = params
+ end
+
+ def execute
+ return Group.none unless can_transfer_group?
+
+ items = if Feature.enabled?(:include_groups_from_group_shares_in_group_transfer_locations)
+ find_all_groups
+ else
+ find_groups
+ end
+
+ items = by_search(items)
+
+ sort(items)
+ end
+
+ private
+
+ attr_reader :current_user, :group_to_be_transferred, :params
+
+ def find_groups
+ GroupsFinder.new( # rubocop: disable CodeReuse/Finder
+ current_user,
+ min_access_level: Gitlab::Access::OWNER,
+ exclude_group_ids: exclude_groups
+ ).execute.without_order
+ end
+
+ def find_all_groups
+ ::Namespace.from_union(
+ [
+ find_groups,
+ groups_originating_from_group_shares_with_owner_access
+ ]
+ )
+ end
+
+ def groups_originating_from_group_shares_with_owner_access
+ GroupGroupLink
+ .with_owner_access
+ .groups_accessible_via(
+ current_user.owned_groups.select(:id)
+ ).id_not_in(exclude_groups)
+ end
+
+ def exclude_groups
+ strong_memoize(:exclude_groups) do
+ exclude_groups = group_to_be_transferred.self_and_descendants.pluck_primary_key
+ exclude_groups << group_to_be_transferred.parent_id if group_to_be_transferred.parent_id
+
+ exclude_groups
+ end
+ end
+
+ def can_transfer_group?
+ Ability.allowed?(current_user, :admin_group, group_to_be_transferred)
+ end
+ end
+end
diff --git a/app/finders/groups/accepting_project_transfers_finder.rb b/app/finders/groups/accepting_project_transfers_finder.rb
index 09d3c430641..a3f58a78eca 100644
--- a/app/finders/groups/accepting_project_transfers_finder.rb
+++ b/app/finders/groups/accepting_project_transfers_finder.rb
@@ -7,10 +7,6 @@ module Groups
end
def execute
- if Feature.disabled?(:include_groups_from_group_shares_in_project_transfer_locations)
- return current_user.manageable_groups
- end
-
groups_accepting_project_transfers =
[
current_user.manageable_groups,
diff --git a/app/finders/groups/base.rb b/app/finders/groups/base.rb
new file mode 100644
index 00000000000..d7f56b1a7a6
--- /dev/null
+++ b/app/finders/groups/base.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Groups
+ class Base
+ private
+
+ def sort(items)
+ items.order(Group.arel_table[:path].asc, Group.arel_table[:id].asc) # rubocop: disable CodeReuse/ActiveRecord
+ end
+
+ def by_search(items)
+ return items if params[:search].blank?
+
+ items.search(params[:search], include_parents: true)
+ end
+ end
+end
diff --git a/app/finders/groups/user_groups_finder.rb b/app/finders/groups/user_groups_finder.rb
index bda8b7cc1e0..b58c1323b1f 100644
--- a/app/finders/groups/user_groups_finder.rb
+++ b/app/finders/groups/user_groups_finder.rb
@@ -13,7 +13,7 @@
#
# Initially created to filter user groups and descendants where the user can create projects
module Groups
- class UserGroupsFinder
+ class UserGroupsFinder < Base
def initialize(current_user, target_user, params = {})
@current_user = current_user
@target_user = target_user
@@ -34,16 +34,6 @@ module Groups
attr_reader :current_user, :target_user, :params
- def sort(items)
- items.order(Group.arel_table[:path].asc, Group.arel_table[:id].asc) # rubocop: disable CodeReuse/ActiveRecord
- end
-
- def by_search(items)
- return items if params[:search].blank?
-
- items.search(params[:search], include_parents: true)
- end
-
def by_permission_scope
if permission_scope_create_projects?
target_user.manageable_groups(include_groups_with_developer_maintainer_access: true)
diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb
index 9a8bc74f435..61d79885001 100644
--- a/app/finders/groups_finder.rb
+++ b/app/finders/groups_finder.rb
@@ -15,6 +15,7 @@
# exclude_group_ids: array of integers
# include_parent_descendants: boolean (defaults to false) - includes descendant groups when
# filtering by parent. The parent param must be present.
+# include_ancestors: boolean (defaults to true)
#
# Users with full private access can see all groups. The `owned` and `parent`
# params can be used to restrict the groups that are returned.
@@ -52,15 +53,7 @@ class GroupsFinder < UnionFinder
return [Group.all] if current_user&.can_read_all_resources? && all_available?
groups = []
-
- if current_user
- if Feature.enabled?(:use_traversal_ids_groups_finder, current_user)
- groups << current_user.authorized_groups.self_and_ancestors
- groups << current_user.groups.self_and_descendants
- else
- groups << Gitlab::ObjectHierarchy.new(groups_for_ancestors, groups_for_descendants).all_objects
- end
- end
+ groups = get_groups_for_user if current_user
groups << Group.unscoped.public_to_user(current_user) if include_public_groups?
groups << Group.none if groups.empty?
@@ -136,4 +129,29 @@ class GroupsFinder < UnionFinder
def min_access_level?
current_user && params[:min_access_level].present?
end
+
+ def include_ancestors?
+ params.fetch(:include_ancestors, true)
+ end
+
+ def get_groups_for_user
+ groups = []
+
+ if Feature.enabled?(:use_traversal_ids_groups_finder, current_user)
+ groups << if include_ancestors?
+ current_user.authorized_groups.self_and_ancestors
+ else
+ current_user.authorized_groups
+ end
+
+ groups << current_user.groups.self_and_descendants
+ elsif include_ancestors?
+ groups << Gitlab::ObjectHierarchy.new(groups_for_ancestors, groups_for_descendants).all_objects
+ else
+ groups << current_user.authorized_groups
+ groups << Gitlab::ObjectHierarchy.new(groups_for_descendants).base_and_descendants
+ end
+
+ groups
+ end
end
diff --git a/app/finders/incident_management/timeline_events_finder.rb b/app/finders/incident_management/timeline_events_finder.rb
index 09de46bb79f..aaf3133236a 100644
--- a/app/finders/incident_management/timeline_events_finder.rb
+++ b/app/finders/incident_management/timeline_events_finder.rb
@@ -31,7 +31,7 @@ module IncidentManagement
end
def sort(collection)
- collection.order_occurred_at_asc
+ collection.order_occurred_at_asc_id_asc
end
end
end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 1088d53c9a0..9f331d381aa 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -46,8 +46,7 @@ class IssuableFinder
requires_cross_project_access unless: -> { params.project? }
- FULL_TEXT_SEARCH_TERM_PATTERN = '[\u0000-\u218F]*'
- FULL_TEXT_SEARCH_TERM_REGEX = /\A#{FULL_TEXT_SEARCH_TERM_PATTERN}\z/.freeze
+ FULL_TEXT_SEARCH_TERM_REGEX = /\A[\p{ASCII}|\p{Latin}]+\z/.freeze
NEGATABLE_PARAMS_HELPER_KEYS = %i[project_id scope status include_subgroups].freeze
attr_accessor :current_user, :params
@@ -59,19 +58,19 @@ class IssuableFinder
class << self
def scalar_params
@scalar_params ||= %i[
- assignee_id
- assignee_username
- author_id
- author_username
- crm_contact_id
- crm_organization_id
- label_name
- milestone_title
- release_tag
- my_reaction_emoji
- search
- in
- ]
+ assignee_id
+ assignee_username
+ author_id
+ author_username
+ crm_contact_id
+ crm_organization_id
+ label_name
+ milestone_title
+ release_tag
+ my_reaction_emoji
+ search
+ in
+ ]
end
def array_params
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index 663dda73a6a..9f96abcd4e5 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -60,10 +60,10 @@ class IssuesFinder < IssuableFinder
# count of issues assigned to the user for the header bar.
return issues.all if current_user && assignee_filter.includes_user?(current_user)
- return issues.where('issues.confidential IS NOT TRUE') if params.user_cannot_see_confidential_issues?
+ return issues.public_only if params.user_cannot_see_confidential_issues?
issues.where('
- issues.confidential IS NOT TRUE
+ issues.confidential = FALSE
OR (issues.confidential = TRUE
AND (issues.author_id = :user_id
OR EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = :user_id AND issue_id = issues.id)
diff --git a/app/finders/merge_requests/by_approvals_finder.rb b/app/finders/merge_requests/by_approvals_finder.rb
index 94f13468327..8b2e9aa8df1 100644
--- a/app/finders/merge_requests/by_approvals_finder.rb
+++ b/app/finders/merge_requests/by_approvals_finder.rb
@@ -71,9 +71,7 @@ module MergeRequests
#
# @param [ActiveRecord::Relation] items the activerecord relation
def with_any_approvals(items)
- items.select_from_union([
- items.with_approvals
- ])
+ items.select_from_union([items.with_approvals])
end
# Merge requests approved by given usernames
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index 06feefb9059..ffa912afd1e 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -30,6 +30,8 @@
# updated_before: datetime
#
class MergeRequestsFinder < IssuableFinder
+ extend ::Gitlab::Utils::Override
+
include MergedAtFilter
def self.scalar_params
@@ -44,8 +46,7 @@ class MergeRequestsFinder < IssuableFinder
:reviewer_id,
:reviewer_username,
:target_branch,
- :wip,
- :attention
+ :wip
]
end
@@ -70,7 +71,6 @@ class MergeRequestsFinder < IssuableFinder
items = by_approvals(items)
items = by_deployments(items)
items = by_reviewer(items)
- items = by_attention(items)
by_source_project_id(items)
end
@@ -84,6 +84,16 @@ class MergeRequestsFinder < IssuableFinder
private
+ override :sort
+ def sort(items)
+ items = super(items)
+
+ return items unless use_grouping_columns?
+
+ grouping_columns = klass.grouping_columns(params[:sort])
+ items.group(grouping_columns) # rubocop:disable CodeReuse/ActiveRecord
+ end
+
def by_commit(items)
return items unless params[:commit_sha].presence
@@ -220,18 +230,18 @@ class MergeRequestsFinder < IssuableFinder
end
end
- def by_attention(items)
- return items unless params.attention?
-
- items.attention(params.attention)
- end
-
def parse_datetime(input)
# To work around http://www.ruby-lang.org/en/news/2021/11/15/date-parsing-method-regexp-dos-cve-2021-41817/
DateTime.parse(input.byteslice(0, 128)) if input
rescue Date::Error
nil
end
+
+ def use_grouping_columns?
+ return false unless params[:sort].present?
+
+ params[:approved_by_usernames].present? || params[:approved_by_ids].present?
+ end
end
MergeRequestsFinder.prepend_mod_with('MergeRequestsFinder')
diff --git a/app/finders/merge_requests_finder/params.rb b/app/finders/merge_requests_finder/params.rb
index 1c6a425c8af..e44e96054d3 100644
--- a/app/finders/merge_requests_finder/params.rb
+++ b/app/finders/merge_requests_finder/params.rb
@@ -21,11 +21,5 @@ class MergeRequestsFinder
end
end
end
-
- def attention
- strong_memoize(:attention) do
- User.find_by_username(params[:attention])
- end
- end
end
end
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index 6b8dcd61d29..6bfe730ebc9 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -119,9 +119,9 @@ class ProjectsFinder < UnionFinder
# This is an optimization - surprisingly PostgreSQL does not optimize
# for this.
#
- # If the default visiblity level and desired visiblity level filter cancels
+ # If the default visibility level and desired visibility level filter cancels
# each other out, don't use the SQL clause for visibility level in
- # `Project.public_or_visible_to_user`. In fact, this then becames equivalent
+ # `Project.public_or_visible_to_user`. In fact, this then becomes equivalent
# to just authorized projects for the user.
#
# E.g.
diff --git a/app/finders/user_groups_counter.rb b/app/finders/user_groups_counter.rb
index 7dbc8502be2..e8e552510cd 100644
--- a/app/finders/user_groups_counter.rb
+++ b/app/finders/user_groups_counter.rb
@@ -8,9 +8,9 @@ class UserGroupsCounter
def execute
Namespace.unscoped do
Namespace.from_union([
- groups,
- project_groups
- ]).group(:user_id).count # rubocop: disable CodeReuse/ActiveRecord
+ groups,
+ project_groups
+ ]).group(:user_id).count # rubocop: disable CodeReuse/ActiveRecord
end
end