Skip to content

Commit 56d9b7a

Browse files
committed
Fixes #38024 - Add ordering by virtual columns
1 parent 1bb2b00 commit 56d9b7a

File tree

7 files changed

+77
-1
lines changed

7 files changed

+77
-1
lines changed

app/controllers/api/v2/base_controller.rb

+14
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,20 @@ def render_error(error, options = { })
168168
render options.merge(:template => "api/v2/errors/#{error}",
169169
:layout => 'api/v2/layouts/error_layout')
170170
end
171+
172+
def resource_scope_for_index(...)
173+
virt_column = params[:order]&.split(' ')&.first
174+
select_method = "select_#{virt_column}"
175+
scope = resource_scope(...)
176+
177+
if virt_column.blank? || resource_class.columns_hash[virt_column] || !scope.respond_to?(select_method)
178+
super(...)
179+
else
180+
ordered_scope = scope.send(select_method).search_for(params[:search]).order(params[:order])
181+
return ordered_scope if paginate_options[:per_page] == 'all'
182+
ordered_scope.paginate(**paginate_options)
183+
end
184+
end
171185
end
172186
end
173187
end

app/controllers/application_controller.rb

+10
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,16 @@ def parameter_filter_context
397397
Foreman::ParameterFilter::Context.new(:ui, controller_name, params[:action])
398398
end
399399

400+
def wrap_for_virt_column_select(base_scope)
401+
virt_column = params[:order]&.split(' ')&.first
402+
select_method = "select_#{virt_column}"
403+
if virt_column.blank? || model_of_controller.columns_hash[virt_column] || !base_scope.respond_to?(select_method)
404+
base_scope.search_for(params[:search], :order => params[:order]).paginate(:page => params[:page], :per_page => params[:per_page])
405+
else
406+
base_scope.send(select_method).search_for(params[:search]).order(params[:order]).paginate(:page => params[:page], :per_page => params[:per_page])
407+
end
408+
end
409+
400410
class << self
401411
def parameter_filter_context
402412
Foreman::ParameterFilter::Context.new(:ui, controller_name, nil)

app/controllers/roles_controller.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class RolesController < ApplicationController
2222

2323
def index
2424
params[:order] ||= 'name'
25-
@roles = Role.authorized(:view_roles).search_for(params[:search], :order => params[:order]).paginate(:page => params[:page], :per_page => params[:per_page])
25+
@roles = wrap_for_virt_column_select(Role.authorized(:view_roles))
2626
end
2727

2828
def new

app/models/role.rb

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class Role < ApplicationRecord
3939
where("#{compare} builtin = 0")
4040
}
4141
scope :cloned, -> { where.not(:cloned_from_id => nil) }
42+
scope :select_locked, -> { select("roles.*,(origin IS NOT NULL AND builtin <> #{BUILTIN_DEFAULT_ROLE}) as locked") }
4243

4344
validates_lengths_from_database
4445
before_destroy :check_deletable

developer_docs/how_to_create_a_plugin.asciidoc

+33
Original file line numberDiff line numberDiff line change
@@ -2391,6 +2391,39 @@ Foreman Webhooks plugin ships with an example "Remote Execution Host Job" templa
23912391

23922392
You can find all observable events by calling `Foreman::EventSubscribers.all_observable_events` in the Rails console.
23932393

2394+
=== How to order by a virtual column
2395+
_Requires Foreman 3.14 or higher, set `requires_foreman '>= 3.14'` in
2396+
engine.rb_
2397+
2398+
Foreman provides a wrapper method in `application_controller.rb` to be able to order by virtual columns called
2399+
`wrap_for_virt_column_select`. It can be used in the `index` action of a controller, for example:
2400+
2401+
[source, ruby]
2402+
....
2403+
# app/controllers/plugin/controller.rb
2404+
def index
2405+
# Model can be already scoped by other means
2406+
@objects = wrap_for_virt_column_select(Model)
2407+
end
2408+
....
2409+
2410+
In order to use this method, the virtual column must be defined in the model by `scope` method from
2411+
`scoped_search` library, for example:
2412+
2413+
[source, ruby]
2414+
....
2415+
# app/models/model.rb
2416+
scope :select_virtual_column, lambda {
2417+
select("model_table.*,(column,column2) as virtual_column")
2418+
}
2419+
....
2420+
2421+
For real example see `app/models/role.rb` and `app/controllers/roles_controller.rb`.
2422+
2423+
NOTE: API controllers don't have `wrap_for_virt_column_select` available, since it's embedded in
2424+
`resource_scope_for_index`, so as long as you inherit from `Api::V2::BaseController` and use `resource_scope_for_index`
2425+
in `index` action, you should be all set. Although, `select_<virtual_column_name>` scope must be defined in the model.
2426+
23942427
[[translating]]
23952428
== Translating
23962429

test/controllers/api/v2/roles_controller_test.rb

+13
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,19 @@ class Api::V2::RolesControllerTest < ActionController::TestCase
1212
assert_equal Role.order(:name).pluck(:name), roles['results'].map { |r| r['name'] }
1313
end
1414

15+
test "should order index by locked" do
16+
unlocked_role = FactoryBot.create(:role, :name => "unlocked role", :origin => '', :builtin => 0)
17+
get :index, params: { :order => "locked DESC" }
18+
assert_response :success
19+
roles = ActiveSupport::JSON.decode(@response.body)
20+
assert_equal unlocked_role.id, roles['results'].first['id']
21+
22+
get :index, params: { :order => "locked ASC" }
23+
assert_response :success
24+
roles = ActiveSupport::JSON.decode(@response.body)
25+
assert_not_equal unlocked_role.id, roles['results'].first['id']
26+
end
27+
1528
test "should show individual record" do
1629
get :show, params: { :id => roles(:manager).to_param }
1730
assert_response :success

test/controllers/roles_controller_test.rb

+5
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ class RolesControllerTest < ActionController::TestCase
6161
assert_not_nil Role.find_by_id(roles(:default_role).id)
6262
end
6363

64+
test "should order index by locked" do
65+
get :index, params: { order: "locked DESC" }, session: set_session_user
66+
assert_response :success
67+
end
68+
6469
context "with taxonomies" do
6570
before do
6671
@permission1 = FactoryBot.create(:permission, :domain, :name => 'permission1')

0 commit comments

Comments
 (0)