From 53b89c9161e48c0f9b4ecd5f544398c9360ea50f Mon Sep 17 00:00:00 2001 From: Mitsuhiro Shibuya Date: Sun, 16 Jun 2024 16:54:14 +0900 Subject: [PATCH] ActiveRecord 7.1 composite primary keys support --- .rubocop.yml | 2 +- .rubocop_todo.yml | 2 +- app/helpers/rails_admin/form_builder.rb | 4 +- .../initializers/active_record_extensions.rb | 23 ---------- lib/rails_admin/abstract_model.rb | 17 ++++--- lib/rails_admin/adapters/active_record.rb | 36 ++++++++++++++- .../adapters/active_record/association.rb | 31 ++++++++++--- .../adapters/composite_primary_keys.rb | 40 ----------------- .../composite_primary_keys/association.rb | 45 ------------------- lib/rails_admin/config.rb | 5 +++ lib/rails_admin/config/fields/association.rb | 12 ++++- .../config/fields/collection_association.rb | 23 ++++++++-- .../config/fields/singular_association.rb | 10 ++++- lib/rails_admin/config/fields/types/all.rb | 1 - .../fields/types/belongs_to_association.rb | 19 +++++++- .../composite_keys_belongs_to_association.rb | 31 ------------- .../fields/types/has_one_association.rb | 4 +- lib/rails_admin/engine.rb | 5 +++ .../extensions/url_for_extension.rb | 15 +++++++ .../support/composite_keys_serializer.rb | 15 +++++++ spec/dummy_app/Gemfile | 1 - spec/dummy_app/app/active_record/fan.rb | 2 +- spec/dummy_app/app/active_record/fanship.rb | 11 +++-- .../app/active_record/favorite_player.rb | 12 +++-- .../dummy_app/app/active_record/nested_fan.rb | 2 +- .../active_record/nested_favorite_player.rb | 2 +- spec/integration/actions/bulk_delete_spec.rb | 4 +- spec/integration/actions/delete_spec.rb | 2 +- spec/integration/actions/edit_spec.rb | 31 ++++++++++++- spec/integration/actions/export_spec.rb | 2 +- spec/integration/actions/index_spec.rb | 23 +++++++++- spec/integration/actions/new_spec.rb | 2 +- spec/integration/actions/show_spec.rb | 2 +- .../fields/belongs_to_association_spec.rb | 14 ++++++ .../fields/has_many_association_spec.rb | 13 ++++++ .../adapters/active_record_spec.rb | 7 +-- spec/spec_helper.rb | 2 +- 37 files changed, 281 insertions(+), 191 deletions(-) delete mode 100644 lib/rails_admin/adapters/composite_primary_keys.rb delete mode 100644 lib/rails_admin/adapters/composite_primary_keys/association.rb delete mode 100644 lib/rails_admin/config/fields/types/composite_keys_belongs_to_association.rb create mode 100644 lib/rails_admin/extensions/url_for_extension.rb create mode 100644 lib/rails_admin/support/composite_keys_serializer.rb diff --git a/.rubocop.yml b/.rubocop.yml index e228e2cbf9..5c76364890 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -109,7 +109,7 @@ Metrics/BlockNesting: Metrics/ClassLength: CountComments: false - Max: 200 # TODO: Lower to 100 + Max: 201 # TODO: Lower to 100 Metrics/CyclomaticComplexity: Max: 15 # TODO: Lower to 6 diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 9f3108dd2c..447ccf2ad2 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -21,7 +21,7 @@ Lint/ReturnInVoidContext: # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. # IgnoredMethods: refine Metrics/BlockLength: - Max: 1119 + Max: 1135 # Offense count: 1 # Configuration parameters: Max, CountKeywordArgs. diff --git a/app/helpers/rails_admin/form_builder.rb b/app/helpers/rails_admin/form_builder.rb index 37cbb03e28..d52f9eb2cf 100644 --- a/app/helpers/rails_admin/form_builder.rb +++ b/app/helpers/rails_admin/form_builder.rb @@ -108,8 +108,8 @@ def dom_name(field) end def hidden_field(method, options = {}) - if method == :id - super method, {value: object.id.to_s} + if method == :id && object.id.is_a?(Array) + super method, {value: RailsAdmin.config.composite_keys_serializer.serialize(object.id)} else super end diff --git a/config/initializers/active_record_extensions.rb b/config/initializers/active_record_extensions.rb index a2c1a7f3c0..b34f31a91a 100644 --- a/config/initializers/active_record_extensions.rb +++ b/config/initializers/active_record_extensions.rb @@ -20,27 +20,4 @@ def safe_send(value) end end end - - if defined?(CompositePrimaryKeys) - # Apply patch until the fix is released: - # https://github.com/composite-primary-keys/composite_primary_keys/pull/572 - CompositePrimaryKeys::CompositeKeys.class_eval do - alias_method :to_param, :to_s - end - - CompositePrimaryKeys::CollectionAssociation.prepend(Module.new do - def ids_writer(ids) - if reflection.association_primary_key.is_a? Array - ids = CompositePrimaryKeys.normalize(Array(ids).reject(&:blank?), reflection.association_primary_key.size) - reflection.association_primary_key.each_with_index do |primary_key, i| - pk_type = klass.type_for_attribute(primary_key) - ids.each do |id| - id[i] = pk_type.cast(id[i]) if id.is_a? Array - end - end - end - super ids - end - end) - end end diff --git a/lib/rails_admin/abstract_model.rb b/lib/rails_admin/abstract_model.rb index 3e0d05c2e9..20884be487 100644 --- a/lib/rails_admin/abstract_model.rb +++ b/lib/rails_admin/abstract_model.rb @@ -105,17 +105,20 @@ def each_associated_children(object) end end + def format_id(id) + id + end + + def parse_id(id) + id + end + private def initialize_active_record @adapter = :active_record - if defined?(::CompositePrimaryKeys) - require 'rails_admin/adapters/composite_primary_keys' - extend Adapters::CompositePrimaryKeys - else - require 'rails_admin/adapters/active_record' - extend Adapters::ActiveRecord - end + require 'rails_admin/adapters/active_record' + extend Adapters::ActiveRecord end def initialize_mongoid diff --git a/lib/rails_admin/adapters/active_record.rb b/lib/rails_admin/adapters/active_record.rb index b8008af9f0..e179280e78 100644 --- a/lib/rails_admin/adapters/active_record.rb +++ b/lib/rails_admin/adapters/active_record.rb @@ -15,7 +15,7 @@ def new(params = {}) end def get(id, scope = scoped) - object = scope.where(primary_key => id).first + object = primary_key_scope(scope, id).first return unless object object.extend(ObjectExtension) @@ -115,10 +115,42 @@ def adapter_supports_joins? true end + def format_id(id) + if primary_key.is_a? Array + RailsAdmin.config.composite_keys_serializer.serialize(id) + else + id + end + end + + def parse_id(id) + if primary_key.is_a?(Array) + ids = RailsAdmin.config.composite_keys_serializer.deserialize(id) + primary_key.each_with_index do |key, i| + ids[i] = model.type_for_attribute(key).cast(ids[i]) + end + ids + else + id + end + end + private + def primary_key_scope(scope, id) + if primary_key.is_a? Array + scope.where(primary_key.zip(parse_id(id)).to_h) + else + scope.where(primary_key => id) + end + end + def bulk_scope(scope, options) - scope.where(primary_key => options[:bulk_ids]) + if primary_key.is_a? Array + options[:bulk_ids].map { |id| primary_key_scope(scope, id) }.reduce(&:or) + else + scope.where(primary_key => options[:bulk_ids]) + end end def sort_scope(scope, options) diff --git a/lib/rails_admin/adapters/active_record/association.rb b/lib/rails_admin/adapters/active_record/association.rb index 675c02ae7f..61f1767948 100644 --- a/lib/rails_admin/adapters/active_record/association.rb +++ b/lib/rails_admin/adapters/active_record/association.rb @@ -42,16 +42,29 @@ def klass def primary_key return nil if polymorphic? - case type - when :has_one - association.klass.primary_key + value = + case type + when :has_one + association.klass.primary_key + else + association.association_primary_key + end + + if value.is_a? Array + :id else - association.association_primary_key - end.try(:to_sym) + value.to_sym + end end def foreign_key - association.foreign_key.to_sym + if association.options[:query_constraints].present? + association.options[:query_constraints].map(&:to_sym) + elsif association.foreign_key.is_a?(Array) + association.foreign_key.map(&:to_sym) + else + association.foreign_key.to_sym + end end def foreign_key_nullable? @@ -75,7 +88,11 @@ def key_accessor when :has_one :"#{name}_id" else - foreign_key + if foreign_key.is_a?(Array) + :"#{name}_id" + else + foreign_key + end end end diff --git a/lib/rails_admin/adapters/composite_primary_keys.rb b/lib/rails_admin/adapters/composite_primary_keys.rb deleted file mode 100644 index 851ea25725..0000000000 --- a/lib/rails_admin/adapters/composite_primary_keys.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -require 'rails_admin/adapters/active_record' -require 'rails_admin/adapters/composite_primary_keys/association' - -module RailsAdmin - module Adapters - module CompositePrimaryKeys - include RailsAdmin::Adapters::ActiveRecord - - def get(id, scope = scoped) - begin - object = scope.find(id) - rescue ::ActiveRecord::RecordNotFound - return nil - end - - object.extend(RailsAdmin::Adapters::ActiveRecord::ObjectExtension) - end - - def associations - model.reflect_on_all_associations.collect do |association| - RailsAdmin::Adapters::CompositePrimaryKeys::Association.new(association, model) - end - end - - private - - def bulk_scope(scope, options) - if primary_key.is_a? Array - options[:bulk_ids].map do |id| - scope.where(primary_key.zip(::CompositePrimaryKeys::CompositeKeys.parse(id)).to_h) - end.reduce(&:or) - else - super - end - end - end - end -end diff --git a/lib/rails_admin/adapters/composite_primary_keys/association.rb b/lib/rails_admin/adapters/composite_primary_keys/association.rb deleted file mode 100644 index 73a7edfdfd..0000000000 --- a/lib/rails_admin/adapters/composite_primary_keys/association.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true - -module RailsAdmin - module Adapters - module CompositePrimaryKeys - class Association < RailsAdmin::Adapters::ActiveRecord::Association - def field_type - if type == :belongs_to && association.foreign_key.is_a?(Array) - :composite_keys_belongs_to_association - else - super - end - end - - def primary_key - return nil if polymorphic? - - value = association.association_primary_key - - if value.is_a? Array - :id - else - value.to_sym - end - end - - def foreign_key - if association.foreign_key.is_a? Array - association.foreign_key.map(&:to_sym) - else - super - end - end - - def key_accessor - if type == :belongs_to && foreign_key.is_a?(Array) - :"#{name}_id" - else - super - end - end - end - end - end -end diff --git a/lib/rails_admin/config.rb b/lib/rails_admin/config.rb index 4d9be5736f..af557c8ee9 100644 --- a/lib/rails_admin/config.rb +++ b/lib/rails_admin/config.rb @@ -2,6 +2,7 @@ require 'rails_admin/config/lazy_model' require 'rails_admin/config/sections/list' +require 'rails_admin/support/composite_keys_serializer' require 'active_support/core_ext/module/attribute_accessors' module RailsAdmin @@ -84,6 +85,9 @@ class << self # Set where RailsAdmin fetches JS/CSS from, defaults to :sprockets attr_writer :asset_source + # For customization of composite keys representation + attr_accessor :composite_keys_serializer + # Setup authentication to be run as a before filter # This is run inside the controller instance so you can setup any authentication you need to # @@ -329,6 +333,7 @@ def reset @navigation_static_links = {} @navigation_static_label = nil @asset_source = nil + @composite_keys_serializer = RailsAdmin::Support::CompositeKeysSerializer @parent_controller = '::ActionController::Base' @forgery_protection_settings = {with: :exception} RailsAdmin::Config::Actions.reset diff --git a/lib/rails_admin/config/fields/association.rb b/lib/rails_admin/config/fields/association.rb index fbde570c82..b5d8194cad 100644 --- a/lib/rails_admin/config/fields/association.rb +++ b/lib/rails_admin/config/fields/association.rb @@ -137,7 +137,7 @@ def value # Returns collection of all selectable records def collection(scope = nil) (scope || bindings[:controller].list_entries(associated_model_config, :index, associated_collection_scope, false)). - map { |o| [o.send(associated_object_label_method), o.send(associated_primary_key).to_s] } + map { |o| [o.send(associated_object_label_method), format_key(o.send(associated_primary_key)).to_s] } end # has many? @@ -152,6 +152,16 @@ def virtual? def associated_model_limit RailsAdmin.config.default_associated_collection_limit end + + private + + def format_key(key) + if key.is_a?(Array) + RailsAdmin.config.composite_keys_serializer.serialize(key) + else + key + end + end end end end diff --git a/lib/rails_admin/config/fields/collection_association.rb b/lib/rails_admin/config/fields/collection_association.rb index b6c91ffcef..fc08cf264c 100644 --- a/lib/rails_admin/config/fields/collection_association.rb +++ b/lib/rails_admin/config/fields/collection_association.rb @@ -23,7 +23,7 @@ def collection(scope = nil) i = 0 super.sort_by { |a| [selected.index(a[1]) || selected.size, i += 1] } else - value.map { |o| [o.send(associated_object_label_method), o.send(associated_primary_key)] } + value.map { |o| [o.send(associated_object_label_method), format_key(o.send(associated_primary_key))] } end end @@ -36,7 +36,24 @@ def multiple? end def selected_ids - value.map { |s| s.send(associated_primary_key).to_s } + value.map { |s| format_key(s.send(associated_primary_key)).to_s } + end + + def parse_input(params) + return unless associated_model_config.abstract_model.primary_key.is_a?(Array) + + if nested_form + params[method_name].each_value do |value| + value[:id] = associated_model_config.abstract_model.parse_id(value[:id]) + end + elsif params[method_name].is_a?(Array) + params[method_name] = params[method_name].map { |key| associated_model_config.abstract_model.parse_id(key) if key.present? }.compact + if params[method_name].empty? + # Workaround for Arel::Visitors::UnsupportedVisitError in #ids_writer, until https://github.com/rails/rails/pull/51116 is in place + params.delete(method_name) + params[name] = [] + end + end end def form_default_value @@ -51,7 +68,7 @@ def widget_options { xhr: !associated_collection_cache_all, 'edit-url': (inline_edit && bindings[:view].authorized?(:edit, associated_model_config.abstract_model) ? bindings[:view].edit_path(model_name: associated_model_config.abstract_model.to_param, id: '__ID__') : ''), - remote_source: bindings[:view].index_path(associated_model_config.abstract_model, source_object_id: bindings[:object].id, source_abstract_model: abstract_model.to_param, associated_collection: name, current_action: bindings[:view].current_action, compact: true), + remote_source: bindings[:view].index_path(associated_model_config.abstract_model, source_object_id: abstract_model.format_id(bindings[:object].id), source_abstract_model: abstract_model.to_param, associated_collection: name, current_action: bindings[:view].current_action, compact: true), scopeBy: dynamic_scope_relationships, sortable: !!orderable, removable: !!removable, diff --git a/lib/rails_admin/config/fields/singular_association.rb b/lib/rails_admin/config/fields/singular_association.rb index 644ea015c8..44e5654a17 100644 --- a/lib/rails_admin/config/fields/singular_association.rb +++ b/lib/rails_admin/config/fields/singular_association.rb @@ -34,6 +34,14 @@ def selected_id raise NoMethodError # abstract end + def parse_input(params) + return unless nested_form && params[method_name].try(:[], :id).present? + + ids = associated_model_config.abstract_model.parse_id(params[method_name][:id]) + ids = ids.to_composite_keys.to_s if ids.respond_to?(:to_composite_keys) + params[method_name][:id] = ids + end + def form_value form_default_value.nil? ? selected_id : form_default_value end @@ -41,7 +49,7 @@ def form_value def widget_options { xhr: !associated_collection_cache_all, - remote_source: bindings[:view].index_path(associated_model_config.abstract_model, source_object_id: bindings[:object].id, source_abstract_model: abstract_model.to_param, associated_collection: name, current_action: bindings[:view].current_action, compact: true), + remote_source: bindings[:view].index_path(associated_model_config.abstract_model, source_object_id: abstract_model.format_id(bindings[:object].id), source_abstract_model: abstract_model.to_param, associated_collection: name, current_action: bindings[:view].current_action, compact: true), scopeBy: dynamic_scope_relationships, } end diff --git a/lib/rails_admin/config/fields/types/all.rb b/lib/rails_admin/config/fields/types/all.rb index b9c1aebdad..0d66d3829a 100644 --- a/lib/rails_admin/config/fields/types/all.rb +++ b/lib/rails_admin/config/fields/types/all.rb @@ -6,7 +6,6 @@ require 'rails_admin/config/fields/types/belongs_to_association' require 'rails_admin/config/fields/types/boolean' require 'rails_admin/config/fields/types/bson_object_id' -require 'rails_admin/config/fields/types/composite_keys_belongs_to_association' require 'rails_admin/config/fields/types/date' require 'rails_admin/config/fields/types/datetime' require 'rails_admin/config/fields/types/decimal' diff --git a/lib/rails_admin/config/fields/types/belongs_to_association.rb b/lib/rails_admin/config/fields/types/belongs_to_association.rb index 4c8f255151..d74034542d 100644 --- a/lib/rails_admin/config/fields/types/belongs_to_association.rb +++ b/lib/rails_admin/config/fields/types/belongs_to_association.rb @@ -21,8 +21,25 @@ class BelongsToAssociation < RailsAdmin::Config::Fields::SingularAssociation true end + register_instance_option :allowed_methods do + nested_form ? [method_name] : Array(association.foreign_key) + end + def selected_id - bindings[:object].safe_send(association.key_accessor) + if association.foreign_key.is_a?(Array) + format_key(association.foreign_key.map { |attribute| bindings[:object].safe_send(attribute) }) + else + bindings[:object].safe_send(association.key_accessor) + end + end + + def parse_input(params) + return super if nested_form + return unless params[method_name].present? && association.foreign_key.is_a?(Array) + + association.foreign_key.zip(RailsAdmin.config.composite_keys_serializer.deserialize(params.delete(method_name))).each do |key, value| + params[key] = value + end end end end diff --git a/lib/rails_admin/config/fields/types/composite_keys_belongs_to_association.rb b/lib/rails_admin/config/fields/types/composite_keys_belongs_to_association.rb deleted file mode 100644 index 1c1f85c134..0000000000 --- a/lib/rails_admin/config/fields/types/composite_keys_belongs_to_association.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -require 'rails_admin/config/fields/types/belongs_to_association' - -module RailsAdmin - module Config - module Fields - module Types - class CompositeKeysBelongsToAssociation < RailsAdmin::Config::Fields::Types::BelongsToAssociation - RailsAdmin::Config::Fields::Types.register(self) - - register_instance_option :allowed_methods do - nested_form ? [method_name] : Array(association.foreign_key) - end - - def selected_id - association.foreign_key.map { |attribute| bindings[:object].safe_send(attribute) }.to_composite_keys.to_s - end - - def parse_input(params) - return unless params[method_name].present? && association.foreign_key.is_a?(Array) && !nested_form - - association.foreign_key.zip(CompositePrimaryKeys::CompositeKeys.parse(params.delete(method_name))).each do |key, value| - params[key] = value - end - end - end - end - end - end -end diff --git a/lib/rails_admin/config/fields/types/has_one_association.rb b/lib/rails_admin/config/fields/types/has_one_association.rb index ca91ed8396..ba1b48999b 100644 --- a/lib/rails_admin/config/fields/types/has_one_association.rb +++ b/lib/rails_admin/config/fields/types/has_one_association.rb @@ -19,14 +19,14 @@ def associated_prepopulate_params end def parse_input(params) - return if nested_form + return super if nested_form id = params.delete(method_name) params[name] = associated_model_config.abstract_model.get(id) if id end def selected_id - value.try(:id).try(:to_s) + format_key(value.try(:id)).try(:to_s) end end end diff --git a/lib/rails_admin/engine.rb b/lib/rails_admin/engine.rb index f61a0c2ecf..3995e4f51d 100644 --- a/lib/rails_admin/engine.rb +++ b/lib/rails_admin/engine.rb @@ -4,6 +4,7 @@ require 'nested_form' require 'rails' require 'rails_admin' +require 'rails_admin/extensions/url_for_extension' require 'rails_admin/version' require 'turbo-rails' @@ -15,6 +16,10 @@ class Engine < Rails::Engine config.action_dispatch.rescue_responses['RailsAdmin::ActionNotAllowed'] = :forbidden + initializer 'RailsAdmin load UrlForExtension' do + RailsAdmin::Engine.routes.singleton_class.prepend(RailsAdmin::Extensions::UrlForExtension) + end + initializer 'RailsAdmin reload config in development' do |app| config.initializer_path = app.root.join('config/initializers/rails_admin.rb') diff --git a/lib/rails_admin/extensions/url_for_extension.rb b/lib/rails_admin/extensions/url_for_extension.rb new file mode 100644 index 0000000000..8328b2dda1 --- /dev/null +++ b/lib/rails_admin/extensions/url_for_extension.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module RailsAdmin + module Extensions + module UrlForExtension + def url_for(options, *args) + case options[:id] + when Array + options[:id] = RailsAdmin.config.composite_keys_serializer.serialize(options[:id]) + end + super options, *args + end + end + end +end diff --git a/lib/rails_admin/support/composite_keys_serializer.rb b/lib/rails_admin/support/composite_keys_serializer.rb new file mode 100644 index 0000000000..3d79e0eb9e --- /dev/null +++ b/lib/rails_admin/support/composite_keys_serializer.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module RailsAdmin + module Support + module CompositeKeysSerializer + def self.serialize(keys) + keys.map { |key| key&.to_s&.gsub('_', '__') }.join('_') + end + + def self.deserialize(string) + string.split('_').map { |key| key&.gsub('__', '_') } + end + end + end +end diff --git a/spec/dummy_app/Gemfile b/spec/dummy_app/Gemfile index a676df3ec7..5a16903ea3 100644 --- a/spec/dummy_app/Gemfile +++ b/spec/dummy_app/Gemfile @@ -17,7 +17,6 @@ group :active_record do gem 'sqlite3', '~> 1.3' end - gem 'composite_primary_keys' gem 'paper_trail', '>= 12.0' end diff --git a/spec/dummy_app/app/active_record/fan.rb b/spec/dummy_app/app/active_record/fan.rb index 106d0ae341..bd6bbf0959 100644 --- a/spec/dummy_app/app/active_record/fan.rb +++ b/spec/dummy_app/app/active_record/fan.rb @@ -3,7 +3,7 @@ class Fan < ActiveRecord::Base has_and_belongs_to_many :teams - if defined?(CompositePrimaryKeys) + if ActiveRecord.gem_version >= Gem::Version.new('7.1') || defined?(CompositePrimaryKeys) has_many :fanships, inverse_of: :fan has_one :fanship, inverse_of: :fan end diff --git a/spec/dummy_app/app/active_record/fanship.rb b/spec/dummy_app/app/active_record/fanship.rb index 5b97bcba19..a23afdc403 100644 --- a/spec/dummy_app/app/active_record/fanship.rb +++ b/spec/dummy_app/app/active_record/fanship.rb @@ -1,12 +1,17 @@ # frozen_string_literal: true -if defined?(CompositePrimaryKeys) +if ActiveRecord.gem_version >= Gem::Version.new('7.1') || defined?(CompositePrimaryKeys) class Fanship < ActiveRecord::Base self.table_name = :fans_teams - self.primary_keys = :fan_id, :team_id + if defined?(CompositePrimaryKeys) + self.primary_keys = :fan_id, :team_id + has_many :favorite_players, foreign_key: %i[fan_id team_id], inverse_of: :fanship + else + self.primary_key = :fan_id, :team_id + has_many :favorite_players, query_constraints: %i[fan_id team_id], inverse_of: :fanship + end belongs_to :fan, inverse_of: :fanships, optional: true belongs_to :team, optional: true - has_many :favorite_players, foreign_key: %i[fan_id team_id], inverse_of: :fanship end else class Fanship; end diff --git a/spec/dummy_app/app/active_record/favorite_player.rb b/spec/dummy_app/app/active_record/favorite_player.rb index de222659d3..455c189c23 100644 --- a/spec/dummy_app/app/active_record/favorite_player.rb +++ b/spec/dummy_app/app/active_record/favorite_player.rb @@ -1,9 +1,15 @@ # frozen_string_literal: true -if defined?(CompositePrimaryKeys) +if ActiveRecord.gem_version >= Gem::Version.new('7.1') || defined?(CompositePrimaryKeys) class FavoritePlayer < ActiveRecord::Base - self.primary_keys = :fan_id, :team_id, :player_id - belongs_to :fanship, foreign_key: %i[fan_id team_id], inverse_of: :favorite_players + if defined?(CompositePrimaryKeys) + self.primary_keys = :fan_id, :team_id, :player_id + belongs_to :fanship, foreign_key: %i[fan_id team_id], inverse_of: :favorite_players + else + self.primary_key = :fan_id, :team_id, :player_id + belongs_to :fanship, query_constraints: %i[fan_id team_id], inverse_of: :favorite_players + end + belongs_to :player end end diff --git a/spec/dummy_app/app/active_record/nested_fan.rb b/spec/dummy_app/app/active_record/nested_fan.rb index 85dd66a63b..f49fc7f453 100644 --- a/spec/dummy_app/app/active_record/nested_fan.rb +++ b/spec/dummy_app/app/active_record/nested_fan.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -if defined?(CompositePrimaryKeys) +if ActiveRecord.gem_version >= Gem::Version.new('7.1') || defined?(CompositePrimaryKeys) class NestedFan < Fan accepts_nested_attributes_for :fanships accepts_nested_attributes_for :fanship diff --git a/spec/dummy_app/app/active_record/nested_favorite_player.rb b/spec/dummy_app/app/active_record/nested_favorite_player.rb index a188ef63cb..a609b6646e 100644 --- a/spec/dummy_app/app/active_record/nested_favorite_player.rb +++ b/spec/dummy_app/app/active_record/nested_favorite_player.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -if defined?(CompositePrimaryKeys) +if ActiveRecord.gem_version >= Gem::Version.new('7.1') || defined?(CompositePrimaryKeys) class NestedFavoritePlayer < FavoritePlayer accepts_nested_attributes_for :fanship end diff --git a/spec/integration/actions/bulk_delete_spec.rb b/spec/integration/actions/bulk_delete_spec.rb index 36a20df8b6..43a1af59bd 100644 --- a/spec/integration/actions/bulk_delete_spec.rb +++ b/spec/integration/actions/bulk_delete_spec.rb @@ -87,7 +87,7 @@ end end - context 'with composite_primary_keys', composite_primary_keys: true do + context 'with composite primary keys', composite_primary_keys: true do let!(:fanships) { FactoryBot.create_list(:fanship, 3) } it 'provides check boxes for bulk operation' do @@ -96,7 +96,7 @@ end it 'deletes selected records' do - delete(bulk_delete_path(bulk_action: 'bulk_delete', model_name: 'fanship', bulk_ids: fanships[0..1].map { |fanship| fanship.id.to_s })) + delete(bulk_delete_path(bulk_action: 'bulk_delete', model_name: 'fanship', bulk_ids: fanships[0..1].map { |fanship| RailsAdmin::Support::CompositeKeysSerializer.serialize(fanship.id) })) expect(flash[:success]).to match(/2 Fanships successfully deleted/) expect(Fanship.all).to eq fanships[2..2] end diff --git a/spec/integration/actions/delete_spec.rb b/spec/integration/actions/delete_spec.rb index 63faf63f7d..fc69f9fb92 100644 --- a/spec/integration/actions/delete_spec.rb +++ b/spec/integration/actions/delete_spec.rb @@ -173,7 +173,7 @@ end end - context 'with composite_primary_keys', composite_primary_keys: true do + context 'with composite primary keys', composite_primary_keys: true do let(:fanship) { FactoryBot.create(:fanship) } it 'deletes the object' do diff --git a/spec/integration/actions/edit_spec.rb b/spec/integration/actions/edit_spec.rb index f44e655296..6fa5d1b323 100644 --- a/spec/integration/actions/edit_spec.rb +++ b/spec/integration/actions/edit_spec.rb @@ -879,7 +879,7 @@ class HelpTest < Tableless end end - context 'with composite_primary_keys', composite_primary_keys: true do + context 'with composite primary keys', composite_primary_keys: true do let(:fanship) { FactoryBot.create(:fanship) } it 'edits the object' do @@ -888,5 +888,34 @@ class HelpTest < Tableless click_button 'Save' expect { fanship.reload }.to change { fanship.since }.from(nil).to(Date.new(2000, 1, 23)) end + + context 'using custom serializer' do + before do + RailsAdmin.config.composite_keys_serializer = Class.new do + def self.serialize(keys) + keys.join(',') + end + + def self.deserialize(string) + string.split(',') + end + end + end + + it 'edits the object' do + visit edit_path(model_name: 'fanship', id: "#{fanship.fan_id},#{fanship.team_id}") + fill_in 'Since', with: '2000-01-23' + click_button 'Save' + expect { fanship.reload }.to change { fanship.since }.from(nil).to(Date.new(2000, 1, 23)) + end + end + + context 'receiving invalid id' do + it 'returns 404' do + visit edit_path(model_name: 'fanship', id: '11') + expect(page.driver.status_code).to eq(404) + is_expected.to have_content("Fanship with id '11' could not be found") + end + end end end diff --git a/spec/integration/actions/export_spec.rb b/spec/integration/actions/export_spec.rb index b91563d61b..4ba5ea8f40 100644 --- a/spec/integration/actions/export_spec.rb +++ b/spec/integration/actions/export_spec.rb @@ -160,7 +160,7 @@ end end - context 'with composite_primary_keys', composite_primary_keys: true do + context 'with composite primary keys', composite_primary_keys: true do let!(:fanship) { FactoryBot.create(:fanship) } it 'exports to CSV' do diff --git a/spec/integration/actions/index_spec.rb b/spec/integration/actions/index_spec.rb index 0aa59415d3..6f13ad32aa 100644 --- a/spec/integration/actions/index_spec.rb +++ b/spec/integration/actions/index_spec.rb @@ -350,7 +350,7 @@ describe 'fields' do before do - if defined?(CompositePrimaryKeys) + if defined?(ActiveRecord) && ActiveRecord.gem_version >= Gem::Version.new('7.1') || defined?(CompositePrimaryKeys) RailsAdmin.config Fan do configure(:fanships) { hide } configure(:fanship) { hide } @@ -1245,7 +1245,7 @@ def visit_page(page) end end - context 'with composite_primary_keys', composite_primary_keys: true do + context 'with composite primary keys', composite_primary_keys: true do let!(:fanships) { FactoryBot.create_list(:fanship, 3) } it 'shows the list' do @@ -1257,5 +1257,24 @@ def visit_page(page) end is_expected.to have_content '3 fanships' end + + context 'using custom serializer' do + before do + RailsAdmin.config.composite_keys_serializer = Class.new do + def self.serialize(keys) + keys.join(',') + end + + def self.deserialize(string) + string.split(',') + end + end + end + + it 'shows the member action links accordingly' do + visit index_path(model_name: 'fanship') + is_expected.to have_css(%(a[href$="/admin/fanship/#{fanships[0].fan_id},#{fanships[0].team_id}/edit"])) + end + end end end diff --git a/spec/integration/actions/new_spec.rb b/spec/integration/actions/new_spec.rb index 3a83184396..e8b67965c4 100644 --- a/spec/integration/actions/new_spec.rb +++ b/spec/integration/actions/new_spec.rb @@ -195,7 +195,7 @@ end end - context 'with composite_primary_keys', composite_primary_keys: true do + context 'with composite primary keys', composite_primary_keys: true do let!(:fan) { FactoryBot.create(:fan) } let!(:team) { FactoryBot.create(:team) } diff --git a/spec/integration/actions/show_spec.rb b/spec/integration/actions/show_spec.rb index 47b639d38f..c08b3619ae 100644 --- a/spec/integration/actions/show_spec.rb +++ b/spec/integration/actions/show_spec.rb @@ -458,7 +458,7 @@ end end - context 'with composite_primary_keys', composite_primary_keys: true do + context 'with composite primary keys', composite_primary_keys: true do let(:fanship) { FactoryBot.create(:fanship) } it 'shows the object' do diff --git a/spec/integration/fields/belongs_to_association_spec.rb b/spec/integration/fields/belongs_to_association_spec.rb index 87436915e9..78f997f5f0 100644 --- a/spec/integration/fields/belongs_to_association_spec.rb +++ b/spec/integration/fields/belongs_to_association_spec.rb @@ -101,6 +101,20 @@ is_expected.to have_content 'Favorite player successfully updated' expect(FavoritePlayer.all.map(&:fanship)).to eq [fanship] end + + context 'with invalid key' do + before do + allow_any_instance_of(RailsAdmin::Config::Fields::Types::BelongsToAssociation). + to receive(:collection).and_return([["Fanship ##{fanship.id}", 'invalid']]) + end + + it 'fails to update' do + visit edit_path(model_name: 'favorite_player', id: favorite_player.id) + select("Fanship ##{fanship.id}", from: 'Fanship') + click_button 'Save' + is_expected.to have_content 'Fanship must exist' + end + end end describe 'via remote-sourced field' do diff --git a/spec/integration/fields/has_many_association_spec.rb b/spec/integration/fields/has_many_association_spec.rb index 1fba4fd729..e2e2fde6e0 100644 --- a/spec/integration/fields/has_many_association_spec.rb +++ b/spec/integration/fields/has_many_association_spec.rb @@ -228,6 +228,19 @@ is_expected.to have_content 'Fan successfully updated' expect(fan.reload.fanships.map(&:team_id)).to match_array fanships.map(&:team_id)[0..1] end + + context 'with invalid key' do + before do + allow_any_instance_of(RailsAdmin::Config::Fields::Types::HasManyAssociation). + to receive(:collection).and_return([["Fanship ##{fanships[0].id}", 'invalid']]) + end + + it 'fails to update' do + visit edit_path(model_name: 'fan', id: fan.id) + select("Fanship ##{fanships[0].id}", from: 'Fanships') + expect { click_button 'Save' }.to raise_error ActiveRecord::RecordNotFound + end + end end describe 'via remote-sourced field' do diff --git a/spec/rails_admin/adapters/active_record_spec.rb b/spec/rails_admin/adapters/active_record_spec.rb index b91427f00b..cc84121ff0 100644 --- a/spec/rails_admin/adapters/active_record_spec.rb +++ b/spec/rails_admin/adapters/active_record_spec.rb @@ -127,9 +127,10 @@ class PlayerWithDefaultScope < Player expect(abstract_model.all(bulk_ids: @players[0..1].collect(&:id))).to match_array @players[0..1] end - it 'supports retrieval by bulk_ids with composite_primary_keys', composite_primary_keys: true do - expect(RailsAdmin::AbstractModel.new(Fanship).all(bulk_ids: ['1,2', '3,4']).to_sql). - to include 'WHERE ("fans_teams"."fan_id" = 1 AND "fans_teams"."team_id" = 2 OR "fans_teams"."fan_id" = 3 AND "fans_teams"."team_id" = 4)' + it 'supports retrieval by bulk_ids with composite primary keys', composite_primary_keys: true do + expect(RailsAdmin::AbstractModel.new(Fanship).all( + bulk_ids: %w[1_2 3_4], + ).to_sql.tr('`', '"')).to include 'WHERE ("fans_teams"."fan_id" = 1 AND "fans_teams"."team_id" = 2 OR "fans_teams"."fan_id" = 3 AND "fans_teams"."team_id" = 4)' end it 'supports pagination' do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ba420fbe43..d6280259e7 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -125,5 +125,5 @@ end end - config.filter_run_excluding composite_primary_keys: true unless defined?(CompositePrimaryKeys) + config.filter_run_excluding composite_primary_keys: true unless defined?(ActiveRecord) && ActiveRecord.gem_version >= Gem::Version.new('7.1') || defined?(CompositePrimaryKeys) end