Commit 32db15b2 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch '28810-projectfinder-should-handle-more-options' into 'master'

ProjectsFinder should handle more options

Closes #28810

See merge request !9682
parents d7e3f232 b996a82f
......@@ -3,6 +3,7 @@ class Admin::ProjectsController < Admin::ApplicationController
before_action :group, only: [:show, :transfer]
def index
params[:sort] ||= 'latest_activity_desc'
@projects = Project.with_statistics
@projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present?
@projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
......
# == FilterProjects
#
# Controller concern to handle projects filtering
# * by name
# * by archived state
#
module FilterProjects
extend ActiveSupport::Concern
def filter_projects(projects)
projects = projects.search(params[:name]) if params[:name].present?
projects = projects.non_archived if params[:archived].blank?
projects = projects.personal(current_user) if params[:personal].present? && current_user
projects
end
end
module ParamsBackwardCompatibility
private
def set_non_archived_param
params[:non_archived] = params[:archived].blank?
end
end
class Dashboard::ProjectsController < Dashboard::ApplicationController
include FilterProjects
include ParamsBackwardCompatibility
before_action :set_non_archived_param
before_action :default_sorting
def index
@projects = load_projects(current_user.authorized_projects)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page])
@projects = load_projects(params.merge(non_public: true)).page(params[:page])
respond_to do |format|
format.html { @last_push = current_user.recent_push }
......@@ -21,10 +22,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
end
def starred
@projects = load_projects(current_user.viewable_starred_projects)
@projects = @projects.includes(:forked_from_project, :tags)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page])
@projects = load_projects(params.merge(starred: true)).
includes(:forked_from_project, :tags).page(params[:page])
@last_push = current_user.recent_push
@groups = []
......@@ -41,14 +40,18 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
private
def load_projects(base_scope)
projects = base_scope.sorted_by_activity.includes(:route, namespace: :route)
def default_sorting
params[:sort] ||= 'latest_activity_desc'
@sort = params[:sort]
end
filter_projects(projects)
def load_projects(finder_params)
ProjectsFinder.new(params: finder_params, current_user: current_user).
execute.includes(:route, namespace: :route)
end
def load_events
@events = Event.in_projects(load_projects(current_user.authorized_projects))
@events = Event.in_projects(load_projects(params.merge(non_public: true)))
@events = event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0)
end
......
class Explore::ProjectsController < Explore::ApplicationController
include FilterProjects
include ParamsBackwardCompatibility
before_action :set_non_archived_param
def index
@projects = load_projects
@tags = @projects.tags_on(:tags)
@projects = @projects.tagged_with(params[:tag]) if params[:tag].present?
@projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
@projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.includes(:namespace).page(params[:page])
params[:sort] ||= 'latest_activity_desc'
@sort = params[:sort]
@projects = load_projects.page(params[:page])
respond_to do |format|
format.html
......@@ -21,10 +19,9 @@ class Explore::ProjectsController < Explore::ApplicationController
end
def trending
@projects = load_projects(Project.trending)
@projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page])
params[:trending] = true
@sort = params[:sort]
@projects = load_projects.page(params[:page])
respond_to do |format|
format.html
......@@ -37,10 +34,7 @@ class Explore::ProjectsController < Explore::ApplicationController
end
def starred
@projects = load_projects
@projects = filter_projects(@projects)
@projects = @projects.reorder('star_count DESC')
@projects = @projects.page(params[:page])
@projects = load_projects.reorder('star_count DESC').page(params[:page])
respond_to do |format|
format.html
......@@ -52,10 +46,10 @@ class Explore::ProjectsController < Explore::ApplicationController
end
end
protected
private
def load_projects(base_scope = nil)
base_scope ||= ProjectsFinder.new.execute(current_user)
base_scope.includes(:route, namespace: :route)
def load_projects
ProjectsFinder.new(current_user: current_user, params: params).
execute.includes(:route, namespace: :route)
end
end
......@@ -27,7 +27,7 @@ class Groups::ApplicationController < ApplicationController
end
def group_projects
@projects ||= GroupProjectsFinder.new(group).execute(current_user)
@projects ||= GroupProjectsFinder.new(group: group, current_user: current_user).execute
end
def authorize_admin_group!
......
class GroupsController < Groups::ApplicationController
include FilterProjects
include IssuesAction
include MergeRequestsAction
include ParamsBackwardCompatibility
respond_to :html
......@@ -105,15 +105,16 @@ class GroupsController < Groups::ApplicationController
protected
def setup_projects
set_non_archived_param
params[:sort] ||= 'latest_activity_desc'
@sort = params[:sort]
options = {}
options[:only_owned] = true if params[:shared] == '0'
options[:only_shared] = true if params[:shared] == '1'
@projects = GroupProjectsFinder.new(group, options).execute(current_user)
@projects = GroupProjectsFinder.new(params: params, group: group, options: options, current_user: current_user).execute
@projects = @projects.includes(:namespace)
@projects = @projects.sorted_by_activity
@projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]) if params[:name].blank?
end
......
......@@ -9,7 +9,7 @@ class Projects::ForksController < Projects::ApplicationController
def index
base_query = project.forks.includes(:creator)
@forks = base_query.merge(ProjectsFinder.new.execute(current_user))
@forks = base_query.merge(ProjectsFinder.new(current_user: current_user).execute)
@total_forks_count = base_query.size
@private_forks_count = @total_forks_count - @forks.size
@public_forks_count = @total_forks_count - @private_forks_count
......
......@@ -140,6 +140,6 @@ class UsersController < ApplicationController
end
def projects_for_current_user
ProjectsFinder.new.execute(current_user)
ProjectsFinder.new(current_user: current_user).execute
end
end
class GroupProjectsFinder < UnionFinder
def initialize(group, options = {})
# GroupProjectsFinder
#
# Used to filter Projects by set of params
#
# Arguments:
# current_user - which user use
# project_ids_relation: int[] - project ids to use
# group
# options:
# only_owned: boolean
# only_shared: boolean
# params:
# sort: string
# visibility_level: int
# tags: string[]
# personal: boolean
# search: string
# non_archived: boolean
#
class GroupProjectsFinder < ProjectsFinder
attr_reader :group, :options
def initialize(group:, params: {}, options: {}, current_user: nil, project_ids_relation: nil)
super(params: params, current_user: current_user, project_ids_relation: project_ids_relation)
@group = group
@options = options
end
def execute(current_user = nil)
segments = group_projects(current_user)
find_union(segments, Project)
end
private
def group_projects(current_user)
only_owned = @options.fetch(:only_owned, false)
only_shared = @options.fetch(:only_shared, false)
def init_collection
only_owned = options.fetch(:only_owned, false)
only_shared = options.fetch(:only_shared, false)
projects = []
if current_user
if @group.users.include?(current_user)
projects << @group.projects unless only_shared
projects << @group.shared_projects unless only_owned
if group.users.include?(current_user)
projects << group.projects unless only_shared
projects << group.shared_projects unless only_owned
else
unless only_shared
projects << @group.projects.visible_to_user(current_user)
projects << @group.projects.public_to_user(current_user)
projects << group.projects.visible_to_user(current_user)
projects << group.projects.public_to_user(current_user)
end
unless only_owned
projects << @group.shared_projects.visible_to_user(current_user)
projects << @group.shared_projects.public_to_user(current_user)
projects << group.shared_projects.visible_to_user(current_user)
projects << group.shared_projects.public_to_user(current_user)
end
end
else
projects << @group.projects.public_only unless only_shared
projects << @group.shared_projects.public_only unless only_owned
projects << group.projects.public_only unless only_shared
projects << group.shared_projects.public_only unless only_owned
end
projects
end
def union(items)
find_union(items, Project)
end
end
......@@ -116,9 +116,9 @@ class IssuableFinder
if current_user && params[:authorized_only].presence && !current_user_related?
current_user.authorized_projects
elsif group
GroupProjectsFinder.new(group).execute(current_user)
GroupProjectsFinder.new(group: group, current_user: current_user).execute
else
projects_finder.execute(current_user, item_project_ids(items))
ProjectsFinder.new(current_user: current_user, project_ids_relation: item_project_ids(items)).execute
end
@projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil)
......@@ -405,8 +405,4 @@ class IssuableFinder
def current_user_related?
params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me'
end
def projects_finder
@projects_finder ||= ProjectsFinder.new
end
end
......@@ -83,7 +83,7 @@ class LabelsFinder < UnionFinder
def projects
return @projects if defined?(@projects)
@projects = skip_authorization ? Project.all : ProjectsFinder.new.execute(current_user)
@projects = skip_authorization ? Project.all : ProjectsFinder.new(current_user: current_user).execute
@projects = @projects.in_namespace(params[:group_id]) if group?
@projects = @projects.where(id: params[:project_ids]) if projects?
@projects = @projects.reorder(nil)
......
# ProjectsFinder
#
# Used to filter Projects by set of params
#
# Arguments:
# current_user - which user use
# project_ids_relation: int[] - project ids to use
# params:
# trending: boolean
# non_public: boolean
# starred: boolean
# sort: string
# visibility_level: int
# tags: string[]
# personal: boolean
# search: string
# non_archived: boolean
#
class ProjectsFinder < UnionFinder
def execute(current_user = nil, project_ids_relation = nil)
segments = all_projects(current_user)
segments.map! { |s| s.where(id: project_ids_relation) } if project_ids_relation
attr_accessor :params
attr_reader :current_user, :project_ids_relation
find_union(segments, Project).with_route
def initialize(params: {}, current_user: nil, project_ids_relation: nil)
@params = params
@current_user = current_user
@project_ids_relation = project_ids_relation
end
def execute
items = init_collection
items = by_ids(items)
items = union(items)
items = by_personal(items)
items = by_visibilty_level(items)
items = by_tags(items)
items = by_search(items)
items = by_archived(items)
sort(items)
end
private
def all_projects(current_user)
def init_collection
projects = []
if params[:trending].present?
projects << Project.trending
elsif params[:starred].present? && current_user
projects << current_user.viewable_starred_projects
else
projects << current_user.authorized_projects if current_user
projects << Project.unscoped.public_to_user(current_user)
projects << Project.unscoped.public_to_user(current_user) unless params[:non_public].present?
end
projects
end
def by_ids(items)
project_ids_relation ? items.map { |item| item.where(id: project_ids_relation) } : items
end
def union(items)
find_union(items, Project).with_route
end
def by_personal(items)
(params[:personal].present? && current_user) ? items.personal(current_user) : items
end
def by_visibilty_level(items)
params[:visibility_level].present? ? items.where(visibility_level: params[:visibility_level]) : items
end
def by_tags(items)
params[:tag].present? ? items.tagged_with(params[:tag]) : items
end
def by_search(items)
params[:search] ||= params[:name]
params[:search].present? ? items.search(params[:search]) : items
end
def sort(items)
params[:sort].present? ? items.sort(params[:sort]) : items
end
def by_archived(projects)
# Back-compatibility with the places where `params[:archived]` can be set explicitly to `false`
params[:non_archived] = !Gitlab::Utils.to_boolean(params[:archived]) if params.key?(:archived)
params[:non_archived] ? projects.non_archived : projects
end
end
......@@ -95,7 +95,7 @@ class TodosFinder
def projects(items)
item_project_ids = items.reorder(nil).select(:project_id)
ProjectsFinder.new.execute(current_user, item_project_ids)
ProjectsFinder.new(current_user: current_user, project_ids_relation: item_project_ids).execute
end
def type?
......
......@@ -25,8 +25,8 @@ module SortingHelper
def projects_sort_options_hash
options = {
sort_value_name => sort_title_name,
sort_value_recently_updated => sort_title_recently_updated,
sort_value_oldest_updated => sort_title_oldest_updated,
sort_value_latest_activity => sort_title_latest_activity,
sort_value_oldest_activity => sort_title_oldest_activity,
sort_value_recently_created => sort_title_recently_created,
sort_value_oldest_created => sort_title_oldest_created
}
......@@ -78,6 +78,14 @@ module SortingHelper
'Last updated'
end
def sort_title_oldest_activity
'Oldest updated'
end
def sort_title_latest_activity
'Last updated'
end
def sort_title_oldest_created
'Oldest created'
end
......@@ -198,6 +206,14 @@ module SortingHelper
'updated_desc'
end
def sort_value_oldest_activity
'latest_activity_asc'
end
def sort_value_latest_activity
'latest_activity_desc'
end
def sort_value_oldest_created
'created_asc'
end
......
......@@ -349,10 +349,15 @@ class Project < ActiveRecord::Base
end
def sort(method)
if method == 'storage_size_desc'
case method.to_s
when 'storage_size_desc'
# storage_size is a joined column so we need to
# pass a string to avoid AR adding the table name
reorder('project_statistics.storage_size DESC, projects.id DESC')
when 'latest_activity_desc'
reorder(last_activity_at: :desc)
when 'latest_activity_asc'
reorder(last_activity_at: :asc)
else
order_by(method)
end
......
......@@ -12,7 +12,7 @@ class GroupPolicy < BasePolicy
can_read ||= globally_viewable
can_read ||= member
can_read ||= @user.admin?
can_read ||= GroupProjectsFinder.new(@subject).execute(@user).any?
can_read ||= GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any?
can! :read_group if can_read
# Only group masters and group owners can create new projects
......@@ -41,6 +41,6 @@ class GroupPolicy < BasePolicy
return true if @subject.internal? && !@user.external?
return true if @subject.users.include?(@user)
GroupProjectsFinder.new(@subject).execute(@user).any?
GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any?
end
end
......@@ -8,7 +8,7 @@ module Search
def execute
group = Group.find_by(id: params[:group_id]) if params[:group_id].present?
projects = ProjectsFinder.new.execute(current_user)
projects = ProjectsFinder.new(current_user: current_user).execute
if group
projects = projects.inside_path(group.full_path)
......
- publicish_project_count = ProjectsFinder.new.execute(current_user).count
- publicish_project_count = ProjectsFinder.new(current_user: current_user).execute.count
.blank-state.blank-state-welcome
%h2.blank-state-welcome-title
Welcome to GitLab
......
- @sort ||= sort_value_recently_updated
- @sort ||= sort_value_latest_activity
.dropdown
- toggle_text = projects_sort_options_hash[@sort]
= dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { id: 'sort-projects-dropdown' })
......
---
title: ProjectsFinder should handle more options
merge_request: 9682
author: Jacopo Beschi @jacopo-beschi
......@@ -142,7 +142,7 @@ module API
end
get ":id/projects" do
group = find_group!(params[:id])
projects = GroupProjectsFinder.new(group).execute(current_user)
projects = GroupProjectsFinder.new(group: group, current_user: current_user).execute
projects = filter_projects(projects)
entity = params[:simple] ? Entities::BasicProjectDetails : Entities::Project
present paginate(projects), with: entity, current_user: current_user
......
......@@ -84,7 +84,7 @@ module API
end
get do
entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails
present_projects ProjectsFinder.new.execute(current_user), with: entity, statistics: params[:statistics]
present_projects ProjectsFinder.new(current_user: current_user).execute, with: entity, statistics: params[:statistics]
end
desc 'Create new project' do
......
......@@ -341,7 +341,7 @@ module API
not_found!('User') unless user
events = user.events.
merge(ProjectsFinder.new.execute(current_user)).
merge(ProjectsFinder.new(current_user: current_user).execute).
references(:project).
with_associations.
recent
......
......@@ -151,7 +151,7 @@ module API
end
get ":id/projects" do
group = find_group!(params[:id])
projects = GroupProjectsFinder.new(group).execute(current_user)
projects = GroupProjectsFinder.new(group: group, current_user: current_user).execute
projects = filter_projects(projects)
entity = params[:simple] ? ::API::Entities::BasicProjectDetails : Entities::Project
present paginate(projects), with: entity, current_user: current_user
......
......@@ -107,7 +107,7 @@ module API
end
get '/visible' do
entity = current_user ? ::API::V3::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails
present_projects ProjectsFinder.new.execute(current_user), with: entity
present_projects ProjectsFinder.new(current_user: current_user).execute, with: entity
end
desc 'Get a projects list for authenticated user' do
......
......@@ -138,7 +138,7 @@ module API
not_found!('User') unless user
events = user.events.
merge(ProjectsFinder.new.execute(current_user)).
merge(ProjectsFinder.new(current_user: current_user).execute).
references(:project).
with_associations.
recent
......
......@@ -3,8 +3,9 @@ require 'spec_helper'
describe GroupProjectsFinder do
let(:group) { create(:group) }
let(:current_user) { create(:user) }
let(:options) { {} }
let(:finder) { described_class.new(source_user) }
let(:finder) { described_class.new(group: group, current_user: current_user, options: options) }
let!(:public_project) { create(:empty_project, :public, group: group, path: '1') }
let!(:private_project) { create(:empty_project, :private, group: group, path: '2') }
......@@ -18,22 +19,27 @@ describe GroupProjectsFinder do
shared_project_3.project_group_links.create(group_access: Gitlab::Access::MASTER, group: group)
end
subject { finder.execute }
describe 'with a group member current user' do
before { group.add_user(current_user, Gitlab::Access::MASTER) }
before do
group.add_master(current_user)
end
context "only shared" do
subject { described_class.new(group, only_shared: true).execute(current_user) }
it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1]) }
let(:options) { { only_shared: true } }
it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1]) }
end
context "only owned" do
subject { described_class.new(group, only_owned: true).execute(current_user) }
it { is_expected.to eq([private_project, public_project]) }
let(:options) { { only_owned: true } }
it { is_expected.to match_array([private_project, public_project]) }
end
context "all" do
subject { described_class.new(group).execute(current_user) }
it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1, private_project, public_project]) }
it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1, private_project, public_project]) }
end
end
......@@ -44,46 +50,56 @@ describe GroupProjectsFinder do
end
context "only shared" do
let(:options) { { only_shared: true } }
context "without external user" do
subject { described_class.new(group, only_shared: true).execute(current_user) }
it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1]) }
it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1]) }
end
context "with external user" do
before { current_user.update_attributes(external: true) }
subject { described_class.new(group, only_shared: true).execute(current_user) }
it { is_expected.to eq([shared_project_2, shared_project_1]) }
before do
current_user.update_attributes(external: true)
end
it { is_expected.to match_array([shared_project_2, shared_project_1]) }
end
end
context "only owned" do
let(:options) { { only_owned: true } }
context "without external user" do
before { private_project.team << [current_user, Gitlab::Access::MASTER] }
subject { described_class.new(group, only_owned: true).execute(current_user) }
it { is_expected.to eq([private_project, public_project]) }
before do
private_project.team << [current_user, Gitlab::Access::MASTER]
end
it { is_expected.to match_array([private_project, public_project]) }
end
context "with external user" do
before { current_user.update_attributes(external: true) }
subject { described_class.new(group, only_owned: true).execute(current_user) }
before do
current_user.update_attributes(external: true)
end
it { is_expected.to eq([public_project]) }
end
end
context "all" do
subject { described_class.new(group).execute(current_user) }
it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1, public_project]) }
end
it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1, public_project]) }
end
end
describe "no user" do
context "only shared" do
subject { described_class.new(group, only_shared: true).execute(current_user) }
it { is_expected.to eq([shared_project_3, shared_project_1]) }
let(:options) { { only_shared: true } }
it { is_expected.to match_array([shared_project_3, shared_project_1]) }
end
context "only owned" do
subject { described_class.new(group, only_owned: true).execute(current_user) }
let(:options) { { only_owned: true } }
it { is_expected.to eq([public_project]) }
end
end
......
......@@ -21,38 +21,144 @@ describe ProjectsFinder do
create(:empty_project, :private, name: 'D', path: 'D')
end
let(:finder) { described_class.new }
let(:params) { {} }
let(:current_user) { user }
let(:project_ids_relation) { nil }
let(:finder) { described_class.new(params: params, current_user: current_user, project_ids_relation: project_ids_relation) }
describe 'without a user' do
subject { finder.execute }
describe 'without a user' do
let(:current_user) { nil }
it { is_expected.to eq([public_project]) }
end
describe 'with a user' do
subject { finder.execute(user) }
describe 'without private projects' do
it { is_expected.to eq([public_project, internal_project]) }
it { is_expected.to match_array([public_project, internal_project]) }
end
describe 'with private projects' do
before do
private_project.add_user(user, Gitlab::Access::MASTER)
private_project.add_master(user)
end
it do
is_expected.to eq([public_project, internal_project, private_project])
end
it { is_expected.to match_array([public_project, internal_project, private_project]) }
end
end
describe 'with project_ids_relation' do
let(:project_ids_relation) { Project.where(id: internal_project.id) }
subject { finder.execute(user, project_ids_relation) }
it { is_expected.to eq([internal_project]) }
end
describe 'filter by visibility_level' do
before do
private_project.add_master(user)
end
context 'private' do
let(:params) { { visibility_level: Gitlab::VisibilityLevel::PRIVATE } }
it { is_expected.to eq([private_project]) }
end
context 'internal' do
let(:params) { { visibility_level: Gitlab::VisibilityLevel::INTERNAL } }
it { is_expected.to eq([internal_project]) }
end
context 'public' do
let(:params) { { visibility_level: Gitlab::VisibilityLevel::PUBLIC } }
it { is_expected.to eq([public_project]) }
end
end
describe 'filter by tags' do
before do
public_project.tag_list.add('foo')
public_project.save!
end
let(:params) { { tag: 'foo' } }
it { is_expected.to eq([public_project]) }
end
describe 'filter by personal' do
let!(:personal_project) { create(:empty_project, namespace: user.namespace) }
let(:params) { { personal: true } }
it { is_expected.to eq([personal_project]) }
end
describe 'filter by search' do
let(:params) { { search: 'C' } }
it { is_expected.to eq([public_project]) }
end
describe 'filter by name for backward compatibility' do
let(:params) { { name: 'C' } }
it { is_expected.to eq([public_project]) }
end
describe 'filter by archived' do
let!(:archived_project) { create(:empty_project, :public, :archived, name: 'E', path: 'E') }
context 'non_archived=true' do
let(:params) { { non_archived: true } }
it { is_expected.to match_array([public_project, internal_project]) }
end
context 'non_archived=false' do
let(:params) { { non_archived: false } }
it { is_expected.to match_array([public_project, internal_project, archived_project]) }
end
describe 'filter by archived for backward compatibility' do
let(:params) { { archived: false } }
it { is_expected.to match_array([public_project, internal_project]) }
end
end
describe 'filter by trending' do
let!(:trending_project) { create(:trending_project, project: public_project) }
let(:params) { { trending: true } }
it { is_expected.to eq([public_project]) }
end
describe 'filter by non_public' do
let(:params) { { non_public: true } }
before do
private_project.add_developer(current_user)
end
it { is_expected.to eq([private_project]) }
end
describe 'filter by viewable_starred_projects' do
let(:params) { { starred: true } }
before do
current_user.toggle_star(public_project)
end
it { is_expected.to eq([public_project]) }
end
describe 'sorting' do
let(:params) { { sort: 'name_asc' } }
it { is_expected.to eq([internal_project, public_project]) }
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