debian-mirror-gitlab/app/finders/issuable_finder.rb

576 lines
15 KiB
Ruby
Raw Normal View History

2018-12-05 23:21:45 +05:30
# frozen_string_literal: true
2015-04-26 12:48:37 +05:30
# IssuableFinder
2014-09-02 18:07:02 +05:30
#
# Used to filter Issues and MergeRequests collections by set of params
#
# Arguments:
# klass - actual class like Issue or MergeRequest
# current_user - which user use
# params:
2018-11-08 19:23:39 +05:30
# scope: 'created_by_me' or 'assigned_to_me' or 'all'
# state: 'opened' or 'closed' or 'locked' or 'all'
2014-09-02 18:07:02 +05:30
# group_id: integer
# project_id: integer
2015-09-11 14:41:01 +05:30
# milestone_title: string
2017-09-10 17:25:29 +05:30
# author_id: integer
2019-02-15 15:39:39 +05:30
# author_username: string
2018-12-13 13:39:08 +05:30
# assignee_id: integer or 'None' or 'Any'
2019-02-15 15:39:39 +05:30
# assignee_username: string
2014-09-02 18:07:02 +05:30
# search: string
2019-03-02 22:35:43 +05:30
# in: 'title', 'description', or a string joining them with comma
2014-09-02 18:07:02 +05:30
# label_name: string
# sort: string
2017-08-17 22:00:37 +05:30
# non_archived: boolean
# iids: integer[]
2018-03-17 18:26:18 +05:30
# my_reaction_emoji: string
2018-03-27 19:54:05 +05:30
# created_after: datetime
# created_before: datetime
# updated_after: datetime
# updated_before: datetime
2019-02-15 15:39:39 +05:30
# attempt_group_search_optimizations: boolean
2014-09-02 18:07:02 +05:30
#
2015-04-26 12:48:37 +05:30
class IssuableFinder
2018-03-27 19:54:05 +05:30
prepend FinderWithCrossProjectAccess
include FinderMethods
2017-09-10 17:25:29 +05:30
include CreatedAtFilter
2019-02-15 15:39:39 +05:30
include Gitlab::Utils::StrongMemoize
2017-09-10 17:25:29 +05:30
2018-03-27 19:54:05 +05:30
requires_cross_project_access unless: -> { project? }
2018-03-17 18:26:18 +05:30
2018-12-13 13:39:08 +05:30
# This is used as a common filter for None / Any
FILTER_NONE = 'none'.freeze
FILTER_ANY = 'any'.freeze
# This is accepted as a deprecated filter and is also used in unassigning users
2018-03-27 19:54:05 +05:30
NONE = '0'.freeze
2015-04-26 12:48:37 +05:30
2014-09-02 18:07:02 +05:30
attr_accessor :current_user, :params
2018-03-27 19:54:05 +05:30
def self.scalar_params
@scalar_params ||= %i[
assignee_id
assignee_username
author_id
author_username
label_name
milestone_title
my_reaction_emoji
search
2019-03-02 22:35:43 +05:30
in
2018-03-27 19:54:05 +05:30
]
end
def self.array_params
2019-02-15 15:39:39 +05:30
@array_params ||= { label_name: [], assignee_username: [] }
2018-03-27 19:54:05 +05:30
end
def self.valid_params
@valid_params ||= scalar_params + [array_params]
end
2017-01-15 13:20:01 +05:30
def initialize(current_user, params = {})
2014-09-02 18:07:02 +05:30
@current_user = current_user
@params = params
2015-09-11 14:41:01 +05:30
end
2014-09-02 18:07:02 +05:30
2015-09-11 14:41:01 +05:30
def execute
2014-09-02 18:07:02 +05:30
items = init_collection
2018-03-27 19:54:05 +05:30
items = filter_items(items)
2019-05-18 00:54:41 +05:30
# This has to be last as we use a CTE as an optimization fence
# for counts by passing the force_cte param and enabling the
# attempt_group_search_optimizations feature flag
2018-11-08 19:23:39 +05:30
# https://www.postgresql.org/docs/current/static/queries-with.html
items = by_search(items)
2018-03-27 19:54:05 +05:30
2019-05-18 00:54:41 +05:30
items = sort(items)
items
2018-03-27 19:54:05 +05:30
end
def filter_items(items)
2018-11-08 19:23:39 +05:30
items = by_project(items)
2019-02-15 15:39:39 +05:30
items = by_group(items)
2014-09-02 18:07:02 +05:30
items = by_scope(items)
2017-09-10 17:25:29 +05:30
items = by_created_at(items)
2018-03-27 19:54:05 +05:30
items = by_updated_at(items)
2019-05-18 00:54:41 +05:30
items = by_closed_at(items)
2014-09-02 18:07:02 +05:30
items = by_state(items)
items = by_group(items)
items = by_assignee(items)
2015-04-26 12:48:37 +05:30
items = by_author(items)
2017-08-17 22:00:37 +05:30
items = by_non_archived(items)
items = by_iids(items)
items = by_milestone(items)
items = by_label(items)
2018-03-27 19:54:05 +05:30
by_my_reaction_emoji(items)
2017-01-15 13:20:01 +05:30
end
2018-03-17 18:26:18 +05:30
def row_count
Gitlab::IssuablesCountForState.new(self).for_state_or_opened(params[:state])
end
2017-08-17 22:00:37 +05:30
# We often get counts for each state by running a query per state, and
# counting those results. This is typically slower than running one query
# (even if that query is slower than any of the individual state queries) and
# grouping and counting within that query.
#
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ActiveRecord
2017-08-17 22:00:37 +05:30
def count_by_state
2019-05-18 00:54:41 +05:30
count_params = params.merge(state: nil, sort: nil, force_cte: true)
2017-08-17 22:00:37 +05:30
finder = self.class.new(current_user, count_params)
2019-05-18 00:54:41 +05:30
2017-08-17 22:00:37 +05:30
counts = Hash.new(0)
# Searching by label includes a GROUP BY in the query, but ours will be last
# because it is added last. Searching by multiple labels also includes a row
# per issuable, so we have to count those in Ruby - which is bad, but still
# better than performing multiple queries.
#
2018-11-08 19:23:39 +05:30
# This does not apply when we are using a CTE for the search, as the labels
# GROUP BY is inside the subquery in that case, so we set labels_count to 1.
2019-05-18 00:54:41 +05:30
#
# Groups and projects have separate feature flags to suggest the use
# of a CTE. The CTE will not be used if the sort doesn't support it,
# but will always be used for the counts here as we ignore sorting
# anyway.
2018-11-08 19:23:39 +05:30
labels_count = label_names.any? ? label_names.count : 1
labels_count = 1 if use_cte_for_search?
2017-08-17 22:00:37 +05:30
finder.execute.reorder(nil).group(:state).count.each do |key, value|
2018-12-05 23:21:45 +05:30
counts[count_key(key)] += value / labels_count
2017-08-17 22:00:37 +05:30
end
counts[:all] = counts.values.sum
2018-11-18 11:00:15 +05:30
counts.with_indifferent_access
2017-08-17 22:00:37 +05:30
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ActiveRecord
2017-08-17 22:00:37 +05:30
2015-09-11 14:41:01 +05:30
def group
return @group if defined?(@group)
@group =
if params[:group_id].present?
Group.find(params[:group_id])
else
nil
end
end
2019-03-02 22:35:43 +05:30
def related_groups
if project? && project && project.group && Ability.allowed?(current_user, :read_group, project.group)
project.group.self_and_ancestors
elsif group
[group]
elsif current_user
Gitlab::ObjectHierarchy.new(current_user.authorized_groups, current_user.groups).all_objects
else
[]
end
end
2015-10-24 18:46:33 +05:30
def project?
params[:project_id].present?
end
2015-09-11 14:41:01 +05:30
def project
return @project if defined?(@project)
2016-11-24 13:41:30 +05:30
project = Project.find(params[:project_id])
project = nil unless Ability.allowed?(current_user, :"read_#{klass.to_ability_name}", project)
2015-12-23 02:04:40 +05:30
2016-11-24 13:41:30 +05:30
@project = project
2015-10-24 18:46:33 +05:30
end
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ActiveRecord
2019-03-02 22:35:43 +05:30
def projects
return @projects if defined?(@projects)
return @projects = [project] if project?
2015-10-24 18:46:33 +05:30
2016-11-24 13:41:30 +05:30
projects =
if current_user && params[:authorized_only].presence && !current_user_related?
current_user.authorized_projects
elsif group
2018-03-17 18:26:18 +05:30
finder_options = { include_subgroups: params[:include_subgroups], only_owned: true }
2018-12-05 23:21:45 +05:30
GroupProjectsFinder.new(group: group, current_user: current_user, options: finder_options).execute # rubocop: disable CodeReuse/Finder
2016-11-24 13:41:30 +05:30
else
2018-12-05 23:21:45 +05:30
ProjectsFinder.new(current_user: current_user).execute # rubocop: disable CodeReuse/Finder
2016-11-24 13:41:30 +05:30
end
@projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil)
2015-09-11 14:41:01 +05:30
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ActiveRecord
2015-09-11 14:41:01 +05:30
def search
params[:search].presence
end
def milestones?
params[:milestone_title].present?
end
def milestones
return @milestones if defined?(@milestones)
@milestones =
2015-10-24 18:46:33 +05:30
if milestones?
2017-09-10 17:25:29 +05:30
if project?
group_id = project.group&.id
project_id = project.id
end
group_id = group.id if group
2015-10-24 18:46:33 +05:30
2017-09-10 17:25:29 +05:30
search_params =
{ title: params[:milestone_title], project_ids: project_id, group_ids: group_id }
2018-12-05 23:21:45 +05:30
MilestonesFinder.new(search_params).execute # rubocop: disable CodeReuse/Finder
2015-09-11 14:41:01 +05:30
else
2016-09-13 17:45:13 +05:30
Milestone.none
2015-09-11 14:41:01 +05:30
end
end
2015-10-24 18:46:33 +05:30
def labels?
params[:label_name].present?
end
def filter_by_no_label?
2019-02-15 15:39:39 +05:30
downcased = label_names.map(&:downcase)
# Label::NONE is deprecated and should be removed in 12.0
downcased.include?(FILTER_NONE) || downcased.include?(Label::NONE)
end
def filter_by_any_label?
label_names.map(&:downcase).include?(FILTER_ANY)
2015-10-24 18:46:33 +05:30
end
2016-04-02 18:10:28 +05:30
def labels
return @labels if defined?(@labels)
2016-11-03 12:29:30 +05:30
@labels =
if labels? && !filter_by_no_label?
2018-12-05 23:21:45 +05:30
LabelsFinder.new(current_user, project_ids: projects, title: label_names).execute(skip_authorization: true) # rubocop: disable CodeReuse/Finder
2016-11-03 12:29:30 +05:30
else
Label.none
2016-04-02 18:10:28 +05:30
end
end
2017-08-17 22:00:37 +05:30
def assignee_id?
2018-12-13 13:39:08 +05:30
params[:assignee_id].present?
2017-08-17 22:00:37 +05:30
end
def assignee_username?
2018-12-13 13:39:08 +05:30
params[:assignee_username].present?
2015-09-11 14:41:01 +05:30
end
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ActiveRecord
2015-09-11 14:41:01 +05:30
def assignee
return @assignee if defined?(@assignee)
@assignee =
2017-08-17 22:00:37 +05:30
if assignee_id?
User.find_by(id: params[:assignee_id])
elsif assignee_username?
2018-12-13 13:39:08 +05:30
User.find_by_username(params[:assignee_username])
2015-09-11 14:41:01 +05:30
else
nil
end
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ActiveRecord
2015-09-11 14:41:01 +05:30
2017-08-17 22:00:37 +05:30
def author_id?
params[:author_id].present? && params[:author_id] != NONE
end
def author_username?
params[:author_username].present? && params[:author_username] != NONE
end
def no_author?
# author_id takes precedence over author_username
params[:author_id] == NONE || params[:author_username] == NONE
2015-09-11 14:41:01 +05:30
end
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ActiveRecord
2015-09-11 14:41:01 +05:30
def author
return @author if defined?(@author)
@author =
2017-08-17 22:00:37 +05:30
if author_id?
User.find_by(id: params[:author_id])
elsif author_username?
2018-12-13 13:39:08 +05:30
User.find_by_username(params[:author_username])
2015-09-11 14:41:01 +05:30
else
nil
end
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ActiveRecord
2015-09-11 14:41:01 +05:30
2019-02-15 15:39:39 +05:30
def use_cte_for_search?
strong_memoize(:use_cte_for_search) do
2019-05-18 00:54:41 +05:30
next false unless search
next false unless Gitlab::Database.postgresql?
# Only simple unsorted & simple sorts can use CTE
next false if params[:sort].present? && !params[:sort].in?(klass.simple_sorts.keys)
attempt_group_search_optimizations? || attempt_project_search_optimizations?
2019-02-15 15:39:39 +05:30
end
end
2014-09-02 18:07:02 +05:30
private
2019-05-18 00:54:41 +05:30
def force_cte?
!!params[:force_cte]
end
2014-09-02 18:07:02 +05:30
def init_collection
2015-10-24 18:46:33 +05:30
klass.all
2014-09-02 18:07:02 +05:30
end
2019-02-15 15:39:39 +05:30
def attempt_group_search_optimizations?
2019-05-18 00:54:41 +05:30
params[:attempt_group_search_optimizations] &&
Feature.enabled?(:attempt_group_search_optimizations, default_enabled: true)
end
def attempt_project_search_optimizations?
params[:attempt_project_search_optimizations] &&
Feature.enabled?(:attempt_project_search_optimizations)
2019-02-15 15:39:39 +05:30
end
2018-12-05 23:21:45 +05:30
def count_key(value)
Array(value).last.to_sym
end
# rubocop: disable CodeReuse/ActiveRecord
2014-09-02 18:07:02 +05:30
def by_scope(items)
2018-03-17 18:26:18 +05:30
return items.none if current_user_related? && !current_user
2014-09-02 18:07:02 +05:30
case params[:scope]
2018-11-08 19:23:39 +05:30
when 'created_by_me', 'authored'
2014-09-02 18:07:02 +05:30
items.where(author_id: current_user.id)
2018-11-08 19:23:39 +05:30
when 'assigned_to_me'
2017-08-17 22:00:37 +05:30
items.assigned_to(current_user)
2014-09-02 18:07:02 +05:30
else
2016-06-02 11:05:42 +05:30
items
2014-09-02 18:07:02 +05:30
end
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ActiveRecord
2014-09-02 18:07:02 +05:30
2018-03-27 19:54:05 +05:30
def by_updated_at(items)
items = items.updated_after(params[:updated_after]) if params[:updated_after].present?
items = items.updated_before(params[:updated_before]) if params[:updated_before].present?
items
end
2019-05-18 00:54:41 +05:30
def by_closed_at(items)
items = items.closed_after(params[:closed_after]) if params[:closed_after].present?
items = items.closed_before(params[:closed_before]) if params[:closed_before].present?
items
end
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ActiveRecord
2014-09-02 18:07:02 +05:30
def by_state(items)
2017-01-15 13:20:01 +05:30
case params[:state].to_s
when 'closed'
items.closed
when 'merged'
items.respond_to?(:merged) ? items.merged : items.closed
when 'opened'
items.opened
2018-11-08 19:23:39 +05:30
when 'locked'
items.where(state: 'locked')
2014-09-02 18:07:02 +05:30
else
2016-11-03 12:29:30 +05:30
items
2014-09-02 18:07:02 +05:30
end
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ActiveRecord
2014-09-02 18:07:02 +05:30
def by_group(items)
2016-06-02 11:05:42 +05:30
# Selection by group is already covered by `by_project` and `projects`
2014-09-02 18:07:02 +05:30
items
end
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ActiveRecord
2014-09-02 18:07:02 +05:30
def by_project(items)
2015-10-24 18:46:33 +05:30
items =
2015-12-23 02:04:40 +05:30
if project?
2018-05-09 12:01:36 +05:30
items.of_projects(projects).references_project
elsif projects
items.merge(projects.reorder(nil)).join_project
2015-10-24 18:46:33 +05:30
else
items.none
end
2014-09-02 18:07:02 +05:30
items
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ActiveRecord
2014-09-02 18:07:02 +05:30
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ActiveRecord
2014-09-02 18:07:02 +05:30
def by_search(items)
2018-11-08 19:23:39 +05:30
return items unless search
if use_cte_for_search?
cte = Gitlab::SQL::RecursiveCTE.new(klass.table_name)
cte << items
items = klass.with(cte.to_arel).from(klass.table_name)
end
2019-03-02 22:35:43 +05:30
items.full_search(search, matched_columns: params[:in])
2017-08-17 22:00:37 +05:30
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ActiveRecord
2014-09-02 18:07:02 +05:30
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ActiveRecord
2017-08-17 22:00:37 +05:30
def by_iids(items)
params[:iids].present? ? items.where(iid: params[:iids]) : items
2014-09-02 18:07:02 +05:30
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ActiveRecord
2014-09-02 18:07:02 +05:30
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ActiveRecord
2014-09-02 18:07:02 +05:30
def sort(items)
2015-12-23 02:04:40 +05:30
# Ensure we always have an explicit sort order (instead of inheriting
# multiple orders when combining ActiveRecord::Relation objects).
2018-05-09 12:01:36 +05:30
params[:sort] ? items.sort_by_attribute(params[:sort], excluded_labels: label_names) : items.reorder(id: :desc)
2014-09-02 18:07:02 +05:30
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ActiveRecord
2014-09-02 18:07:02 +05:30
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ActiveRecord
2014-09-02 18:07:02 +05:30
def by_assignee(items)
2018-12-13 13:39:08 +05:30
if filter_by_no_assignee?
items.where(assignee_id: nil)
elsif filter_by_any_assignee?
items.where('assignee_id IS NOT NULL')
elsif assignee
items.where(assignee_id: assignee.id)
2017-08-17 22:00:37 +05:30
elsif assignee_id? || assignee_username? # assignee not found
2018-12-13 13:39:08 +05:30
items.none
else
items
2015-04-26 12:48:37 +05:30
end
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ActiveRecord
2015-04-26 12:48:37 +05:30
2018-12-13 13:39:08 +05:30
def filter_by_no_assignee?
# Assignee_id takes precedence over assignee_username
[NONE, FILTER_NONE].include?(params[:assignee_id].to_s.downcase) || params[:assignee_username].to_s == NONE
end
def filter_by_any_assignee?
params[:assignee_id].to_s.downcase == FILTER_ANY
end
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ActiveRecord
2015-04-26 12:48:37 +05:30
def by_author(items)
2017-08-17 22:00:37 +05:30
if author
items = items.where(author_id: author.id)
elsif no_author?
items = items.where(author_id: nil)
elsif author_id? || author_username? # author not found
items = items.none
2014-09-02 18:07:02 +05:30
end
items
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ActiveRecord
2014-09-02 18:07:02 +05:30
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ActiveRecord
2015-10-24 18:46:33 +05:30
def by_milestone(items)
if milestones?
if filter_by_no_milestone?
items = items.left_joins_milestones.where(milestone_id: [-1, nil])
2018-12-05 23:21:45 +05:30
elsif filter_by_any_milestone?
items = items.any_milestone
2016-06-02 11:05:42 +05:30
elsif filter_by_upcoming_milestone?
2019-03-02 22:35:43 +05:30
upcoming_ids = Milestone.upcoming_ids(projects, related_groups)
items = items.left_joins_milestones.where(milestone_id: upcoming_ids)
2017-08-17 22:00:37 +05:30
elsif filter_by_started_milestone?
2019-05-18 00:54:41 +05:30
items = items.left_joins_milestones.merge(Milestone.started)
2015-10-24 18:46:33 +05:30
else
items = items.with_milestone(params[:milestone_title])
2015-10-24 18:46:33 +05:30
end
end
items
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ActiveRecord
2015-10-24 18:46:33 +05:30
2018-12-13 13:39:08 +05:30
def filter_by_no_milestone?
# Accepts `No Milestone` for compatibility
params[:milestone_title].to_s.downcase == FILTER_NONE || params[:milestone_title] == Milestone::None.title
end
def filter_by_any_milestone?
# Accepts `Any Milestone` for compatibility
params[:milestone_title].to_s.downcase == FILTER_ANY || params[:milestone_title] == Milestone::Any.title
end
def filter_by_upcoming_milestone?
params[:milestone_title] == Milestone::Upcoming.name
end
def filter_by_started_milestone?
params[:milestone_title] == Milestone::Started.name
end
2014-09-02 18:07:02 +05:30
def by_label(items)
2018-03-17 18:26:18 +05:30
return items unless labels?
items =
2015-10-24 18:46:33 +05:30
if filter_by_no_label?
2018-03-17 18:26:18 +05:30
items.without_label
2019-02-15 15:39:39 +05:30
elsif filter_by_any_label?
items.any_label
2015-10-24 18:46:33 +05:30
else
2018-03-17 18:26:18 +05:30
items.with_label(label_names, params[:sort])
2015-10-24 18:46:33 +05:30
end
2018-03-17 18:26:18 +05:30
items
end
def by_my_reaction_emoji(items)
if params[:my_reaction_emoji].present? && current_user
2018-12-13 13:39:08 +05:30
items =
if filter_by_no_reaction?
items.not_awarded(current_user)
elsif filter_by_any_reaction?
items.awarded(current_user)
else
items.awarded(current_user, params[:my_reaction_emoji])
end
2014-09-02 18:07:02 +05:30
end
items
end
2018-12-13 13:39:08 +05:30
def filter_by_no_reaction?
params[:my_reaction_emoji].to_s.downcase == FILTER_NONE
end
def filter_by_any_reaction?
params[:my_reaction_emoji].to_s.downcase == FILTER_ANY
end
2016-04-02 18:10:28 +05:30
def label_names
if labels?
params[:label_name].is_a?(String) ? params[:label_name].split(',') : params[:label_name]
else
[]
end
2016-04-02 18:10:28 +05:30
end
2017-08-17 22:00:37 +05:30
def by_non_archived(items)
params[:non_archived].present? ? items.non_archived : items
end
2015-04-26 12:48:37 +05:30
def current_user_related?
2018-11-08 19:23:39 +05:30
scope = params[:scope]
scope == 'created_by_me' || scope == 'authored' || scope == 'assigned_to_me'
2015-04-26 12:48:37 +05:30
end
2014-09-02 18:07:02 +05:30
end