Commit 00b81c87 authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Add OR filtering for author username

This adds OR filtering of authors for issues, MRs, and epics
parent 7b52ec04
......@@ -88,7 +88,7 @@ class IssuableFinder
end
def valid_params
@valid_params ||= scalar_params + [array_params.merge(not: {})]
@valid_params ||= scalar_params + [array_params.merge(or: {}, not: {})]
end
end
......@@ -377,6 +377,7 @@ class IssuableFinder
Issuables::AuthorFilter.new(
items,
params: original_params,
or_filters_enabled: or_filters_enabled?,
not_filters_enabled: not_filters_enabled?
).filter
end
......@@ -498,6 +499,12 @@ class IssuableFinder
params[:non_archived].present? ? items.non_archived : items
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)
......
# 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 no_author?
issuables.where(author_id: nil)
elsif params[:author_id].present?
issuables.where(author_id: params[:author_id])
if params[:author_id].present?
issuables.authored(params[:author_id])
elsif params[:author_username].present?
issuables.where(author_id: authors_by_username(params[:author_username]))
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_params.present? && not_filters_enabled?
return issuables unless not_filters_enabled? && not_params.present?
if not_params[:author_id].present?
issuables.where.not(author_id: not_params[:author_id])
issuables.not_authored(not_params[:author_id])
elsif not_params[:author_username].present?
issuables.where.not(author_id: authors_by_username(not_params[:author_username]))
issuables.not_authored(User.by_username(not_params[:author_username]))
else
issuables
end
end
def no_author?
# author_id takes precedence over author_username
params[:author_id] == NONE || params[:author_username] == NONE
end
def authors_by_username(usernames)
User.where(username: usernames)
end
end
end
# frozen_string_literal: true
module Issuables
class BaseFilter
# This is used as a common filter for None / Any
FILTER_NONE = 'none'
FILTER_ANY = 'any'
# This is used in unassigning users
NONE = '0'
attr_reader :issuables, :params
def initialize(issuables, params:, not_filters_enabled: false)
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
......@@ -21,10 +17,18 @@ module Issuables
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
......
......@@ -86,6 +86,7 @@ module Issuable
before_validation :truncate_description_on_import!
scope :authored, ->(user) { where(author_id: user) }
scope :not_authored, ->(user) { where.not(author_id: user) }
scope :recent, -> { reorder(id: :desc) }
scope :of_projects, ->(ids) { where(project_id: ids) }
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
......@@ -105,6 +105,22 @@ RSpec.describe EpicsFinder do
it 'returns all epics authored by the given user' do
expect(epics(author_id: user.id)).to contain_exactly(epic2)
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
context 'by label' do
......
......@@ -179,33 +179,54 @@ RSpec.describe IssuesFinder do
end
end
context 'filtering by author ID' do
let(:params) { { author_id: user2.id } }
context 'filtering by author' do
context 'by author ID' do
let(:params) { { author_id: user2.id } }
it 'returns issues created by that user' do
expect(issues).to contain_exactly(issue3)
it 'returns issues created by that user' do
expect(issues).to contain_exactly(issue3)
end
end
end
context 'filtering by not author ID' do
let(:params) { { not: { author_id: user2.id } } }
context 'using OR' do
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
expect(issues).to contain_exactly(issue1, issue2, issue4, issue5)
it 'does not add any filter' do
expect(issues).to contain_exactly(issue1, issue2, issue3, issue4, issue5, issue6)
end
end
end
end
context 'filtering by nonexistent author ID and issue term using CTE for search' do
let(:params) do
{
author_id: 'does-not-exist',
search: 'git',
attempt_group_search_optimizations: true
}
context 'filtering by NOT author ID' do
let(:params) { { not: { author_id: user2.id } } }
it 'returns issues not created by that user' do
expect(issues).to contain_exactly(issue1, issue2, issue4, issue5)
end
end
it 'returns no results' do
expect(issues).to be_empty
context 'filtering by nonexistent author ID and issue term using CTE for search' do
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
......
......@@ -41,30 +41,51 @@ RSpec.describe MergeRequestsFinder do
expect(merge_requests).to contain_exactly(merge_request1)
end
it 'filters by nonexistent author ID and MR term using CTE for search' do
params = {
author_id: 'does-not-exist',
search: 'git',
attempt_group_search_optimizations: true
}
context 'filtering by author' do
subject(:merge_requests) { described_class.new(user, params).execute }
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
end
before do
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
let(:params) { { not: { author_id: user2.id } } }
context 'when feature flag is disabled' do
before do
stub_feature_flags(or_issuable_queries: false)
end
before do
merge_request2.update!(author: user2)
merge_request3.update!(author: user2)
it 'does not add any filter' do
expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request3, merge_request4, merge_request5)
end
end
end
it 'returns merge requests not created by that user' do
merge_requests = described_class.new(user, params).execute
context 'with nonexistent author ID and MR term using CTE for search' do
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
......
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