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

531 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
#
2021-03-08 18:12:59 +05:30
# Note: This class is NOT meant to be instantiated. Instead you should
# look at IssuesFinder or EpicsFinder, which inherit from this.
#
2014-09-02 18:07:02 +05:30
# 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
2021-10-27 15:23:28 +05:30
# milestone_title: string (cannot be simultaneously used with milestone_wildcard_id)
# milestone_wildcard_id: 'none', 'any', 'upcoming', 'started' (cannot be simultaneously used with milestone_title)
2020-01-01 13:55:28 +05:30
# release_tag: 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
2019-07-07 11:18:12 +05:30
# attempt_project_search_optimizations: boolean
2022-01-26 12:08:38 +05:30
# crm_contact_id: integer
# crm_organization_id: integer
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
2020-04-22 19:07:51 +05:30
requires_cross_project_access unless: -> { params.project? }
2019-12-04 20:38:33 +05:30
2023-01-13 00:05:48 +05:30
FULL_TEXT_SEARCH_TERM_PATTERN = '[\u0000-\u02FF\u1E00-\u1EFF\u2070-\u218F]*'
FULL_TEXT_SEARCH_TERM_REGEX = /\A#{FULL_TEXT_SEARCH_TERM_PATTERN}\z/.freeze
2020-05-24 23:13:21 +05:30
NEGATABLE_PARAMS_HELPER_KEYS = %i[project_id scope status include_subgroups].freeze
2015-04-26 12:48:37 +05:30
2014-09-02 18:07:02 +05:30
attr_accessor :current_user, :params
2021-04-17 20:07:23 +05:30
attr_reader :original_params
2020-11-24 15:15:51 +05:30
attr_writer :parent
2014-09-02 18:07:02 +05:30
2021-09-04 01:27:46 +05:30
delegate(*%i[milestones], to: :params)
2020-04-22 19:07:51 +05:30
2019-12-04 20:38:33 +05:30
class << self
def scalar_params
@scalar_params ||= %i[
2022-10-11 01:57:18 +05:30
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
]
2019-12-04 20:38:33 +05:30
end
2018-03-27 19:54:05 +05:30
2019-12-04 20:38:33 +05:30
def array_params
@array_params ||= { label_name: [], assignee_username: [] }
end
# This should not be used in controller strong params!
def negatable_scalar_params
2020-05-24 23:13:21 +05:30
@negatable_scalar_params ||= scalar_params - %i[search in]
2019-12-04 20:38:33 +05:30
end
# This should not be used in controller strong params!
def negatable_array_params
@negatable_array_params ||= array_params.keys.append(:iids)
end
# This should not be used in controller strong params!
def negatable_params
@negatable_params ||= negatable_scalar_params + negatable_array_params
end
2018-03-27 19:54:05 +05:30
2019-12-04 20:38:33 +05:30
def valid_params
2021-04-17 20:07:23 +05:30
@valid_params ||= scalar_params + [array_params.merge(or: {}, not: {})]
2019-12-04 20:38:33 +05:30
end
2018-03-27 19:54:05 +05:30
end
2020-04-22 19:07:51 +05:30
def params_class
IssuableFinder::Params
end
2021-03-08 18:12:59 +05:30
def klass
raise NotImplementedError
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
2021-04-17 20:07:23 +05:30
@original_params = params
2020-04-22 19:07:51 +05:30
@params = params_class.new(params, current_user, klass)
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-12-04 20:38:33 +05:30
# Let's see if we have to negate anything
2021-01-03 14:25:43 +05:30
items = filter_negated_items(items) if should_filter_negated_args?
2019-12-04 20:38:33 +05:30
2019-07-07 11:18:12 +05:30
# This has to be last as we use a CTE as an optimization fence
2020-11-24 15:15:51 +05:30
# for counts by passing the force_cte param and passing the
# attempt_group_search_optimizations param
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
2021-04-29 21:17:54 +05:30
sort(items)
2018-03-27 19:54:05 +05:30
end
def filter_items(items)
2021-04-29 21:17:54 +05:30
# Selection by group is already covered by `by_project` and `projects` for project-based issuables
# Group-based issuables have their own group filter methods
2018-11-08 19:23:39 +05:30
items = by_project(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-07-07 11:18:12 +05:30
items = by_closed_at(items)
2014-09-02 18:07:02 +05:30
items = by_state(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)
2020-01-01 13:55:28 +05:30
items = by_release(items)
2017-08-17 22:00:37 +05:30
items = by_label(items)
2022-01-26 12:08:38 +05:30
items = by_my_reaction_emoji(items)
items = by_crm_contact(items)
by_crm_organization(items)
2017-01-15 13:20:01 +05:30
end
2021-01-03 14:25:43 +05:30
def should_filter_negated_args?
2020-05-24 23:13:21 +05:30
# API endpoints send in `nil` values so we test if there are any non-nil
2021-01-03 14:25:43 +05:30
not_params.present? && not_params.values.any?
end
2020-05-24 23:13:21 +05:30
2021-01-03 14:25:43 +05:30
# Negates all params found in `negatable_params`
def filter_negated_items(items)
2020-05-24 23:13:21 +05:30
items = by_negated_milestone(items)
items = by_negated_release(items)
items = by_negated_my_reaction_emoji(items)
by_negated_iids(items)
end
2018-03-17 18:26:18 +05:30
def row_count
2021-01-03 14:25:43 +05:30
Gitlab::IssuablesCountForState
2021-01-29 00:20:46 +05:30
.new(self, nil, fast_fail: true)
2021-01-03 14:25:43 +05:30
.for_state_or_opened(params[:state])
2018-03-17 18:26:18 +05:30
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-07-07 11:18:12 +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-07-07 11:18:12 +05:30
2021-11-11 11:23:49 +05:30
state_counts = finder
.execute
.reorder(nil)
.group(:state_id)
.count
2017-08-17 22:00:37 +05:30
counts = Hash.new(0)
2021-11-11 11:23:49 +05:30
state_counts.each do |key, value|
counts[count_key(key)] += value
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 search
params[:search].presence
end
2019-02-15 15:39:39 +05:30
def use_cte_for_search?
strong_memoize(:use_cte_for_search) do
2019-07-07 11:18:12 +05:30
next false unless search
2021-11-18 22:05:49 +05:30
next false unless default_or_simple_sort?
2019-07-07 11:18:12 +05:30
attempt_group_search_optimizations? || attempt_project_search_optimizations?
2019-02-15 15:39:39 +05:30
end
end
2020-11-24 15:15:51 +05:30
def parent_param=(obj)
@parent = obj
params[parent_param] = parent if parent
end
def parent_param
case parent
when Project
:project_id
when Group
:group_id
else
raise "Unexpected parent: #{parent.class}"
end
end
2014-09-02 18:07:02 +05:30
private
2020-11-24 15:15:51 +05:30
attr_reader :parent
2020-05-24 23:13:21 +05:30
def not_params
strong_memoize(:not_params) do
params_class.new(params[:not].dup, current_user, klass).tap do |not_params|
next unless not_params.present?
# These are "helper" params that modify the results, like :in and :search. They usually come in at the top-level
# params, but if they do come in inside the `:not` params, the inner ones should take precedence.
2021-04-29 21:17:54 +05:30
not_helpers = params.slice(*NEGATABLE_PARAMS_HELPER_KEYS).merge(params[:not].to_h.slice(*NEGATABLE_PARAMS_HELPER_KEYS))
2020-05-24 23:13:21 +05:30
not_helpers.each do |key, value|
not_params[key] = value unless not_params[key].present?
end
end
end
end
2019-07-07 11:18:12 +05:30
def force_cte?
!!params[:force_cte]
end
2014-09-02 18:07:02 +05:30
def init_collection
2023-03-17 16:20:25 +05:30
return klass.all if params.user_can_see_all_issuables?
# Only admins and auditors can see hidden issuables, for other users we filter out hidden issuables
klass.without_hidden
2014-09-02 18:07:02 +05:30
end
2021-11-18 22:05:49 +05:30
def default_or_simple_sort?
params[:sort].blank? || params[:sort].to_s.in?(klass.simple_sorts.keys)
end
2019-02-15 15:39:39 +05:30
def attempt_group_search_optimizations?
2020-11-24 15:15:51 +05:30
params[:attempt_group_search_optimizations]
2019-07-07 11:18:12 +05:30
end
def attempt_project_search_optimizations?
2020-11-24 15:15:51 +05:30
params[: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)
2019-12-26 22:10:19 +05:30
# value may be an array if the finder used in `count_by_state` added an
# additional `group by`. Anyway we are sure that state will be always the
# last item because it's added as the last one to the query.
2019-12-21 20:55:43 +05:30
value = Array(value).last
klass.available_states.key(value)
2018-12-05 23:21:45 +05:30
end
# rubocop: disable CodeReuse/ActiveRecord
2014-09-02 18:07:02 +05:30
def by_scope(items)
2020-04-22 19:07:51 +05:30
return items.none if params.current_user_related? && !current_user
2018-03-17 18:26:18 +05:30
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-07-07 11:18:12 +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
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'
2019-12-21 20:55:43 +05:30
items.with_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: disable CodeReuse/ActiveRecord
2014-09-02 18:07:02 +05:30
def by_project(items)
2022-08-13 15:12:31 +05:30
# When finding issues for multiple projects it's more efficient
# to use a JOIN instead of running a sub-query
# See https://gitlab.com/gitlab-org/gitlab/-/commit/8591cc02be6b12ed60f763a5e0147f2cbbca99e1
if params.projects.is_a?(ActiveRecord::Relation)
items.merge(params.projects.reorder(nil)).join_project
elsif params.projects
2020-04-22 19:07:51 +05:30
items.of_projects(params.projects).references_project
else
items.none
end
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_search(items)
2018-11-08 19:23:39 +05:30
return items unless search
2019-12-26 22:10:19 +05:30
return items if items.is_a?(ActiveRecord::NullRelation)
2021-11-11 11:23:49 +05:30
return items if Feature.enabled?(:disable_anonymous_search, type: :ops) && current_user.nil?
2018-11-08 19:23:39 +05:30
2023-01-13 00:05:48 +05:30
return filter_by_full_text_search(items) if use_full_text_search?
2022-05-07 20:08:51 +05:30
2018-11-08 19:23:39 +05:30
if use_cte_for_search?
2021-06-08 01:23:25 +05:30
cte = Gitlab::SQL::CTE.new(klass.table_name, items)
2018-11-08 19:23:39 +05:30
items = klass.with(cte.to_arel).from(klass.table_name)
end
2019-09-30 21:07:59 +05:30
items.full_search(search, matched_columns: params[:in], use_minimum_char_limit: !use_cte_for_search?)
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
2022-05-07 20:08:51 +05:30
def use_full_text_search?
2023-01-13 00:05:48 +05:30
klass.try(:pg_full_text_searchable_columns).present? &&
2022-05-07 20:08:51 +05:30
params[:search] =~ FULL_TEXT_SEARCH_TERM_REGEX &&
2022-07-16 23:28:13 +05:30
Feature.enabled?(:issues_full_text_search, params.project || params.group)
2022-05-07 20:08:51 +05:30
end
2023-01-13 00:05:48 +05:30
def filter_by_full_text_search(items)
items.pg_full_text_search(search, matched_columns: params[:in].to_s.split(','))
end
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
2020-05-24 23:13:21 +05:30
# rubocop: disable CodeReuse/ActiveRecord
def by_negated_iids(items)
not_params[:iids].present? ? items.where.not(iid: not_params[:iids]) : items
end
# rubocop: enable CodeReuse/ActiveRecord
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).
2021-11-11 11:23:49 +05:30
params[:sort] ? items.sort_by_attribute(params[:sort], excluded_labels: label_filter.label_names_excluded_from_priority_sort) : 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
2015-04-26 12:48:37 +05:30
def by_author(items)
2021-04-17 20:07:23 +05:30
Issuables::AuthorFilter.new(
params: original_params,
2021-06-08 01:23:25 +05:30
or_filters_enabled: or_filters_enabled?
2021-09-04 01:27:46 +05:30
).filter(items)
2020-05-24 23:13:21 +05:30
end
2020-03-13 15:44:24 +05:30
2020-05-24 23:13:21 +05:30
def by_assignee(items)
2021-09-04 01:27:46 +05:30
assignee_filter.filter(items)
2019-07-31 22:56:46 +05:30
end
2021-09-04 01:27:46 +05:30
def assignee_filter
strong_memoize(:assignee_filter) do
Issuables::AssigneeFilter.new(
params: original_params,
or_filters_enabled: or_filters_enabled?
)
2020-05-24 23:13:21 +05:30
end
end
2021-11-11 11:23:49 +05:30
def by_label(items)
label_filter.filter(items)
end
def label_filter
strong_memoize(:label_filter) do
Issuables::LabelFilter.new(
params: original_params,
project: params.project,
2023-03-17 16:20:25 +05:30
group: params.group,
or_filters_enabled: or_filters_enabled?
2021-11-11 11:23:49 +05:30
)
end
end
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ActiveRecord
2015-10-24 18:46:33 +05:30
def by_milestone(items)
2020-04-22 19:07:51 +05:30
return items unless params.milestones?
if params.filter_by_no_milestone?
items.left_joins_milestones.where(milestone_id: [-1, nil])
elsif params.filter_by_any_milestone?
items.any_milestone
elsif params.filter_by_upcoming_milestone?
upcoming_ids = Milestone.upcoming_ids(params.projects, params.related_groups)
items.left_joins_milestones.where(milestone_id: upcoming_ids)
elsif params.filter_by_started_milestone?
items.left_joins_milestones.merge(Milestone.started)
else
items.with_milestone(params[:milestone_title])
2015-10-24 18:46:33 +05:30
end
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ActiveRecord
2015-10-24 18:46:33 +05:30
2020-05-24 23:13:21 +05:30
# rubocop: disable CodeReuse/ActiveRecord
def by_negated_milestone(items)
return items unless not_params.milestones?
if not_params.filter_by_upcoming_milestone?
items.joins(:milestone).merge(Milestone.not_upcoming)
elsif not_params.filter_by_started_milestone?
items.joins(:milestone).merge(Milestone.not_started)
else
2022-08-13 15:12:31 +05:30
items.without_particular_milestones(not_params[:milestone_title])
2020-05-24 23:13:21 +05:30
end
end
# rubocop: enable CodeReuse/ActiveRecord
2020-01-01 13:55:28 +05:30
def by_release(items)
2020-04-22 19:07:51 +05:30
return items unless params.releases?
2021-03-08 18:12:59 +05:30
return items if params.group? # don't allow release filtering at group level
2020-01-01 13:55:28 +05:30
2020-04-22 19:07:51 +05:30
if params.filter_by_no_release?
2020-01-01 13:55:28 +05:30
items.without_release
2020-04-22 19:07:51 +05:30
elsif params.filter_by_any_release?
2020-01-01 13:55:28 +05:30
items.any_release
else
items.with_release(params[:release_tag], params[:project_id])
end
end
2020-05-24 23:13:21 +05:30
def by_negated_release(items)
return items unless not_params.releases?
items.without_particular_release(not_params[:release_tag], not_params[:project_id])
end
2020-04-22 19:07:51 +05:30
def by_my_reaction_emoji(items)
return items unless params[:my_reaction_emoji] && current_user
2018-12-13 13:39:08 +05:30
2020-04-22 19:07:51 +05:30
if params.filter_by_no_reaction?
items.not_awarded(current_user)
elsif params.filter_by_any_reaction?
items.awarded(current_user)
else
2020-04-22 19:07:51 +05:30
items.awarded(current_user, params[:my_reaction_emoji])
end
2016-04-02 18:10:28 +05:30
end
2020-05-24 23:13:21 +05:30
def by_negated_my_reaction_emoji(items)
return items unless not_params[:my_reaction_emoji] && current_user
items.not_awarded(current_user, not_params[:my_reaction_emoji])
2017-08-17 22:00:37 +05:30
end
2020-05-24 23:13:21 +05:30
def by_non_archived(items)
params[:non_archived].present? ? items.non_archived : items
2020-03-13 15:44:24 +05:30
end
2021-04-17 20:07:23 +05:30
2022-01-26 12:08:38 +05:30
def by_crm_contact(items)
2022-07-29 17:44:30 +05:30
return items unless can_filter_by_crm_contact?
2022-01-26 12:08:38 +05:30
Issuables::CrmContactFilter.new(params: original_params).filter(items)
end
def by_crm_organization(items)
2022-07-29 17:44:30 +05:30
return items unless can_filter_by_crm_organization?
2022-01-26 12:08:38 +05:30
Issuables::CrmOrganizationFilter.new(params: original_params).filter(items)
end
2021-04-17 20:07:23 +05:30
def or_filters_enabled?
strong_memoize(:or_filters_enabled) do
2022-07-16 23:28:13 +05:30
Feature.enabled?(:or_issuable_queries, feature_flag_scope)
2021-04-17 20:07:23 +05:30
end
end
def feature_flag_scope
params.group || params.project
end
2022-07-29 17:44:30 +05:30
def can_filter_by_crm_contact?
current_user&.can?(:read_crm_contact, root_group)
end
def can_filter_by_crm_organization?
current_user&.can?(:read_crm_organization, root_group)
end
def root_group
strong_memoize(:root_group) do
base_group = params.group || params.project&.group
base_group&.root_ancestor
end
end
2014-09-02 18:07:02 +05:30
end