Last active
July 18, 2018 18:57
-
-
Save martinsp/92d17373699df5c16b0f00bbb1b174e3 to your computer and use it in GitHub Desktop.
ActiveRecord assign_nested_attributes_for_collection_association bug
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# frozen_string_literal: true | |
begin | |
require "bundler/inline" | |
rescue LoadError => e | |
$stderr.puts "Bundler version 1.10 or later is required. Please update your Bundler" | |
raise e | |
end | |
gemfile(true) do | |
source "https://rubygems.org" | |
git_source(:github) { |repo| "https://github.com/#{repo}.git" } | |
# Activate the gem you are reporting the issue against. | |
gem "activerecord", "5.2.0" | |
gem "sqlite3" | |
end | |
require "active_record" | |
require "minitest/autorun" | |
require "logger" | |
# Ensure backward compatibility with Minitest 4 | |
Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test) | |
# This connection will do for database-independent bug reports. | |
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") | |
ActiveRecord::Base.logger = Logger.new(STDOUT) | |
ActiveRecord::Schema.define do | |
create_table :posts, force: true do |t| | |
end | |
create_table :comments, force: true do |t| | |
t.integer :post_id | |
t.string :a | |
t.string :b | |
end | |
end | |
module PostCommentsExtension | |
def build(attributes = {}) | |
super({ b: 'from extension'}.merge(attributes)) | |
end | |
end | |
class Post < ActiveRecord::Base | |
has_many :comments, extend: PostCommentsExtension | |
accepts_nested_attributes_for :comments | |
end | |
class Comment < ActiveRecord::Base | |
belongs_to :post | |
end | |
class BugTest < Minitest::Test | |
def test_association_stuff | |
post = Post.create! | |
comment = post.comments.build | |
assert_equal 'from extension', comment.b | |
post = Post.create! | |
post.comments_attributes = [{ a: 'comment 1'}] | |
assert_equal 'from extension', post.comments[0].b | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# frozen_string_literal: true | |
begin | |
require "bundler/inline" | |
rescue LoadError => e | |
$stderr.puts "Bundler version 1.10 or later is required. Please update your Bundler" | |
raise e | |
end | |
gemfile(true) do | |
source "https://rubygems.org" | |
git_source(:github) { |repo| "https://github.com/#{repo}.git" } | |
# Activate the gem you are reporting the issue against. | |
gem "activerecord", "5.2.0" | |
gem "sqlite3" | |
end | |
require "active_record" | |
require "minitest/autorun" | |
require "logger" | |
# Ensure backward compatibility with Minitest 4 | |
Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test) | |
# This connection will do for database-independent bug reports. | |
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") | |
ActiveRecord::Base.logger = Logger.new(STDOUT) | |
ActiveRecord::Schema.define do | |
create_table :posts, force: true do |t| | |
end | |
create_table :comments, force: true do |t| | |
t.integer :post_id | |
t.string :a | |
t.string :b | |
end | |
end | |
module PostCommentsExtension | |
def build(attributes = {}) | |
super({ b: 'from extension'}.merge(attributes)) | |
end | |
end | |
class Post < ActiveRecord::Base | |
has_many :comments, extend: PostCommentsExtension | |
accepts_nested_attributes_for :comments | |
# override for method defined in: activerecord/lib/active_record/nested_attributes.rb | |
# the only change is on lines 92/93 | |
def assign_nested_attributes_for_collection_association(association_name, attributes_collection) | |
options = nested_attributes_options[association_name] | |
if attributes_collection.respond_to?(:permitted?) | |
attributes_collection = attributes_collection.to_h | |
end | |
unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array) | |
raise ArgumentError, "Hash or Array expected for attribute `#{association_name}`, got #{attributes_collection.class.name} (#{attributes_collection.inspect})" | |
end | |
check_record_limit!(options[:limit], attributes_collection) | |
if attributes_collection.is_a? Hash | |
keys = attributes_collection.keys | |
attributes_collection = if keys.include?("id") || keys.include?(:id) | |
[attributes_collection] | |
else | |
attributes_collection.values | |
end | |
end | |
association = association(association_name) | |
existing_records = if association.loaded? | |
association.target | |
else | |
attribute_ids = attributes_collection.map { |a| a["id"] || a[:id] }.compact | |
attribute_ids.empty? ? [] : association.scope.where(association.klass.primary_key => attribute_ids) | |
end | |
attributes_collection.each do |attributes| | |
if attributes.respond_to?(:permitted?) | |
attributes = attributes.to_h | |
end | |
attributes = attributes.with_indifferent_access | |
if attributes["id"].blank? | |
unless reject_new_record?(association_name, attributes) | |
# association.build(attributes.except(*UNASSIGNABLE_KEYS)) | |
association.reader.build(attributes.except(*UNASSIGNABLE_KEYS)) | |
end | |
elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes["id"].to_s } | |
unless call_reject_if(association_name, attributes) | |
# Make sure we are operating on the actual object which is in the association's | |
# proxy_target array (either by finding it, or adding it if not found) | |
# Take into account that the proxy_target may have changed due to callbacks | |
target_record = association.target.detect { |record| record.id.to_s == attributes["id"].to_s } | |
if target_record | |
existing_record = target_record | |
else | |
association.add_to_target(existing_record, :skip_callbacks) | |
end | |
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) | |
end | |
else | |
raise_nested_attributes_record_not_found!(association_name, attributes["id"]) | |
end | |
end | |
end | |
end | |
class Comment < ActiveRecord::Base | |
belongs_to :post | |
end | |
class BugTest < Minitest::Test | |
def test_association_stuff | |
post = Post.create! | |
comment = post.comments.build | |
assert_equal 'from extension', comment.b | |
post = Post.create! | |
post.comments_attributes = [{ a: 'comment 1'}] | |
assert_equal 'from extension', post.comments[0].b | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment