Commit b08e0e74 authored by Dylan Griffith's avatar Dylan Griffith

Merge branch '255322-search-sort-issues-and-mrs' into 'master'

Add the Ability to Sort to Issue and Merge Request: Backend

Closes #255322

See merge request gitlab-org/gitlab!43295
parents 953eef55 34b6ce77
# frozen_string_literal: true # frozen_string_literal: true
module SearchHelper module SearchHelper
SEARCH_PERMITTED_PARAMS = [:search, :scope, :project_id, :group_id, :repository_ref, :snippets, :state, :confidential].freeze SEARCH_PERMITTED_PARAMS = [:search, :scope, :project_id, :group_id, :repository_ref, :snippets, :sort, :state, :confidential].freeze
def search_autocomplete_opts(term) def search_autocomplete_opts(term)
return unless current_user return unless current_user
......
...@@ -14,6 +14,7 @@ module Search ...@@ -14,6 +14,7 @@ module Search
Gitlab::SearchResults.new(current_user, Gitlab::SearchResults.new(current_user,
params[:search], params[:search],
projects, projects,
sort: params[:sort],
filters: { state: params[:state], confidential: params[:confidential] }) filters: { state: params[:state], confidential: params[:confidential] })
end end
......
---
title: Add sort parameter to Issue and Merge Request scopes
merge_request: 43295
author:
type: added
...@@ -16,6 +16,7 @@ module EE ...@@ -16,6 +16,7 @@ module EE
params[:search], params[:search],
elastic_projects, elastic_projects,
public_and_internal_projects: elastic_global, public_and_internal_projects: elastic_global,
sort: params[:sort],
filters: { confidential: params[:confidential], state: params[:state] } filters: { confidential: params[:confidential], state: params[:state] }
) )
end end
......
...@@ -118,6 +118,25 @@ module Elastic ...@@ -118,6 +118,25 @@ module Elastic
query_hash query_hash
end end
def apply_sort(query_hash, options)
case options[:sort]
when 'oldest'
query_hash.merge(sort: {
created_at: {
order: 'asc'
}
})
when 'newest'
query_hash.merge(sort: {
created_at: {
order: 'desc'
}
})
else
query_hash
end
end
# Builds an elasticsearch query that will select projects the user is # Builds an elasticsearch query that will select projects the user is
# granted access to. # granted access to.
# #
......
...@@ -24,6 +24,7 @@ module Elastic ...@@ -24,6 +24,7 @@ module Elastic
query_hash = project_ids_filter(query_hash, options) query_hash = project_ids_filter(query_hash, options)
query_hash = confidentiality_filter(query_hash, options) query_hash = confidentiality_filter(query_hash, options)
query_hash = state_filter(query_hash, options) query_hash = state_filter(query_hash, options)
query_hash = apply_sort(query_hash, options)
search(query_hash, options) search(query_hash, options)
end end
......
...@@ -23,6 +23,7 @@ module Elastic ...@@ -23,6 +23,7 @@ module Elastic
options[:features] = 'merge_requests' options[:features] = 'merge_requests'
query_hash = project_ids_filter(query_hash, options) query_hash = project_ids_filter(query_hash, options)
query_hash = state_filter(query_hash, options) query_hash = state_filter(query_hash, options)
query_hash = apply_sort(query_hash, options)
search(query_hash, options) search(query_hash, options)
end end
......
...@@ -7,17 +7,18 @@ module Gitlab ...@@ -7,17 +7,18 @@ module Gitlab
DEFAULT_PER_PAGE = Gitlab::SearchResults::DEFAULT_PER_PAGE DEFAULT_PER_PAGE = Gitlab::SearchResults::DEFAULT_PER_PAGE
attr_reader :current_user, :query, :public_and_internal_projects, :filters attr_reader :current_user, :query, :public_and_internal_projects, :sort, :filters
# Limit search results by passed projects # Limit search results by passed projects
# It allows us to search only for projects user has access to # It allows us to search only for projects user has access to
attr_reader :limit_project_ids attr_reader :limit_project_ids
def initialize(current_user, query, limit_project_ids = nil, public_and_internal_projects: true, filters: {}) def initialize(current_user, query, limit_project_ids = nil, public_and_internal_projects: true, sort: nil, filters: {})
@current_user = current_user @current_user = current_user
@query = query @query = query
@limit_project_ids = limit_project_ids @limit_project_ids = limit_project_ids
@public_and_internal_projects = public_and_internal_projects @public_and_internal_projects = public_and_internal_projects
@sort = sort
@filters = filters @filters = filters
end end
...@@ -170,7 +171,7 @@ module Gitlab ...@@ -170,7 +171,7 @@ module Gitlab
relation = relation.public_send(preload_method) if preload_method # rubocop:disable GitlabSecurity/PublicSend relation = relation.public_send(preload_method) if preload_method # rubocop:disable GitlabSecurity/PublicSend
Kaminari.paginate_array( Kaminari.paginate_array(
relation, relation.to_a,
total_count: paginated_base.total_count, total_count: paginated_base.total_count,
limit: per_page, limit: per_page,
offset: per_page * (page - 1) offset: per_page * (page - 1)
...@@ -181,7 +182,8 @@ module Gitlab ...@@ -181,7 +182,8 @@ module Gitlab
{ {
current_user: current_user, current_user: current_user,
project_ids: limit_project_ids, project_ids: limit_project_ids,
public_and_internal_projects: public_and_internal_projects public_and_internal_projects: public_and_internal_projects,
sort: sort
} }
end end
......
...@@ -158,6 +158,8 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need ...@@ -158,6 +158,8 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
end end
describe 'issues' do describe 'issues' do
let(:scope) { 'issues' }
before do before do
@issue_1 = create( @issue_1 = create(
:issue, :issue,
...@@ -228,7 +230,6 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need ...@@ -228,7 +230,6 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
let!(:opened_result) { create(:issue, :opened, project: project, title: 'foo opened') } let!(:opened_result) { create(:issue, :opened, project: project, title: 'foo opened') }
let!(:confidential_result) { create(:issue, :confidential, project: project, title: 'foo confidential') } let!(:confidential_result) { create(:issue, :confidential, project: project, title: 'foo confidential') }
let(:scope) { 'issues' }
let(:results) { described_class.new(user, 'foo', [project.id], filters: filters) } let(:results) { described_class.new(user, 'foo', [project.id], filters: filters) }
before do before do
...@@ -240,6 +241,22 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need ...@@ -240,6 +241,22 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
include_examples 'search results filtered by state' include_examples 'search results filtered by state'
include_examples 'search results filtered by confidential' include_examples 'search results filtered by confidential'
end end
context 'ordering' do
let(:query) { 'sorted' }
let!(:project) { create(:project, :public) }
let!(:old_result) { create(:issue, project: project, title: 'sorted old', created_at: 1.month.ago) }
let!(:new_result) { create(:issue, project: project, title: 'sorted recent', created_at: 1.day.ago) }
let!(:very_old_result) { create(:issue, project: project, title: 'sorted very old', created_at: 1.year.ago) }
let(:results) { described_class.new(user, query, [project.id], sort: sort) }
before do
ensure_elasticsearch_index!
end
include_examples 'search results sorted'
end
end end
describe 'notes' do describe 'notes' do
...@@ -481,6 +498,8 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need ...@@ -481,6 +498,8 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
end end
describe 'merge requests' do describe 'merge requests' do
let(:scope) { 'merge_requests' }
before do before do
@merge_request_1 = create( @merge_request_1 = create(
:merge_request, :merge_request,
...@@ -554,7 +573,6 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need ...@@ -554,7 +573,6 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
let!(:opened_result) { create(:merge_request, :opened, source_project: project, title: 'foo opened') } let!(:opened_result) { create(:merge_request, :opened, source_project: project, title: 'foo opened') }
let!(:closed_result) { create(:merge_request, :closed, source_project: project, title: 'foo closed') } let!(:closed_result) { create(:merge_request, :closed, source_project: project, title: 'foo closed') }
let(:scope) { 'merge_requests' }
let(:results) { described_class.new(user, 'foo', [project.id], filters: filters) } let(:results) { described_class.new(user, 'foo', [project.id], filters: filters) }
include_examples 'search results filtered by state' do include_examples 'search results filtered by state' do
...@@ -563,6 +581,22 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need ...@@ -563,6 +581,22 @@ RSpec.describe Gitlab::Elastic::SearchResults, :elastic, :sidekiq_might_not_need
end end
end end
end end
context 'ordering' do
let(:query) { 'sorted' }
let!(:project) { create(:project, :public) }
let!(:old_result) { create(:merge_request, :opened, source_project: project, source_branch: 'old-1', title: 'sorted old', created_at: 1.month.ago) }
let!(:new_result) { create(:merge_request, :opened, source_project: project, source_branch: 'new-1', title: 'sorted recent', created_at: 1.day.ago) }
let!(:very_old_result) { create(:merge_request, :opened, source_project: project, source_branch: 'very-old-1', title: 'sorted very old', created_at: 1.year.ago) }
let(:results) { described_class.new(user, query, [project.id], sort: sort) }
before do
ensure_elasticsearch_index!
end
include_examples 'search results sorted'
end
end end
describe 'project scoping' do describe 'project scoping' do
......
...@@ -7,7 +7,7 @@ module Gitlab ...@@ -7,7 +7,7 @@ module Gitlab
DEFAULT_PAGE = 1 DEFAULT_PAGE = 1
DEFAULT_PER_PAGE = 20 DEFAULT_PER_PAGE = 20
attr_reader :current_user, :query, :filters attr_reader :current_user, :query, :sort, :filters
# Limit search results by passed projects # Limit search results by passed projects
# It allows us to search only for projects user has access to # It allows us to search only for projects user has access to
...@@ -19,11 +19,12 @@ module Gitlab ...@@ -19,11 +19,12 @@ module Gitlab
# query # query
attr_reader :default_project_filter attr_reader :default_project_filter
def initialize(current_user, query, limit_projects = nil, default_project_filter: false, filters: {}) def initialize(current_user, query, limit_projects = nil, sort: nil, default_project_filter: false, filters: {})
@current_user = current_user @current_user = current_user
@query = query @query = query
@limit_projects = limit_projects || Project.all @limit_projects = limit_projects || Project.all
@default_project_filter = default_project_filter @default_project_filter = default_project_filter
@sort = sort
@filters = filters @filters = filters
end end
...@@ -124,6 +125,19 @@ module Gitlab ...@@ -124,6 +125,19 @@ module Gitlab
end end
end end
# rubocop: disable CodeReuse/ActiveRecord
def apply_sort(scope)
case sort
when 'oldest'
scope.reorder('created_at ASC')
when 'newest'
scope.reorder('created_at DESC')
else
scope
end
end
# rubocop: enable CodeReuse/ActiveRecord
def projects def projects
limit_projects.search(query) limit_projects.search(query)
end end
...@@ -135,7 +149,7 @@ module Gitlab ...@@ -135,7 +149,7 @@ module Gitlab
issues = issues.where(project_id: project_ids_relation) # rubocop: disable CodeReuse/ActiveRecord issues = issues.where(project_id: project_ids_relation) # rubocop: disable CodeReuse/ActiveRecord
end end
issues apply_sort(issues)
end end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
...@@ -155,7 +169,7 @@ module Gitlab ...@@ -155,7 +169,7 @@ module Gitlab
merge_requests = merge_requests.in_projects(project_ids_relation) merge_requests = merge_requests.in_projects(project_ids_relation)
end end
merge_requests apply_sort(merge_requests)
end end
def default_scope def default_scope
......
...@@ -11,9 +11,11 @@ RSpec.describe Gitlab::SearchResults do ...@@ -11,9 +11,11 @@ RSpec.describe Gitlab::SearchResults do
let_it_be(:issue) { create(:issue, project: project, title: 'foo') } let_it_be(:issue) { create(:issue, project: project, title: 'foo') }
let_it_be(:milestone) { create(:milestone, project: project, title: 'foo') } let_it_be(:milestone) { create(:milestone, project: project, title: 'foo') }
let(:merge_request) { create(:merge_request, source_project: project, title: 'foo') } let(:merge_request) { create(:merge_request, source_project: project, title: 'foo') }
let(:query) { 'foo' }
let(:filters) { {} } let(:filters) { {} }
let(:sort) { nil }
subject(:results) { described_class.new(user, 'foo', Project.order(:id), filters: filters) } subject(:results) { described_class.new(user, query, Project.order(:id), sort: sort, filters: filters) }
context 'as a user with access' do context 'as a user with access' do
before do before do
...@@ -137,10 +139,12 @@ RSpec.describe Gitlab::SearchResults do ...@@ -137,10 +139,12 @@ RSpec.describe Gitlab::SearchResults do
end end
describe '#merge_requests' do describe '#merge_requests' do
let(:scope) { 'merge_requests' }
it 'includes project filter by default' do it 'includes project filter by default' do
expect(results).to receive(:project_ids_relation).and_call_original expect(results).to receive(:project_ids_relation).and_call_original
results.objects('merge_requests') results.objects(scope)
end end
it 'skips project filter if default project context is used' do it 'skips project filter if default project context is used' do
...@@ -148,24 +152,34 @@ RSpec.describe Gitlab::SearchResults do ...@@ -148,24 +152,34 @@ RSpec.describe Gitlab::SearchResults do
expect(results).not_to receive(:project_ids_relation) expect(results).not_to receive(:project_ids_relation)
results.objects('merge_requests') results.objects(scope)
end end
context 'filtering' do context 'filtering' do
let!(:opened_result) { create(:merge_request, :opened, source_project: project, title: 'foo opened') } let!(:opened_result) { create(:merge_request, :opened, source_project: project, title: 'foo opened') }
let!(:closed_result) { create(:merge_request, :closed, source_project: project, title: 'foo closed') } let!(:closed_result) { create(:merge_request, :closed, source_project: project, title: 'foo closed') }
let(:scope) { 'merge_requests' }
let(:query) { 'foo' } let(:query) { 'foo' }
include_examples 'search results filtered by state' include_examples 'search results filtered by state'
end end
context 'ordering' do
let(:query) { 'sorted' }
let!(:old_result) { create(:merge_request, :opened, source_project: project, source_branch: 'old-1', title: 'sorted old', created_at: 1.month.ago) }
let!(:new_result) { create(:merge_request, :opened, source_project: project, source_branch: 'new-1', title: 'sorted recent', created_at: 1.day.ago) }
let!(:very_old_result) { create(:merge_request, :opened, source_project: project, source_branch: 'very-old-1', title: 'sorted very old', created_at: 1.year.ago) }
include_examples 'search results sorted'
end
end end
describe '#issues' do describe '#issues' do
let(:scope) { 'issues' }
it 'includes project filter by default' do it 'includes project filter by default' do
expect(results).to receive(:project_ids_relation).and_call_original expect(results).to receive(:project_ids_relation).and_call_original
results.objects('issues') results.objects(scope)
end end
it 'skips project filter if default project context is used' do it 'skips project filter if default project context is used' do
...@@ -173,12 +187,10 @@ RSpec.describe Gitlab::SearchResults do ...@@ -173,12 +187,10 @@ RSpec.describe Gitlab::SearchResults do
expect(results).not_to receive(:project_ids_relation) expect(results).not_to receive(:project_ids_relation)
results.objects('issues') results.objects(scope)
end end
context 'filtering' do context 'filtering' do
let(:scope) { 'issues' }
let_it_be(:closed_result) { create(:issue, :closed, project: project, title: 'foo closed') } let_it_be(:closed_result) { create(:issue, :closed, project: project, title: 'foo closed') }
let_it_be(:opened_result) { create(:issue, :opened, project: project, title: 'foo open') } let_it_be(:opened_result) { create(:issue, :opened, project: project, title: 'foo open') }
let_it_be(:confidential_result) { create(:issue, :confidential, project: project, title: 'foo confidential') } let_it_be(:confidential_result) { create(:issue, :confidential, project: project, title: 'foo confidential') }
...@@ -186,6 +198,15 @@ RSpec.describe Gitlab::SearchResults do ...@@ -186,6 +198,15 @@ RSpec.describe Gitlab::SearchResults do
include_examples 'search results filtered by state' include_examples 'search results filtered by state'
include_examples 'search results filtered by confidential' include_examples 'search results filtered by confidential'
end end
context 'ordering' do
let(:query) { 'sorted' }
let!(:old_result) { create(:issue, project: project, title: 'sorted old', created_at: 1.month.ago) }
let!(:new_result) { create(:issue, project: project, title: 'sorted recent', created_at: 1.day.ago) }
let!(:very_old_result) { create(:issue, project: project, title: 'sorted very old', created_at: 1.year.ago) }
include_examples 'search results sorted'
end
end end
describe '#users' do describe '#users' do
......
# frozen_string_literal: true
RSpec.shared_examples 'search results sorted' do
context 'sort: newest' do
let(:sort) { 'newest' }
it 'sorts results by created_at' do
expect(results.objects(scope).map(&:id)).to eq([new_result.id, old_result.id, very_old_result.id])
end
end
context 'sort: oldest' do
let(:sort) { 'oldest' }
it 'sorts results by created_at' do
expect(results.objects(scope).map(&:id)).to eq([very_old_result.id, old_result.id, new_result.id])
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