diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index b57da88054d..0e4521d03e9 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -112,7 +112,8 @@ def resource_class_join(association, scope) end def resource_scope_for_index(...) - scope = resource_scope(...).search_for(*search_options) + virtual_column_scope = virtual_column_scope(resource_scope(...)) + scope = virtual_column_scope || resource_scope(...).search_for(*search_options) return scope if paginate_options[:per_page] == 'all' scope.paginate(**paginate_options) end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index dab973f19f6..7d7701d5e6f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -232,7 +232,7 @@ def remote_user_provided? end def resource_base_with_search - resource_base.search_for(params[:search], :order => params[:order]) + virtual_column_scope(resource_base) || resource_base.search_for(params[:search], :order => params[:order]) end def resource_base_search_and_page(tables = []) @@ -393,6 +393,11 @@ def parameter_filter_context Foreman::ParameterFilter::Context.new(:ui, controller_name, params[:action]) end + def wrap_for_virt_column_select(base_scope) + scope = virtual_column_scope(base_scope) || base_scope.search_for(params[:search], :order => params[:order]) + scope.paginate(:page => params[:page], :per_page => params[:per_page]) + end + class << self def parameter_filter_context Foreman::ParameterFilter::Context.new(:ui, controller_name, nil) diff --git a/app/controllers/concerns/application_shared.rb b/app/controllers/concerns/application_shared.rb index 25538aa03d2..c1a11bb9610 100644 --- a/app/controllers/concerns/application_shared.rb +++ b/app/controllers/concerns/application_shared.rb @@ -144,4 +144,12 @@ def find_session_taxonomy(taxonomy, user) def find_selected_columns @selected_columns = User.current.table_preferences.find_by(name: controller_name)&.columns&.sort end + + def virtual_column_scope(base_scope) + virt_column = params[:order]&.split(' ')&.first + select_method = "select_#{virt_column}" + model = respond_to?(:model_of_controller) ? model_of_controller : resource_class + return if virt_column.blank? || model.columns_hash[virt_column] || !base_scope.virtual_column_scopes.include?(select_method.to_sym) + base_scope.public_send(select_method).search_for(params[:search]).reorder(params[:order]) + end end diff --git a/app/controllers/roles_controller.rb b/app/controllers/roles_controller.rb index 744cedea814..6fe7d96903e 100644 --- a/app/controllers/roles_controller.rb +++ b/app/controllers/roles_controller.rb @@ -22,7 +22,7 @@ class RolesController < ApplicationController def index params[:order] ||= 'name' - @roles = Role.authorized(:view_roles).search_for(params[:search], :order => params[:order]).paginate(:page => params[:page], :per_page => params[:per_page]) + @roles = resource_base_search_and_page end def new diff --git a/app/models/application_record.rb b/app/models/application_record.rb index d221630946b..04dac0920af 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -49,4 +49,14 @@ def id_and_type end alias_attribute :to_label, :name_method alias_attribute :to_s, :to_label + + def self.virtual_column_scope(name, proc) + name = name.to_sym + virtual_column_scopes << name + scope name, proc + end + + def self.virtual_column_scopes + @virtual_column_scopes ||= [] + end end diff --git a/app/models/role.rb b/app/models/role.rb index 842c751db3c..67ab3775ed0 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -39,6 +39,7 @@ class Role < ApplicationRecord where("#{compare} builtin = 0") } scope :cloned, -> { where.not(:cloned_from_id => nil) } + virtual_column_scope :select_locked, -> { select("roles.*,(origin IS NOT NULL AND builtin <> #{BUILTIN_DEFAULT_ROLE}) as locked") } validates_lengths_from_database before_destroy :check_deletable diff --git a/developer_docs/how_to_create_a_plugin.asciidoc b/developer_docs/how_to_create_a_plugin.asciidoc index 5ce63e147d2..6b0941ba343 100644 --- a/developer_docs/how_to_create_a_plugin.asciidoc +++ b/developer_docs/how_to_create_a_plugin.asciidoc @@ -1521,7 +1521,7 @@ module ForemanRemoteExecution # We need to make sure, there's a AR relation we'd be searching on, because the class name can't be determined by the # association name, it needs to be specified explicitly (as a string). Similarly for the foreign key. has_one :execution_status_object, :class_name => 'HostStatus::ExecutionStatus', :foreign_key => 'host_id' - + # Then we define the searching, the relation is the association name we define on a line above # :rename key indicates the term user will use to search hosts for a given state, the convention is $feature_status # :complete_value must be a hash with symbolized keys specifying all possible state values, otherwise the autocompletion @@ -2391,6 +2391,38 @@ Foreman Webhooks plugin ships with an example "Remote Execution Host Job" templa You can find all observable events by calling `Foreman::EventSubscribers.all_observable_events` in the Rails console. +=== How to order by a virtual column +_Requires Foreman 3.16 or higher, set `requires_foreman '>= 3.17'` in +engine.rb_ + +NOTE: The following assumes usage of: +* `resource_scope_for_index` in the API controller's index action. +* `resource_base_with_search` or `resource_base_search_and_page` in the UI controller's index action. + +Starting Foreman 3.16, both UI and API controllers can order by virtual columns. +This is achieved by using the `virtual_column_scope :select_, lambda {}` scope in the model. +The scope should return the virtual column in the select statement. + +[source, ruby] +.... +# app/models/model.rb +virtual_column_scope :select_virtual_column, lambda { + select("model_table.*,(column,column2) as virtual_column") +} +.... + +NOTE: In case your UI controller doesn't use `resource_base_with_search` or `resource_base_search_and_page`, +you can use the `wrap_for_virt_column_select` method in the controller to order by virtual columns. + +[source, ruby] +.... +# app/controllers/plugin/controller.rb +def index + # Model can be already scoped by other means + @objects = wrap_for_virt_column_select(Model) +end +.... + [[translating]] == Translating diff --git a/test/controllers/api/v2/roles_controller_test.rb b/test/controllers/api/v2/roles_controller_test.rb index 2f0ff1d84d2..a2b7eb680ea 100755 --- a/test/controllers/api/v2/roles_controller_test.rb +++ b/test/controllers/api/v2/roles_controller_test.rb @@ -12,6 +12,19 @@ class Api::V2::RolesControllerTest < ActionController::TestCase assert_equal Role.order(:name).pluck(:name), roles['results'].map { |r| r['name'] } end + test "should order index by locked" do + unlocked_role = FactoryBot.create(:role, :name => "unlocked role", :origin => '', :builtin => 0) + get :index, params: { :order => "locked DESC" } + assert_response :success + roles = ActiveSupport::JSON.decode(@response.body) + assert_equal unlocked_role.id, roles['results'].first['id'] + + get :index, params: { :order => "locked ASC" } + assert_response :success + roles = ActiveSupport::JSON.decode(@response.body) + assert_not_equal unlocked_role.id, roles['results'].first['id'] + end + test "should show individual record" do get :show, params: { :id => roles(:manager).to_param } assert_response :success diff --git a/test/controllers/roles_controller_test.rb b/test/controllers/roles_controller_test.rb index 3b3ef45714f..52e55f78eb8 100644 --- a/test/controllers/roles_controller_test.rb +++ b/test/controllers/roles_controller_test.rb @@ -61,6 +61,11 @@ class RolesControllerTest < ActionController::TestCase assert_not_nil Role.find_by_id(roles(:default_role).id) end + test "should order index by locked" do + get :index, params: { order: "locked DESC" }, session: set_session_user + assert_response :success + end + context "with taxonomies" do before do @permission1 = FactoryBot.create(:permission, :domain, :name => 'permission1')