Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Make sure nested through associations are read only

  • Loading branch information...
commit edc176d33be9499f4c096779c5b4711b5daf0c06 1 parent d619e39
@jonleighton jonleighton authored
View
6 activerecord/lib/active_record/associations.rb
@@ -64,6 +64,12 @@ def initialize(owner, reflection)
super("Cannot dissociate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to delete the has_many :through record associating them.")
end
end
+
+ class HasManyThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc
+ def initialize(owner, reflection)
+ super("Cannot modify association '#{owner.class.name}##{reflection.name}' because it goes through more than one other association.")
+ end
+ end
class HasAndBelongsToManyAssociationWithPrimaryKeyError < ActiveRecordError #:nodoc:
def initialize(reflection)
View
10 activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -8,6 +8,11 @@ module Associations
class HasManyThroughAssociation < HasManyAssociation #:nodoc:
include ThroughAssociationScope
+ def build(attributes = {}, &block)
+ ensure_not_nested
+ super
+ end
+
alias_method :new, :build
def create!(attrs = nil)
@@ -37,6 +42,7 @@ def size
protected
def create_record(attrs, force = true)
+ ensure_not_nested
ensure_owner_is_not_new
transaction do
@@ -60,6 +66,8 @@ def construct_find_options!(options)
end
def insert_record(record, force = true, validate = true)
+ ensure_not_nested
+
if record.new_record?
if force
record.save!
@@ -75,6 +83,8 @@ def insert_record(record, force = true, validate = true)
# TODO - add dependent option support
def delete_records(records)
+ ensure_not_nested
+
klass = @reflection.through_reflection.klass
records.each do |associate|
klass.delete_all(construct_join_attributes(associate))
View
2  activerecord/lib/active_record/associations/has_one_through_association.rb
@@ -14,6 +14,8 @@ def replace(new_value)
private
def create_through_record(new_value) #nodoc:
+ ensure_not_nested
+
klass = @reflection.through_reflection.klass
current_object = @owner.send(@reflection.through_reflection.name)
View
27 activerecord/lib/active_record/associations/through_association_scope.rb
@@ -8,15 +8,18 @@ module ThroughAssociationScope
protected
def construct_scope
- { :create => construct_owner_attributes(@reflection),
- :find => { :conditions => construct_conditions,
- :joins => construct_joins,
- :include => @reflection.options[:include] || @reflection.source_reflection.options[:include],
- :select => construct_select,
- :order => @reflection.options[:order],
- :limit => @reflection.options[:limit],
- :readonly => @reflection.options[:readonly],
- } }
+ scope = {}
+ scope[:find] = {
+ :conditions => construct_conditions,
+ :joins => construct_joins,
+ :include => @reflection.options[:include] || @reflection.source_reflection.options[:include],
+ :select => construct_select,
+ :order => @reflection.options[:order],
+ :limit => @reflection.options[:limit],
+ :readonly => @reflection.options[:readonly]
+ }
+ scope[:create] = construct_owner_attributes(@reflection) unless @reflection.nested?
+ scope
end
# Build SQL conditions from attributes, qualified by table name.
@@ -299,6 +302,12 @@ def build_sti_condition
end
alias_method :sql_conditions, :conditions
+
+ def ensure_not_nested
+ if @reflection.nested?
+ raise HasManyThroughNestedAssociationsAreReadonly.new(@owner, @reflection)
+ end
+ end
end
end
end
View
4 activerecord/lib/active_record/reflection.rb
@@ -395,6 +395,10 @@ def through_reflection_chain
chain
end
end
+
+ def nested?
+ through_reflection_chain.length > 2
+ end
# Gets an array of possible <tt>:through</tt> source reflection names:
#
View
42 activerecord/test/cases/associations/nested_has_many_through_associations_test.rb
@@ -363,6 +363,48 @@ def test_has_many_through_with_sti_on_through_reflection
assert !scope.where("comments.type" => "SpecialComment").empty?
assert !scope.where("comments.type" => "SubSpecialComment").empty?
end
+
+ def test_nested_has_many_through_writers_should_raise_error
+ david = authors(:david)
+ subscriber = subscribers(:first)
+
+ assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
+ david.subscribers = [subscriber]
+ end
+
+ assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
+ david.subscriber_ids = [subscriber.id]
+ end
+
+ assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
+ david.subscribers << subscriber
+ end
+
+ assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
+ david.subscribers.delete(subscriber)
+ end
+
+ assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
+ david.subscribers.clear
+ end
+
+ assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
+ david.subscribers.build
+ end
+
+ assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
+ david.subscribers.create
+ end
+ end
+
+ def test_nested_has_one_through_writers_should_raise_error
+ groucho = members(:groucho)
+ founding = member_types(:founding)
+
+ assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do
+ groucho.nested_member_type = founding
+ end
+ end
private
Please sign in to comment.
Something went wrong with that request. Please try again.