diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 591433b48c..6dcf02c77f 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,7 @@ *SVN* +* Introduce the :readonly option to all associations. Records from the association cannot be saved. #11084 [miloops] + * Multiparameter attributes for time columns fail over to DateTime when out of range of Time [Geoff Buesing] * Base#instantiate_time_object uses Time.zone.local() [Geoff Buesing] diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 3ff2d5db6b..4070813ec8 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -669,6 +669,7 @@ module ActiveRecord # * :source_type: Specifies type of the source association used by has_many :through queries where the source # association is a polymorphic +belongs_to+. # * :uniq - if set to +true+, duplicates will be omitted from the collection. Useful in conjunction with :through. + # * :readonly - if set to +true+, all the associated objects are readonly through the association. # # Option examples: # has_many :comments, :order => "posted_on" @@ -677,6 +678,7 @@ module ActiveRecord # has_many :tracks, :order => "position", :dependent => :destroy # has_many :comments, :dependent => :nullify # has_many :tags, :as => :taggable + # has_many :reports, :readonly => true # has_many :subscribers, :through => :subscriptions, :source => :user # has_many :subscribers, :class_name => "Person", :finder_sql => # 'SELECT DISTINCT people.* ' + @@ -735,13 +737,15 @@ module ActiveRecord # as the default +foreign_key+. # * :include - specify second-order associations that should be eager loaded when this object is loaded. # * :as: Specifies a polymorphic interface (See #belongs_to). - # + # * :readonly - if set to +true+, the associated object is readonly through the association. + # # Option examples: # has_one :credit_card, :dependent => :destroy # destroys the associated credit card # has_one :credit_card, :dependent => :nullify # updates the associated records foreign key value to NULL rather than destroying it # has_one :last_comment, :class_name => "Comment", :order => "posted_on" # has_one :project_manager, :class_name => "Person", :conditions => "role = 'project_manager'" # has_one :attachment, :as => :attachable + # has_one :boss, :readonly => :true def has_one(association_id, options = {}) reflection = create_has_one_reflection(association_id, options) @@ -811,6 +815,7 @@ module ActiveRecord # * :polymorphic - specify this association is a polymorphic association by passing +true+. # Note: If you've enabled the counter cache, then you may want to add the counter cache attribute # to the attr_readonly list in the associated classes (e.g. class Post; attr_readonly :comments_count; end). + # * :readonly - if set to +true+, the associated object is readonly through the association. # # Option examples: # belongs_to :firm, :foreign_key => "client_of" @@ -818,6 +823,7 @@ module ActiveRecord # belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id", # :conditions => 'discounts > #{payments_count}' # belongs_to :attachable, :polymorphic => true + # belongs_to :project, :readonly => true def belongs_to(association_id, options = {}) reflection = create_belongs_to_reflection(association_id, options) @@ -970,12 +976,14 @@ module ActiveRecord # * :offset: An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows. # * :select: By default, this is * as in SELECT * FROM, but can be changed if, for example, you want to do a join # but not include the joined columns. + # * :readonly - if set to +true+, all the associated objects are readonly through the association. # # Option examples: # has_and_belongs_to_many :projects # has_and_belongs_to_many :projects, :include => [ :milestones, :manager ] # has_and_belongs_to_many :nations, :class_name => "Country" # has_and_belongs_to_many :categories, :join_table => "prods_cats" + # has_and_belongs_to_many :categories, :readonly => true # has_and_belongs_to_many :active_projects, :join_table => 'developers_projects', :delete_sql => # 'DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}' def has_and_belongs_to_many(association_id, options = {}, &extension) @@ -1234,7 +1242,7 @@ module ActiveRecord :uniq, :finder_sql, :counter_sql, :before_add, :after_add, :before_remove, :after_remove, - :extend + :extend, :readonly ) options[:extend] = create_extension_modules(association_id, extension, options[:extend]) @@ -1244,7 +1252,7 @@ module ActiveRecord def create_has_one_reflection(association_id, options) options.assert_valid_keys( - :class_name, :foreign_key, :remote, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as + :class_name, :foreign_key, :remote, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :readonly ) create_reflection(:has_one, association_id, options, self) @@ -1253,7 +1261,7 @@ module ActiveRecord def create_belongs_to_reflection(association_id, options) options.assert_valid_keys( :class_name, :foreign_key, :foreign_type, :remote, :conditions, :order, :include, :dependent, - :counter_cache, :extend, :polymorphic + :counter_cache, :extend, :polymorphic, :readonly ) reflection = create_reflection(:belongs_to, association_id, options, self) @@ -1272,7 +1280,7 @@ module ActiveRecord :uniq, :finder_sql, :delete_sql, :insert_sql, :before_add, :after_add, :before_remove, :after_remove, - :extend + :extend, :readonly ) options[:extend] = create_extension_modules(association_id, extension, options[:extend]) diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index d4d3bf7682..9fc0d44a01 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -114,7 +114,8 @@ module ActiveRecord :offset => @reflection.options[:offset], :joins => @reflection.options[:joins], :include => @reflection.options[:include], - :select => @reflection.options[:select] + :select => @reflection.options[:select], + :readonly => @reflection.options[:readonly] ) end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index f5557618c4..9ff3f13592 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -44,7 +44,8 @@ module ActiveRecord @reflection.klass.find( @owner[@reflection.primary_key_name], :conditions => conditions, - :include => @reflection.options[:include] + :include => @reflection.options[:include], + :readonly => @reflection.options[:readonly] ) end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 312cc1e487..3ff9fe3b9f 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -53,7 +53,8 @@ module ActiveRecord @reflection.klass.find(:first, :conditions => @finder_sql, :order => @reflection.options[:order], - :include => @reflection.options[:include] + :include => @reflection.options[:include], + :readonly => @reflection.options[:readonly] ) end diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index cfc58ffa10..89b978ad06 100755 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -469,6 +469,11 @@ class HasOneAssociationsTest < ActiveRecord::TestCase end end + def test_cant_save_readonly_association + assert_raise(ActiveRecord::ReadOnlyRecord) { companies(:first_firm).readonly_account.save! } + assert companies(:first_firm).readonly_account.readonly? + end + end @@ -544,6 +549,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 2, companies(:first_firm).limited_clients.find_all_by_type('Client', :limit => 9_000).length end + def test_dynamic_find_all_should_respect_readonly_access + companies(:first_firm).readonly_clients.find(:all).each { |c| assert_raise(ActiveRecord::ReadOnlyRecord) { c.save! } } + companies(:first_firm).readonly_clients.find(:all).each { |c| assert c.readonly? } + end + def test_triple_equality assert !(Array === Firm.find(:first).clients) assert Firm.find(:first).clients === Array @@ -1581,6 +1591,11 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal post.author_id, author2.id end + def test_cant_save_readonly_association + assert_raise(ActiveRecord::ReadOnlyRecord) { companies(:first_client).readonly_firm.save! } + assert companies(:first_client).readonly_firm.readonly? + end + end @@ -1987,6 +2002,11 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 2, projects(:active_record).limited_developers.find_all_by_name('Jamis', :limit => 9_000).length end + def test_dynamic_find_all_should_respect_readonly_access + projects(:active_record).readonly_developers.each { |d| assert_raise(ActiveRecord::ReadOnlyRecord) { d.save! } if d.valid?} + projects(:active_record).readonly_developers.each { |d| d.readonly? } + end + def test_new_with_values_in_collection jamis = DeveloperForProjectWithAfterCreateHook.find_by_name('Jamis') david = DeveloperForProjectWithAfterCreateHook.find_by_name('David') diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index c2dedec3ef..c8ee40ea09 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -159,9 +159,9 @@ class ReflectionTest < ActiveRecord::TestCase end def test_reflection_of_all_associations - assert_equal 17, Firm.reflect_on_all_associations.size - assert_equal 15, Firm.reflect_on_all_associations(:has_many).size - assert_equal 2, Firm.reflect_on_all_associations(:has_one).size + assert_equal 19, Firm.reflect_on_all_associations.size + assert_equal 16, Firm.reflect_on_all_associations(:has_many).size + assert_equal 3, Firm.reflect_on_all_associations(:has_one).size assert_equal 0, Firm.reflect_on_all_associations(:belongs_to).size end diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index c33f188a02..3d76dfd398 100755 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -40,8 +40,10 @@ class Firm < Company :counter_sql => 'SELECT COUNT(*) FROM companies WHERE client_of = 1000' has_many :clients_using_finder_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE 1=1' has_many :plain_clients, :class_name => 'Client' + has_many :readonly_clients, :class_name => 'Client', :readonly => true has_one :account, :foreign_key => "firm_id", :dependent => :destroy + has_one :readonly_account, :foreign_key => "firm_id", :class_name => "Account", :readonly => true end class DependentFirm < Company @@ -60,6 +62,7 @@ class Client < Company belongs_to :firm_with_basic_id, :class_name => "Firm", :foreign_key => "firm_id" belongs_to :firm_with_other_name, :class_name => "Firm", :foreign_key => "client_of" belongs_to :firm_with_condition, :class_name => "Firm", :foreign_key => "client_of", :conditions => ["1 = ?", 1] + belongs_to :readonly_firm, :class_name => "Firm", :foreign_key => "firm_id", :readonly => true # Record destruction so we can test whether firm.clients.clear has # is calling client.destroy, deleting from the database, or setting diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb index b90d2c6fbc..e1ab89eca5 100644 --- a/activerecord/test/models/project.rb +++ b/activerecord/test/models/project.rb @@ -1,5 +1,6 @@ class Project < ActiveRecord::Base has_and_belongs_to_many :developers, :uniq => true, :order => 'developers.name desc, developers.id desc' + has_and_belongs_to_many :readonly_developers, :class_name => "Developer", :readonly => true has_and_belongs_to_many :selected_developers, :class_name => "Developer", :select => "developers.*", :uniq => true has_and_belongs_to_many :non_unique_developers, :order => 'developers.name desc, developers.id desc', :class_name => 'Developer' has_and_belongs_to_many :limited_developers, :class_name => "Developer", :limit => 1