Commit 64dff20e authored by Jan Provaznik's avatar Jan Provaznik

Merge branch 'issuable-or-filters-author' into 'master'

Add IssuableFinder support for OR filtering of authors [RUN ALL RSPEC] [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!54444
parents de1b5559 00b81c87
...@@ -47,6 +47,7 @@ class IssuableFinder ...@@ -47,6 +47,7 @@ class IssuableFinder
NEGATABLE_PARAMS_HELPER_KEYS = %i[project_id scope status include_subgroups].freeze NEGATABLE_PARAMS_HELPER_KEYS = %i[project_id scope status include_subgroups].freeze
attr_accessor :current_user, :params attr_accessor :current_user, :params
attr_reader :original_params
attr_writer :parent attr_writer :parent
delegate(*%i[assignee milestones], to: :params) delegate(*%i[assignee milestones], to: :params)
...@@ -87,7 +88,7 @@ class IssuableFinder ...@@ -87,7 +88,7 @@ class IssuableFinder
end end
def valid_params def valid_params
@valid_params ||= scalar_params + [array_params.merge(not: {})] @valid_params ||= scalar_params + [array_params.merge(or: {}, not: {})]
end end
end end
...@@ -101,6 +102,7 @@ class IssuableFinder ...@@ -101,6 +102,7 @@ class IssuableFinder
def initialize(current_user, params = {}) def initialize(current_user, params = {})
@current_user = current_user @current_user = current_user
@original_params = params
@params = params_class.new(params, current_user, klass) @params = params_class.new(params, current_user, klass)
end end
...@@ -142,7 +144,7 @@ class IssuableFinder ...@@ -142,7 +144,7 @@ class IssuableFinder
end end
def should_filter_negated_args? def should_filter_negated_args?
return false unless Feature.enabled?(:not_issuable_queries, params.group || params.project, default_enabled: true) return false unless not_filters_enabled?
# API endpoints send in `nil` values so we test if there are any non-nil # API endpoints send in `nil` values so we test if there are any non-nil
not_params.present? && not_params.values.any? not_params.present? && not_params.values.any?
...@@ -150,7 +152,6 @@ class IssuableFinder ...@@ -150,7 +152,6 @@ class IssuableFinder
# Negates all params found in `negatable_params` # Negates all params found in `negatable_params`
def filter_negated_items(items) def filter_negated_items(items)
items = by_negated_author(items)
items = by_negated_assignee(items) items = by_negated_assignee(items)
items = by_negated_label(items) items = by_negated_label(items)
items = by_negated_milestone(items) items = by_negated_milestone(items)
...@@ -372,31 +373,14 @@ class IssuableFinder ...@@ -372,31 +373,14 @@ class IssuableFinder
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def by_author(items) def by_author(items)
if params.author Issuables::AuthorFilter.new(
items.where(author_id: params.author.id) items,
elsif params.no_author? params: original_params,
items.where(author_id: nil) or_filters_enabled: or_filters_enabled?,
elsif params.author_id? || params.author_username? # author not found not_filters_enabled: not_filters_enabled?
items.none ).filter
else
items
end
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def by_negated_author(items)
if not_params.author
items.where.not(author_id: not_params.author.id)
elsif not_params.author_id? || not_params.author_username? # author not found
items.none
else
items
end
end end
# rubocop: enable CodeReuse/ActiveRecord
def by_assignee(items) def by_assignee(items)
if params.filter_by_no_assignee? if params.filter_by_no_assignee?
...@@ -514,4 +498,20 @@ class IssuableFinder ...@@ -514,4 +498,20 @@ class IssuableFinder
def by_non_archived(items) def by_non_archived(items)
params[:non_archived].present? ? items.non_archived : items params[:non_archived].present? ? items.non_archived : items
end end
def or_filters_enabled?
strong_memoize(:or_filters_enabled) do
Feature.enabled?(:or_issuable_queries, feature_flag_scope, default_enabled: :yaml)
end
end
def not_filters_enabled?
strong_memoize(:not_filters_enabled) do
Feature.enabled?(:not_issuable_queries, feature_flag_scope, default_enabled: :yaml)
end
end
def feature_flag_scope
params.group || params.project
end
end end
...@@ -27,19 +27,6 @@ class IssuableFinder ...@@ -27,19 +27,6 @@ class IssuableFinder
params.present? params.present?
end end
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
end
def filter_by_no_assignee? def filter_by_no_assignee?
params[:assignee_id].to_s.downcase == FILTER_NONE params[:assignee_id].to_s.downcase == FILTER_NONE
end end
...@@ -169,20 +156,6 @@ class IssuableFinder ...@@ -169,20 +156,6 @@ class IssuableFinder
end end
end end
# rubocop: disable CodeReuse/ActiveRecord
def author
strong_memoize(:author) do
if author_id?
User.find_by(id: params[:author_id])
elsif author_username?
User.find_by_username(params[:author_username])
else
nil
end
end
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def assignees def assignees
strong_memoize(:assignees) do strong_memoize(:assignees) do
......
# frozen_string_literal: true
module Issuables
class AuthorFilter < BaseFilter
def filter
filtered = by_author(issuables)
filtered = by_author_union(filtered)
by_negated_author(filtered)
end
private
def by_author(issuables)
if params[:author_id].present?
issuables.authored(params[:author_id])
elsif params[:author_username].present?
issuables.authored(User.by_username(params[:author_username]))
else
issuables
end
end
def by_author_union(issuables)
return issuables unless or_filters_enabled? && or_params&.fetch(:author_username).present?
issuables.authored(User.by_username(or_params[:author_username]))
end
def by_negated_author(issuables)
return issuables unless not_filters_enabled? && not_params.present?
if not_params[:author_id].present?
issuables.not_authored(not_params[:author_id])
elsif not_params[:author_username].present?
issuables.not_authored(User.by_username(not_params[:author_username]))
else
issuables
end
end
end
end
# frozen_string_literal: true
module Issuables
class BaseFilter
attr_reader :issuables, :params
def initialize(issuables, params:, or_filters_enabled: false, not_filters_enabled: false)
@issuables = issuables
@params = params
@or_filters_enabled = or_filters_enabled
@not_filters_enabled = not_filters_enabled
end
def filter
raise NotImplementedError
end
private
def or_params
params[:or]
end
def not_params
params[:not]
end
def or_filters_enabled?
@or_filters_enabled
end
def not_filters_enabled?
@not_filters_enabled
end
end
end
...@@ -86,6 +86,7 @@ module Issuable ...@@ -86,6 +86,7 @@ module Issuable
before_validation :truncate_description_on_import! before_validation :truncate_description_on_import!
scope :authored, ->(user) { where(author_id: user) } scope :authored, ->(user) { where(author_id: user) }
scope :not_authored, ->(user) { where.not(author_id: user) }
scope :recent, -> { reorder(id: :desc) } scope :recent, -> { reorder(id: :desc) }
scope :of_projects, ->(ids) { where(project_id: ids) } scope :of_projects, ->(ids) { where(project_id: ids) }
scope :opened, -> { with_state(:opened) } scope :opened, -> { with_state(:opened) }
......
---
name: or_issuable_queries
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54444
rollout_issue_url:
milestone: '13.10'
type: development
group: group::project management
default_enabled: false
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
class EpicsFinder < IssuableFinder class EpicsFinder < IssuableFinder
include TimeFrameFilter include TimeFrameFilter
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
extend ::Gitlab::Utils::Override
IID_STARTS_WITH_PATTERN = %r{\A(\d)+\z}.freeze IID_STARTS_WITH_PATTERN = %r{\A(\d)+\z}.freeze
...@@ -123,13 +124,12 @@ class EpicsFinder < IssuableFinder ...@@ -123,13 +124,12 @@ class EpicsFinder < IssuableFinder
end end
def filter_negated_items(items) def filter_negated_items(items)
return items unless Feature.enabled?(:not_issuable_queries, group, default_enabled: true) return items unless not_filters_enabled?
# API endpoints send in `nil` values so we test if there are any non-nil # API endpoints send in `nil` values so we test if there are any non-nil
return items unless not_params&.values&.any? return items unless not_params&.values&.any?
items = by_negated_label(items) by_negated_label(items)
by_negated_author(items)
end end
def group def group
...@@ -242,4 +242,9 @@ class EpicsFinder < IssuableFinder ...@@ -242,4 +242,9 @@ class EpicsFinder < IssuableFinder
def group_projects def group_projects
Project.in_namespace(permissioned_related_groups).with_issues_available_for_user(current_user) Project.in_namespace(permissioned_related_groups).with_issues_available_for_user(current_user)
end end
override :feature_flag_scope
def feature_flag_scope
group
end
end end
...@@ -105,6 +105,22 @@ RSpec.describe EpicsFinder do ...@@ -105,6 +105,22 @@ RSpec.describe EpicsFinder do
it 'returns all epics authored by the given user' do it 'returns all epics authored by the given user' do
expect(epics(author_id: user.id)).to contain_exactly(epic2) expect(epics(author_id: user.id)).to contain_exactly(epic2)
end end
context 'using OR' do
it 'returns all epics authored by any of the given users' do
expect(epics(or: { author_username: [epic2.author.username, epic3.author.username] })).to contain_exactly(epic2, epic3)
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(or_issuable_queries: false)
end
it 'does not add any filter' do
expect(epics(or: { author_username: [epic2.author.username, epic3.author.username] })).to contain_exactly(epic1, epic2, epic3)
end
end
end
end end
context 'by label' do context 'by label' do
......
...@@ -179,33 +179,54 @@ RSpec.describe IssuesFinder do ...@@ -179,33 +179,54 @@ RSpec.describe IssuesFinder do
end end
end end
context 'filtering by author ID' do context 'filtering by author' do
let(:params) { { author_id: user2.id } } context 'by author ID' do
let(:params) { { author_id: user2.id } }
it 'returns issues created by that user' do it 'returns issues created by that user' do
expect(issues).to contain_exactly(issue3) expect(issues).to contain_exactly(issue3)
end
end end
end
context 'filtering by not author ID' do context 'using OR' do
let(:params) { { not: { author_id: user2.id } } } let(:issue6) { create(:issue, project: project2) }
let(:params) { { or: { author_username: [issue3.author.username, issue6.author.username] } } }
it 'returns issues created by any of the given users' do
expect(issues).to contain_exactly(issue3, issue6)
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(or_issuable_queries: false)
end
it 'returns issues not created by that user' do it 'does not add any filter' do
expect(issues).to contain_exactly(issue1, issue2, issue4, issue5) expect(issues).to contain_exactly(issue1, issue2, issue3, issue4, issue5, issue6)
end
end
end end
end
context 'filtering by nonexistent author ID and issue term using CTE for search' do context 'filtering by NOT author ID' do
let(:params) do let(:params) { { not: { author_id: user2.id } } }
{
author_id: 'does-not-exist', it 'returns issues not created by that user' do
search: 'git', expect(issues).to contain_exactly(issue1, issue2, issue4, issue5)
attempt_group_search_optimizations: true end
}
end end
it 'returns no results' do context 'filtering by nonexistent author ID and issue term using CTE for search' do
expect(issues).to be_empty let(:params) do
{
author_id: 'does-not-exist',
search: 'git',
attempt_group_search_optimizations: true
}
end
it 'returns no results' do
expect(issues).to be_empty
end
end end
end end
......
...@@ -41,30 +41,51 @@ RSpec.describe MergeRequestsFinder do ...@@ -41,30 +41,51 @@ RSpec.describe MergeRequestsFinder do
expect(merge_requests).to contain_exactly(merge_request1) expect(merge_requests).to contain_exactly(merge_request1)
end end
it 'filters by nonexistent author ID and MR term using CTE for search' do context 'filtering by author' do
params = { subject(:merge_requests) { described_class.new(user, params).execute }
author_id: 'does-not-exist',
search: 'git',
attempt_group_search_optimizations: true
}
merge_requests = described_class.new(user, params).execute context 'using OR' do
let(:params) { { or: { author_username: [merge_request1.author.username, merge_request2.author.username] } } }
expect(merge_requests).to be_empty before do
end merge_request1.update!(author: create(:user))
merge_request2.update!(author: create(:user))
end
it 'returns merge requests created by any of the given users' do
expect(merge_requests).to contain_exactly(merge_request1, merge_request2)
end
context 'filtering by not author ID' do context 'when feature flag is disabled' do
let(:params) { { not: { author_id: user2.id } } } before do
stub_feature_flags(or_issuable_queries: false)
end
before do it 'does not add any filter' do
merge_request2.update!(author: user2) expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request3, merge_request4, merge_request5)
merge_request3.update!(author: user2) end
end
end end
it 'returns merge requests not created by that user' do context 'with nonexistent author ID and MR term using CTE for search' do
merge_requests = described_class.new(user, params).execute let(:params) { { author_id: 'does-not-exist', search: 'git', attempt_group_search_optimizations: true } }
it 'returns no results' do
expect(merge_requests).to be_empty
end
end
expect(merge_requests).to contain_exactly(merge_request1, merge_request4, merge_request5) context 'filtering by not author ID' do
let(:params) { { not: { author_id: user2.id } } }
before do
merge_request2.update!(author: user2)
merge_request3.update!(author: user2)
end
it 'returns merge requests not created by that user' do
expect(merge_requests).to contain_exactly(merge_request1, merge_request4, merge_request5)
end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment