public
Description: Ruby on Rails
Homepage: http://rubyonrails.org
Clone URL: git://github.com/rails/rails.git
Add :accessible option to Associations for allowing mass assignments using hash. 
[#474 state:resolved]

Allows nested Hashes (i.e. from nested forms) 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' }

Signed-off-by: Pratik Naik <pratiknaik@gmail.com>
ddollar (author)
Sun Jul 13 18:13:50 -0700 2008
lifo (committer)
Sun Jul 13 18:53:21 -0700 2008
commit  e0750d6a5c7f621e4ca12205137c0b135cab444a
tree    442e795385f4114b8270e9d5d325b307bbb1bc13
parent  c6f397c5cecf183680c191dd2128c0a96c5b9399
...
1
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
4
5
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
0
@@ -1,5 +1,23 @@
0
 *Edge*
0
 
0
+* Add :accessible option to associations for allowing (opt-in) mass assignment. #474. [David Dollar] Example :
0
+
0
+  class Post < ActiveRecord::Base
0
+    belongs_to :author,   :accessible => true
0
+    has_many   :comments, :accessible => true
0
+  end
0
+
0
+  post = Post.create({
0
+    :title    => 'Accessible Attributes',
0
+    :author   => { :name => 'David Dollar' },
0
+    :comments => [
0
+      { :body => 'First Post!' },
0
+      { :body => 'Nested Hashes are great!' }
0
+    ]
0
+  })
0
+
0
+  post.comments << { :body => 'Another Comment' }
0
+
0
 * Add :tokenizer option to validates_length_of to specify how to split up the attribute string. #507. [David Lowenfels] Example :
0
 
0
   # Ensure essay contains at least 100 words.
...
692
693
694
 
695
696
697
...
774
775
776
 
777
778
779
...
863
864
865
 
866
867
868
...
1034
1035
1036
 
1037
1038
1039
...
1109
1110
1111
 
 
1112
1113
1114
...
1357
1358
1359
1360
 
1361
1362
1363
...
1367
1368
1369
1370
 
1371
1372
1373
...
1383
1384
1385
1386
 
1387
1388
1389
...
1403
1404
1405
1406
 
1407
1408
1409
...
692
693
694
695
696
697
698
...
775
776
777
778
779
780
781
...
865
866
867
868
869
870
871
...
1037
1038
1039
1040
1041
1042
1043
...
1113
1114
1115
1116
1117
1118
1119
1120
...
1363
1364
1365
 
1366
1367
1368
1369
...
1373
1374
1375
 
1376
1377
1378
1379
...
1389
1390
1391
 
1392
1393
1394
1395
...
1409
1410
1411
 
1412
1413
1414
1415
0
@@ -692,6 +692,7 @@ module ActiveRecord
0
       # * <tt>:uniq</tt> - If true, duplicates will be omitted from the collection. Useful in conjunction with <tt>:through</tt>.
0
       # * <tt>:readonly</tt> - If true, all the associated objects are readonly through the association.
0
       # * <tt>:validate</tt> - If false, don't validate the associated objects when saving the parent object. true by default.
0
+      # * <tt>:accessible</tt> - Mass assignment is allowed for this assocation (similar to <tt>ActiveRecord::Base#attr_accessible</tt>).
0
       #
0
       # Option examples:
0
       #   has_many :comments, :order => "posted_on"
0
@@ -774,6 +775,7 @@ module ActiveRecord
0
       #   association is a polymorphic +belongs_to+.      
0
       # * <tt>:readonly</tt> - If true, the associated object is readonly through the association.
0
       # * <tt>:validate</tt> - If false, don't validate the associated object when saving the parent object. +false+ by default.
0
+      # * <tt>:accessible</tt> - Mass assignment is allowed for this assocation (similar to <tt>ActiveRecord::Base#attr_accessible</tt>).
0
       #
0
       # Option examples:
0
       #   has_one :credit_card, :dependent => :destroy  # destroys the associated credit card
0
@@ -863,6 +865,7 @@ module ActiveRecord
0
       #   to the +attr_readonly+ list in the associated classes (e.g. <tt>class Post; attr_readonly :comments_count; end</tt>).
0
       # * <tt>:readonly</tt> - If true, the associated object is readonly through the association.
0
       # * <tt>:validate</tt> - If false, don't validate the associated objects when saving the parent object. +false+ by default.
0
+      # * <tt>:accessible</tt> - Mass assignment is allowed for this assocation (similar to <tt>ActiveRecord::Base#attr_accessible</tt>).
0
       #
0
       # Option examples:
0
       #   belongs_to :firm, :foreign_key => "client_of"
0
@@ -1034,6 +1037,7 @@ module ActiveRecord
0
       #   but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error.
0
       # * <tt>:readonly</tt> - If true, all the associated objects are readonly through the association.
0
       # * <tt>:validate</tt> - If false, don't validate the associated objects when saving the parent object. +true+ by default.
0
+      # * <tt>:accessible</tt> - Mass assignment is allowed for this assocation (similar to <tt>ActiveRecord::Base#attr_accessible</tt>).
0
       #
0
       # Option examples:
0
       #   has_and_belongs_to_many :projects
0
@@ -1109,6 +1113,8 @@ module ActiveRecord
0
               association = association_proxy_class.new(self, reflection)
0
             end
0
 
0
+            new_value = reflection.klass.new(new_value) if reflection.options[:accessible] && new_value.is_a?(Hash)
0
+
0
             if association_proxy_class == HasOneThroughAssociation
0
               association.create_through_record(new_value)
0
               self.send(reflection.name, new_value)
0
@@ -1357,7 +1363,7 @@ module ActiveRecord
0
             :finder_sql, :counter_sql,
0
             :before_add, :after_add, :before_remove, :after_remove,
0
             :extend, :readonly,
0
-            :validate
0
+            :validate, :accessible
0
           )
0
 
0
           options[:extend] = create_extension_modules(association_id, extension, options[:extend])
0
@@ -1367,7 +1373,7 @@ module ActiveRecord
0
 
0
         def create_has_one_reflection(association_id, options)
0
           options.assert_valid_keys(
0
-            :class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :readonly, :validate, :primary_key
0
+            :class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :readonly, :validate, :primary_key, :accessible
0
           )
0
 
0
           create_reflection(:has_one, association_id, options, self)
0
@@ -1383,7 +1389,7 @@ module ActiveRecord
0
         def create_belongs_to_reflection(association_id, options)
0
           options.assert_valid_keys(
0
             :class_name, :foreign_key, :foreign_type, :remote, :select, :conditions, :include, :dependent,
0
-            :counter_cache, :extend, :polymorphic, :readonly, :validate
0
+            :counter_cache, :extend, :polymorphic, :readonly, :validate, :accessible
0
           )
0
 
0
           reflection = create_reflection(:belongs_to, association_id, options, self)
0
@@ -1403,7 +1409,7 @@ module ActiveRecord
0
             :finder_sql, :delete_sql, :insert_sql,
0
             :before_add, :after_add, :before_remove, :after_remove,
0
             :extend, :readonly,
0
-            :validate
0
+            :validate, :accessible
0
           )
0
 
0
           options[:extend] = create_extension_modules(association_id, extension, options[:extend])
...
97
98
99
 
 
100
101
102
...
229
230
231
 
 
 
 
232
233
234
...
97
98
99
100
101
102
103
104
...
231
232
233
234
235
236
237
238
239
240
0
@@ -97,6 +97,8 @@ module ActiveRecord
0
 
0
         @owner.transaction do
0
           flatten_deeper(records).each do |record|
0
+            record = @reflection.klass.new(record) if @reflection.options[:accessible] && record.is_a?(Hash)
0
+
0
             raise_on_type_mismatch(record)
0
             add_record_to_target_with_callbacks(record) do |r|
0
               result &&= insert_record(record) unless @owner.new_record?
0
@@ -229,6 +231,10 @@ module ActiveRecord
0
       # Replace this collection with +other_array+
0
       # This will perform a diff and delete/add only records that have changed.
0
       def replace(other_array)
0
+        other_array.map! do |val|
0
+          val.is_a?(Hash) ? @reflection.klass.new(val) : val
0
+        end if @reflection.options[:accessible]
0
+
0
         other_array.each { |val| raise_on_type_mismatch(val) }
0
 
0
         load_target
...
189
190
191
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
193
194
...
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
0
@@ -189,6 +189,114 @@ class AssociationProxyTest < ActiveRecord::TestCase
0
     end
0
   end
0
 
0
+  def test_belongs_to_mass_assignment
0
+    post_attributes   = { :title => 'Associations', :body => 'Are They Accessible?' }
0
+    author_attributes = { :name  => 'David Dollar' }
0
+
0
+    assert_no_difference 'Author.count' do
0
+      assert_raise(ActiveRecord::AssociationTypeMismatch) do
0
+        Post.create(post_attributes.merge({:author => author_attributes}))
0
+      end
0
+    end
0
+
0
+    assert_difference 'Author.count' do
0
+      post = Post.create(post_attributes.merge({:creatable_author => author_attributes}))
0
+      assert_equal post.creatable_author.name, author_attributes[:name]
0
+    end
0
+  end
0
+
0
+  def test_has_one_mass_assignment
0
+    post_attributes    = { :title => 'Associations', :body => 'Are They Accessible?' }
0
+    comment_attributes = { :body  => 'Setter Takes Hash' }
0
+
0
+    assert_no_difference 'Comment.count' do
0
+      assert_raise(ActiveRecord::AssociationTypeMismatch) do
0
+        Post.create(post_attributes.merge({:uncreatable_comment => comment_attributes}))
0
+      end
0
+    end
0
+
0
+    assert_difference 'Comment.count' do
0
+      post = Post.create(post_attributes.merge({:creatable_comment => comment_attributes}))
0
+      assert_equal post.creatable_comment.body, comment_attributes[:body]
0
+    end
0
+  end
0
+
0
+  def test_has_many_mass_assignment
0
+    post               = posts(:welcome)
0
+    post_attributes    = { :title => 'Associations', :body => 'Are They Accessible?' }
0
+    comment_attributes = { :body  => 'Setter Takes Hash' }
0
+
0
+    assert_no_difference 'Comment.count' do
0
+      assert_raise(ActiveRecord::AssociationTypeMismatch) do
0
+        Post.create(post_attributes.merge({:comments => [comment_attributes]}))
0
+      end
0
+      assert_raise(ActiveRecord::AssociationTypeMismatch) do
0
+        post.comments << comment_attributes
0
+      end
0
+    end
0
+
0
+    assert_difference 'Comment.count' do
0
+      post = Post.create(post_attributes.merge({:creatable_comments => [comment_attributes]}))
0
+      assert_equal post.creatable_comments.last.body, comment_attributes[:body]
0
+    end
0
+
0
+    assert_difference 'Comment.count' do
0
+      post.creatable_comments << comment_attributes
0
+      assert_equal post.comments.last.body, comment_attributes[:body]
0
+    end
0
+
0
+    post.creatable_comments = [comment_attributes, comment_attributes]
0
+    assert_equal post.creatable_comments.count, 2
0
+  end
0
+
0
+  def test_has_and_belongs_to_many_mass_assignment
0
+    post                = posts(:welcome)
0
+    post_attributes     = { :title => 'Associations', :body => 'Are They Accessible?' }
0
+    category_attributes = { :name  => 'Accessible Association', :type => 'Category' }
0
+
0
+    assert_no_difference 'Category.count' do
0
+      assert_raise(ActiveRecord::AssociationTypeMismatch) do
0
+        Post.create(post_attributes.merge({:categories => [category_attributes]}))
0
+      end
0
+      assert_raise(ActiveRecord::AssociationTypeMismatch) do
0
+        post.categories << category_attributes
0
+      end
0
+    end
0
+
0
+    assert_difference 'Category.count' do
0
+      post = Post.create(post_attributes.merge({:creatable_categories => [category_attributes]}))
0
+      assert_equal post.creatable_categories.last.name, category_attributes[:name]
0
+    end
0
+
0
+    assert_difference 'Category.count' do
0
+      post.creatable_categories << category_attributes
0
+      assert_equal post.creatable_categories.last.name, category_attributes[:name]
0
+    end
0
+
0
+    post.creatable_categories = [category_attributes, category_attributes]
0
+    assert_equal post.creatable_categories.count, 2
0
+  end
0
+
0
+  def test_association_proxy_setter_can_take_hash
0
+    special_comment_attributes = { :body => 'Setter Takes Hash' }
0
+
0
+    post = posts(:welcome)
0
+    post.creatable_comment = { :body => 'Setter Takes Hash' }
0
+
0
+    assert_equal post.creatable_comment.body, special_comment_attributes[:body]
0
+  end
0
+
0
+  def test_association_collection_can_take_hash
0
+    post_attributes = { :title => 'Setter Takes', :body => 'Hash' }
0
+    david = authors(:david)
0
+
0
+    post = (david.posts << post_attributes).last
0
+    assert_equal post.title, post_attributes[:title]
0
+
0
+    david.posts = [post_attributes, post_attributes]
0
+    assert_equal david.posts.count, 2
0
+  end
0
+
0
   def setup_dangling_association
0
     josh = Author.create(:name => "Josh")
0
     p = Post.create(:title => "New on Edge", :body => "More cool stuff!", :author => josh)
...
1
2
 
3
4
5
...
1
 
2
3
4
5
0
@@ -1,5 +1,5 @@
0
 class Author < ActiveRecord::Base
0
-  has_many :posts
0
+  has_many :posts, :accessible => true
0
   has_many :posts_with_comments, :include => :comments, :class_name => "Post"
0
   has_many :posts_with_comments_sorted_by_comment_id, :include => :comments, :class_name => "Post", :order => 'comments.id'
0
   has_many :posts_with_categories, :include => :categories, :class_name => "Post"
...
33
34
35
 
 
 
 
 
 
36
37
38
...
33
34
35
36
37
38
39
40
41
42
43
44
0
@@ -33,6 +33,12 @@ class Post < ActiveRecord::Base
0
   has_and_belongs_to_many :categories
0
   has_and_belongs_to_many :special_categories, :join_table => "categories_posts", :association_foreign_key => 'category_id'
0
 
0
+  belongs_to              :creatable_author,     :class_name => 'Author',   :accessible => true
0
+  has_one                 :uncreatable_comment,  :class_name => 'Comment',  :accessible => false, :order => 'id desc'
0
+  has_one                 :creatable_comment,    :class_name => 'Comment',  :accessible => true,  :order => 'id desc'
0
+  has_many                :creatable_comments,   :class_name => 'Comment',  :accessible => true,  :dependent => :destroy
0
+  has_and_belongs_to_many :creatable_categories, :class_name => 'Category', :accessible => true
0
+
0
   has_many :taggings, :as => :taggable
0
   has_many :tags, :through => :taggings do
0
     def add_joins_and_select

Comments

glennpow Fri Jul 25 06:39:37 -0700 2008

I am attempting to utilize mass-assign for my models, but can’t get the update_attributes to work correctly when I have association_collections as child models. My create methods work fine, since the hash passes in an Array value for these collections. I.E.:

emails => [ {…}, {…} ] )

But, when I try to edit/update my model, the field names in the form naturally have the :id of the email included in them, so that the resulting value passed to the mass-assign is not an Array, but a Hash of this form:

emails => {"5" => {…}, “6” => {…}}

Where the 5 and 6 are the :id values of the respective emails. This gets passed into the update_attributes, which eventually gets to:

AssociationCollection.replace(other_array)

which assumes “emails” to be an Array. Shouldn’t this replace method be “smarter” so that if a Hash is passed in, it will ascertain the :id values from it, and then reassign the model attributes accordingly? Furthermore, what would happen if there were a combination of updated models (emails) and perhaps one new email (that didn’t have an :id). Does the mass-assign functionality handle this situation?

Any help would be appreciated.

-Glenn