mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Make sure nested through associations are read only
This commit is contained in:
parent
d619e39938
commit
edc176d33b
6 changed files with 82 additions and 9 deletions
|
@ -65,6 +65,12 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
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:
|
class HasAndBelongsToManyAssociationWithPrimaryKeyError < ActiveRecordError #:nodoc:
|
||||||
def initialize(reflection)
|
def initialize(reflection)
|
||||||
super("Primary key is not allowed in a has_and_belongs_to_many join table (#{reflection.options[:join_table]}).")
|
super("Primary key is not allowed in a has_and_belongs_to_many join table (#{reflection.options[:join_table]}).")
|
||||||
|
|
|
@ -8,6 +8,11 @@ module ActiveRecord
|
||||||
class HasManyThroughAssociation < HasManyAssociation #:nodoc:
|
class HasManyThroughAssociation < HasManyAssociation #:nodoc:
|
||||||
include ThroughAssociationScope
|
include ThroughAssociationScope
|
||||||
|
|
||||||
|
def build(attributes = {}, &block)
|
||||||
|
ensure_not_nested
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
alias_method :new, :build
|
alias_method :new, :build
|
||||||
|
|
||||||
def create!(attrs = nil)
|
def create!(attrs = nil)
|
||||||
|
@ -37,6 +42,7 @@ module ActiveRecord
|
||||||
|
|
||||||
protected
|
protected
|
||||||
def create_record(attrs, force = true)
|
def create_record(attrs, force = true)
|
||||||
|
ensure_not_nested
|
||||||
ensure_owner_is_not_new
|
ensure_owner_is_not_new
|
||||||
|
|
||||||
transaction do
|
transaction do
|
||||||
|
@ -60,6 +66,8 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def insert_record(record, force = true, validate = true)
|
def insert_record(record, force = true, validate = true)
|
||||||
|
ensure_not_nested
|
||||||
|
|
||||||
if record.new_record?
|
if record.new_record?
|
||||||
if force
|
if force
|
||||||
record.save!
|
record.save!
|
||||||
|
@ -75,6 +83,8 @@ module ActiveRecord
|
||||||
|
|
||||||
# TODO - add dependent option support
|
# TODO - add dependent option support
|
||||||
def delete_records(records)
|
def delete_records(records)
|
||||||
|
ensure_not_nested
|
||||||
|
|
||||||
klass = @reflection.through_reflection.klass
|
klass = @reflection.through_reflection.klass
|
||||||
records.each do |associate|
|
records.each do |associate|
|
||||||
klass.delete_all(construct_join_attributes(associate))
|
klass.delete_all(construct_join_attributes(associate))
|
||||||
|
|
|
@ -14,6 +14,8 @@ module ActiveRecord
|
||||||
private
|
private
|
||||||
|
|
||||||
def create_through_record(new_value) #nodoc:
|
def create_through_record(new_value) #nodoc:
|
||||||
|
ensure_not_nested
|
||||||
|
|
||||||
klass = @reflection.through_reflection.klass
|
klass = @reflection.through_reflection.klass
|
||||||
|
|
||||||
current_object = @owner.send(@reflection.through_reflection.name)
|
current_object = @owner.send(@reflection.through_reflection.name)
|
||||||
|
|
|
@ -8,15 +8,18 @@ module ActiveRecord
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def construct_scope
|
def construct_scope
|
||||||
{ :create => construct_owner_attributes(@reflection),
|
scope = {}
|
||||||
:find => { :conditions => construct_conditions,
|
scope[:find] = {
|
||||||
|
:conditions => construct_conditions,
|
||||||
:joins => construct_joins,
|
:joins => construct_joins,
|
||||||
:include => @reflection.options[:include] || @reflection.source_reflection.options[:include],
|
:include => @reflection.options[:include] || @reflection.source_reflection.options[:include],
|
||||||
:select => construct_select,
|
:select => construct_select,
|
||||||
:order => @reflection.options[:order],
|
:order => @reflection.options[:order],
|
||||||
:limit => @reflection.options[:limit],
|
:limit => @reflection.options[:limit],
|
||||||
:readonly => @reflection.options[:readonly],
|
:readonly => @reflection.options[:readonly]
|
||||||
} }
|
}
|
||||||
|
scope[:create] = construct_owner_attributes(@reflection) unless @reflection.nested?
|
||||||
|
scope
|
||||||
end
|
end
|
||||||
|
|
||||||
# Build SQL conditions from attributes, qualified by table name.
|
# Build SQL conditions from attributes, qualified by table name.
|
||||||
|
@ -299,6 +302,12 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
alias_method :sql_conditions, :conditions
|
alias_method :sql_conditions, :conditions
|
||||||
|
|
||||||
|
def ensure_not_nested
|
||||||
|
if @reflection.nested?
|
||||||
|
raise HasManyThroughNestedAssociationsAreReadonly.new(@owner, @reflection)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -396,6 +396,10 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def nested?
|
||||||
|
through_reflection_chain.length > 2
|
||||||
|
end
|
||||||
|
|
||||||
# Gets an array of possible <tt>:through</tt> source reflection names:
|
# Gets an array of possible <tt>:through</tt> source reflection names:
|
||||||
#
|
#
|
||||||
# [:singularized, :pluralized]
|
# [:singularized, :pluralized]
|
||||||
|
|
|
@ -364,6 +364,48 @@ class NestedHasManyThroughAssociationsTest < ActiveRecord::TestCase
|
||||||
assert !scope.where("comments.type" => "SubSpecialComment").empty?
|
assert !scope.where("comments.type" => "SubSpecialComment").empty?
|
||||||
end
|
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
|
private
|
||||||
|
|
||||||
def assert_includes_and_joins_equal(query, expected, association)
|
def assert_includes_and_joins_equal(query, expected, association)
|
||||||
|
|
Loading…
Reference in a new issue