From 1a294768e9ebacec0f8a900b517720530e1097e9 Mon Sep 17 00:00:00 2001 From: David Dollar Date: Sun, 13 Jul 2008 21:53:14 -0400 Subject: [PATCH] Allows nested Hashes to hydrate the appropriate ActiveRecord models. class Post < ActiveRecord::Base belongs_to :author, :accessible => true has_many :comments, :accessible => true end post = Post.create({ :title => 'Accessible Attributes', :author => { :name => 'David Dollar' }, :comments => [ { :body => 'First Post!' }, { :body => 'Nested Hashes are great!' } ] }) post.comments << { :body => 'Another Comment' } Includes tests for the following associations: belongs_to has_one has_many has_and_belongs_to_many Much thanks to lifo for the idea to make this be an option on associations and invaluable help in getting it there. --- activerecord/lib/active_record/associations.rb | 14 ++- .../associations/association_collection.rb | 6 + activerecord/test/cases/associations_test.rb | 108 ++++++++++++++++++++ activerecord/test/models/author.rb | 2 +- activerecord/test/models/post.rb | 6 + 5 files changed, 131 insertions(+), 5 deletions(-) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 6931744..d43e07a 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -692,6 +692,7 @@ module ActiveRecord # * :uniq - If true, duplicates will be omitted from the collection. Useful in conjunction with :through. # * :readonly - If true, all the associated objects are readonly through the association. # * :validate - If false, don't validate the associated objects when saving the parent object. true by default. + # * :accessible - Mass assignment is allowed for this assocation (similar to ActiveRecord::Base#attr_accessible). # # Option examples: # has_many :comments, :order => "posted_on" @@ -774,6 +775,7 @@ module ActiveRecord # association is a polymorphic +belongs_to+. # * :readonly - If true, the associated object is readonly through the association. # * :validate - If false, don't validate the associated object when saving the parent object. +false+ by default. + # * :accessible - Mass assignment is allowed for this assocation (similar to ActiveRecord::Base#attr_accessible). # # Option examples: # has_one :credit_card, :dependent => :destroy # destroys the associated credit card @@ -863,6 +865,7 @@ module ActiveRecord # to the +attr_readonly+ list in the associated classes (e.g. class Post; attr_readonly :comments_count; end). # * :readonly - If true, the associated object is readonly through the association. # * :validate - If false, don't validate the associated objects when saving the parent object. +false+ by default. + # * :accessible - Mass assignment is allowed for this assocation (similar to ActiveRecord::Base#attr_accessible). # # Option examples: # belongs_to :firm, :foreign_key => "client_of" @@ -1034,6 +1037,7 @@ module ActiveRecord # but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error. # * :readonly - If true, all the associated objects are readonly through the association. # * :validate - If false, don't validate the associated objects when saving the parent object. +true+ by default. + # * :accessible - Mass assignment is allowed for this assocation (similar to ActiveRecord::Base#attr_accessible). # # Option examples: # has_and_belongs_to_many :projects @@ -1109,6 +1113,8 @@ module ActiveRecord association = association_proxy_class.new(self, reflection) end + new_value = reflection.klass.new(new_value) if reflection.options[:accessible] && new_value.is_a?(Hash) + if association_proxy_class == HasOneThroughAssociation association.create_through_record(new_value) self.send(reflection.name, new_value) @@ -1357,7 +1363,7 @@ module ActiveRecord :finder_sql, :counter_sql, :before_add, :after_add, :before_remove, :after_remove, :extend, :readonly, - :validate + :validate, :accessible ) options[:extend] = create_extension_modules(association_id, extension, options[:extend]) @@ -1367,7 +1373,7 @@ module ActiveRecord def create_has_one_reflection(association_id, options) options.assert_valid_keys( - :class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :readonly, :validate, :primary_key + :class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :readonly, :validate, :primary_key, :accessible ) create_reflection(:has_one, association_id, options, self) @@ -1383,7 +1389,7 @@ module ActiveRecord def create_belongs_to_reflection(association_id, options) options.assert_valid_keys( :class_name, :foreign_key, :foreign_type, :remote, :select, :conditions, :include, :dependent, - :counter_cache, :extend, :polymorphic, :readonly, :validate + :counter_cache, :extend, :polymorphic, :readonly, :validate, :accessible ) reflection = create_reflection(:belongs_to, association_id, options, self) @@ -1403,7 +1409,7 @@ module ActiveRecord :finder_sql, :delete_sql, :insert_sql, :before_add, :after_add, :before_remove, :after_remove, :extend, :readonly, - :validate + :validate, :accessible ) options[:extend] = create_extension_modules(association_id, extension, options[:extend]) diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb index eb39714..a8db8d8 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/association_collection.rb @@ -94,6 +94,8 @@ module ActiveRecord @owner.transaction do flatten_deeper(records).each do |record| + record = @reflection.klass.new(record) if @reflection.options[:accessible] && record.is_a?(Hash) + raise_on_type_mismatch(record) add_record_to_target_with_callbacks(record) do |r| result &&= insert_record(record) unless @owner.new_record? @@ -226,6 +228,10 @@ module ActiveRecord # Replace this collection with +other_array+ # This will perform a diff and delete/add only records that have changed. def replace(other_array) + other_array.map! do |val| + val.is_a?(Hash) ? @reflection.klass.new(val) : val + end if @reflection.options[:accessible] + other_array.each { |val| raise_on_type_mismatch(val) } load_target diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 59349dd..474da11 100755 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -189,6 +189,114 @@ class AssociationProxyTest < ActiveRecord::TestCase end end + def test_belongs_to_mass_assignment + post_attributes = { :title => 'Associations', :body => 'Are They Accessible?' } + author_attributes = { :name => 'David Dollar' } + + assert_no_difference 'Author.count' do + assert_raise(ActiveRecord::AssociationTypeMismatch) do + Post.create(post_attributes.merge({:author => author_attributes})) + end + end + + assert_difference 'Author.count' do + post = Post.create(post_attributes.merge({:creatable_author => author_attributes})) + assert_equal post.creatable_author.name, author_attributes[:name] + end + end + + def test_has_one_mass_assignment + post_attributes = { :title => 'Associations', :body => 'Are They Accessible?' } + comment_attributes = { :body => 'Setter Takes Hash' } + + assert_no_difference 'Comment.count' do + assert_raise(ActiveRecord::AssociationTypeMismatch) do + Post.create(post_attributes.merge({:uncreatable_comment => comment_attributes})) + end + end + + assert_difference 'Comment.count' do + post = Post.create(post_attributes.merge({:creatable_comment => comment_attributes})) + assert_equal post.creatable_comment.body, comment_attributes[:body] + end + end + + def test_has_many_mass_assignment + post = posts(:welcome) + post_attributes = { :title => 'Associations', :body => 'Are They Accessible?' } + comment_attributes = { :body => 'Setter Takes Hash' } + + assert_no_difference 'Comment.count' do + assert_raise(ActiveRecord::AssociationTypeMismatch) do + Post.create(post_attributes.merge({:comments => [comment_attributes]})) + end + assert_raise(ActiveRecord::AssociationTypeMismatch) do + post.comments << comment_attributes + end + end + + assert_difference 'Comment.count' do + post = Post.create(post_attributes.merge({:creatable_comments => [comment_attributes]})) + assert_equal post.creatable_comments.last.body, comment_attributes[:body] + end + + assert_difference 'Comment.count' do + post.creatable_comments << comment_attributes + assert_equal post.comments.last.body, comment_attributes[:body] + end + + post.creatable_comments = [comment_attributes, comment_attributes] + assert_equal post.creatable_comments.count, 2 + end + + def test_has_and_belongs_to_many_mass_assignment + post = posts(:welcome) + post_attributes = { :title => 'Associations', :body => 'Are They Accessible?' } + category_attributes = { :name => 'Accessible Association', :type => 'Category' } + + assert_no_difference 'Category.count' do + assert_raise(ActiveRecord::AssociationTypeMismatch) do + Post.create(post_attributes.merge({:categories => [category_attributes]})) + end + assert_raise(ActiveRecord::AssociationTypeMismatch) do + post.categories << category_attributes + end + end + + assert_difference 'Category.count' do + post = Post.create(post_attributes.merge({:creatable_categories => [category_attributes]})) + assert_equal post.creatable_categories.last.name, category_attributes[:name] + end + + assert_difference 'Category.count' do + post.creatable_categories << category_attributes + assert_equal post.creatable_categories.last.name, category_attributes[:name] + end + + post.creatable_categories = [category_attributes, category_attributes] + assert_equal post.creatable_categories.count, 2 + end + + def test_association_proxy_setter_can_take_hash + special_comment_attributes = { :body => 'Setter Takes Hash' } + + post = posts(:welcome) + post.creatable_comment = { :body => 'Setter Takes Hash' } + + assert_equal post.creatable_comment.body, special_comment_attributes[:body] + end + + def test_association_collection_can_take_hash + post_attributes = { :title => 'Setter Takes', :body => 'Hash' } + david = authors(:david) + + post = (david.posts << post_attributes).last + assert_equal post.title, post_attributes[:title] + + david.posts = [post_attributes, post_attributes] + assert_equal david.posts.count, 2 + end + def setup_dangling_association josh = Author.create(:name => "Josh") p = Post.create(:title => "New on Edge", :body => "More cool stuff!", :author => josh) diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 82e0690..136dc39 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -1,5 +1,5 @@ class Author < ActiveRecord::Base - has_many :posts + has_many :posts, :accessible => true has_many :posts_with_comments, :include => :comments, :class_name => "Post" has_many :posts_with_comments_sorted_by_comment_id, :include => :comments, :class_name => "Post", :order => 'comments.id' has_many :posts_with_categories, :include => :categories, :class_name => "Post" diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 3adbc0c..572f89e 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -33,6 +33,12 @@ class Post < ActiveRecord::Base has_and_belongs_to_many :categories has_and_belongs_to_many :special_categories, :join_table => "categories_posts", :association_foreign_key => 'category_id' + belongs_to :creatable_author, :class_name => 'Author', :accessible => true + has_one :uncreatable_comment, :class_name => 'Comment', :accessible => false, :order => 'id desc' + has_one :creatable_comment, :class_name => 'Comment', :accessible => true, :order => 'id desc' + has_many :creatable_comments, :class_name => 'Comment', :accessible => true, :dependent => :destroy + has_and_belongs_to_many :creatable_categories, :class_name => 'Category', :accessible => true + has_many :taggings, :as => :taggable has_many :tags, :through => :taggings do def add_joins_and_select -- 1.5.6.1