Skip to content

Commit

Permalink
ActiveRecord 7.1 composite primary keys support
Browse files Browse the repository at this point in the history
  • Loading branch information
mshibuya committed Aug 18, 2024
1 parent 5245d5b commit 53b89c9
Show file tree
Hide file tree
Showing 37 changed files with 281 additions and 191 deletions.
2 changes: 1 addition & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .rubocop_todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions app/helpers/rails_admin/form_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 0 additions & 23 deletions config/initializers/active_record_extensions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
17 changes: 10 additions & 7 deletions lib/rails_admin/abstract_model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 34 additions & 2 deletions lib/rails_admin/adapters/active_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
31 changes: 24 additions & 7 deletions lib/rails_admin/adapters/active_record/association.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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

Expand Down
40 changes: 0 additions & 40 deletions lib/rails_admin/adapters/composite_primary_keys.rb

This file was deleted.

45 changes: 0 additions & 45 deletions lib/rails_admin/adapters/composite_primary_keys/association.rb

This file was deleted.

5 changes: 5 additions & 0 deletions lib/rails_admin/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
#
Expand Down Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion lib/rails_admin/config/fields/association.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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
Expand Down
23 changes: 20 additions & 3 deletions lib/rails_admin/config/fields/collection_association.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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,
Expand Down
10 changes: 9 additions & 1 deletion lib/rails_admin/config/fields/singular_association.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,22 @@ 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

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
Expand Down
Loading

0 comments on commit 53b89c9

Please sign in to comment.