From 5cda000bf0f6d85d1a1efedf9fa4d0b6eaf988a1 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 27 Feb 2009 13:50:24 +0100 Subject: [PATCH 1/8] Fixed that autosave should validate associations even if master is invalid [#1930 status:committed] --- .../lib/active_record/associations.rb | 120 +---- .../has_and_belongs_to_many_association.rb | 4 +- .../associations/has_many_association.rb | 4 +- .../has_many_through_association.rb | 4 +- .../lib/active_record/autosave_association.rb | 237 ++++++--- .../belongs_to_associations_test.rb | 129 ----- .../has_many_associations_test.rb | 129 ----- .../associations/has_one_associations_test.rb | 87 ---- .../test/cases/autosave_association_test.rb | 450 +++++++++++++++++- 9 files changed, 629 insertions(+), 535 deletions(-) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index e2dc883b1b886..b6f87a0486199 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -786,11 +786,7 @@ module ClassMethods # 'ORDER BY p.first_name' def has_many(association_id, options = {}, &extension) reflection = create_has_many_reflection(association_id, options, &extension) - configure_dependency_for_has_many(reflection) - - add_multiple_associated_validation_callbacks(reflection.name) unless options[:validate] == false - add_multiple_associated_save_callbacks(reflection.name) add_association_callbacks(reflection.name, reflection.options) if options[:through] @@ -872,10 +868,10 @@ def has_many(association_id, options = {}, &extension) # [:source] # Specifies the source association name used by has_one :through queries. Only use it if the name cannot be # inferred from the association. has_one :favorite, :through => :favorites will look for a - # :favorite on Favorite, unless a :source is given. + # :favorite on Favorite, unless a :source is given. # [:source_type] # Specifies type of the source association used by has_one :through queries where the source - # association is a polymorphic +belongs_to+. + # association is a polymorphic +belongs_to+. # [:readonly] # If true, the associated object is readonly through the association. # [:validate] @@ -898,22 +894,9 @@ def has_one(association_id, options = {}) association_accessor_methods(reflection, ActiveRecord::Associations::HasOneThroughAssociation) else reflection = create_has_one_reflection(association_id, options) - - method_name = "has_one_after_save_for_#{reflection.name}".to_sym - define_method(method_name) do - association = association_instance_get(reflection.name) - if association && (new_record? || association.new_record? || association[reflection.primary_key_name] != id) - association[reflection.primary_key_name] = id - association.save(true) - end - end - after_save method_name - - add_single_associated_validation_callbacks(reflection.name) if options[:validate] == true association_accessor_methods(reflection, HasOneAssociation) association_constructor_method(:build, reflection, HasOneAssociation) association_constructor_method(:create, reflection, HasOneAssociation) - configure_dependency_for_has_one(reflection) end end @@ -1006,40 +989,10 @@ def belongs_to(association_id, options = {}) if reflection.options[:polymorphic] association_accessor_methods(reflection, BelongsToPolymorphicAssociation) - - method_name = "polymorphic_belongs_to_before_save_for_#{reflection.name}".to_sym - define_method(method_name) do - association = association_instance_get(reflection.name) - if association && association.target - if association.new_record? - association.save(true) - end - - if association.updated? - self[reflection.primary_key_name] = association.id - self[reflection.options[:foreign_type]] = association.class.base_class.name.to_s - end - end - end - before_save method_name else association_accessor_methods(reflection, BelongsToAssociation) association_constructor_method(:build, reflection, BelongsToAssociation) association_constructor_method(:create, reflection, BelongsToAssociation) - - method_name = "belongs_to_before_save_for_#{reflection.name}".to_sym - define_method(method_name) do - if association = association_instance_get(reflection.name) - if association.new_record? - association.save(true) - end - - if association.updated? - self[reflection.primary_key_name] = association.id - end - end - end - before_save method_name end # Create the callbacks to update counter cache @@ -1067,8 +1020,6 @@ def belongs_to(association_id, options = {}) ) end - add_single_associated_validation_callbacks(reflection.name) if options[:validate] == true - configure_dependency_for_belongs_to(reflection) end @@ -1234,9 +1185,6 @@ def belongs_to(association_id, options = {}) # '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) reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension) - - add_multiple_associated_validation_callbacks(reflection.name) unless options[:validate] == false - add_multiple_associated_save_callbacks(reflection.name) collection_accessor_methods(reflection, HasAndBelongsToManyAssociation) # Don't use a before_destroy callback since users' before_destroy @@ -1358,70 +1306,6 @@ def collection_accessor_methods(reflection, association_proxy_class, writer = tr end end - def add_single_associated_validation_callbacks(association_name) - method_name = "validate_associated_records_for_#{association_name}".to_sym - define_method(method_name) do - if association = association_instance_get(association_name) - errors.add association_name unless association.target.nil? || association.valid? - end - end - - validate method_name - end - - def add_multiple_associated_validation_callbacks(association_name) - method_name = "validate_associated_records_for_#{association_name}".to_sym - define_method(method_name) do - association = association_instance_get(association_name) - - if association - if new_record? - association - elsif association.loaded? - association.select { |record| record.new_record? } - else - association.target.select { |record| record.new_record? } - end.each do |record| - errors.add association_name unless record.valid? - end - end - end - - validate method_name - end - - def add_multiple_associated_save_callbacks(association_name) - method_name = "before_save_associated_records_for_#{association_name}".to_sym - define_method(method_name) do - @new_record_before_save = new_record? - true - end - before_save method_name - - method_name = "after_create_or_update_associated_records_for_#{association_name}".to_sym - define_method(method_name) do - association = association_instance_get(association_name) - - records_to_save = if @new_record_before_save - association - elsif association && association.loaded? - association.select { |record| record.new_record? } - elsif association && !association.loaded? - association.target.select { |record| record.new_record? } - else - [] - end - records_to_save.each { |record| association.send(:insert_record, record) } unless records_to_save.blank? - - # reconstruct the SQL queries now that we know the owner's id - association.send(:construct_sql) if association.respond_to?(:construct_sql) - end - - # Doesn't use after_save as that would save associations added in after_create/after_update twice - after_create method_name - after_update method_name - end - def association_constructor_method(constructor, reflection, association_proxy_class) define_method("#{constructor}_#{reflection.name}") do |*params| attributees = params.first unless params.empty? diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index a5cc3bf091b6b..af9ce3dfb2534 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -28,12 +28,12 @@ def count_records load_target.size end - def insert_record(record, force=true) + def insert_record(record, force = true, validate = true) if record.new_record? if force record.save! else - return false unless record.save + return false unless record.save(validate) end end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 3348079e9dd78..a2cbabfe0c3d7 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -56,9 +56,9 @@ def cached_counter_attribute_name "#{@reflection.name}_count" end - def insert_record(record) + def insert_record(record, force = false, validate = true) set_belongs_to_association_for(record) - record.save + force ? record.save! : record.save(validate) end # Deletes the records according to the :dependent option. diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 2eeeb28d1f2d5..d5d188ac2aad8 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -47,12 +47,12 @@ def construct_find_options!(options) options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil? end - def insert_record(record, force=true) + def insert_record(record, force = true, validate = true) if record.new_record? if force record.save! else - return false unless record.save + return false unless record.save(validate) end end through_reflection = @reflection.through_reflection diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 680b41518cce4..6e09b13150fa0 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -125,79 +125,63 @@ module ActiveRecord # post.author.name = '' # post.save(false) # => true module AutosaveAssociation + ASSOCIATION_TYPES = %w{ has_one belongs_to has_many has_and_belongs_to_many } + def self.included(base) base.class_eval do + base.extend(ClassMethods) alias_method_chain :reload, :autosave_associations - alias_method_chain :save, :autosave_associations - alias_method_chain :save!, :autosave_associations - alias_method_chain :valid?, :autosave_associations - %w{ has_one belongs_to has_many has_and_belongs_to_many }.each do |type| + ASSOCIATION_TYPES.each do |type| base.send("valid_keys_for_#{type}_association") << :autosave end end end - # Saves the parent, self, and any loaded autosave associations. - # In addition, it destroys all children that were marked for destruction - # with mark_for_destruction. - # - # This all happens inside a transaction, _if_ the Transactions module is included into - # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. - def save_with_autosave_associations(perform_validation = true) - returning(save_without_autosave_associations(perform_validation)) do |valid| - if valid - self.class.reflect_on_all_autosave_associations.each do |reflection| - if (association = association_instance_get(reflection.name)) && association.loaded? - if association.is_a?(Array) - association.proxy_target.each do |child| - child.marked_for_destruction? ? child.destroy : child.save(perform_validation) - end - else - association.marked_for_destruction? ? association.destroy : association.save(perform_validation) - end - end + module ClassMethods + private + + # def belongs_to(name, options = {}) + # super + # add_autosave_association_callbacks(reflect_on_association(name)) + # end + ASSOCIATION_TYPES.each do |type| + module_eval %{ + def #{type}(name, options = {}) + super + add_autosave_association_callbacks(reflect_on_association(name)) end - end + } end - end - # Attempts to save the record just like save_with_autosave_associations but - # will raise a RecordInvalid exception instead of returning false if the - # record is not valid. - def save_with_autosave_associations! - if valid_with_autosave_associations? - save_with_autosave_associations(false) || raise(RecordNotSaved) - else - raise RecordInvalid.new(self) - end - end + # Adds a validate and save callback for the association as specified by + # the +reflection+. + def add_autosave_association_callbacks(reflection) + save_method = "autosave_associated_records_for_#{reflection.name}" + validation_method = "validate_associated_records_for_#{reflection.name}" + validate validation_method - # Returns whether or not the parent, self, and any loaded autosave associations are valid. - def valid_with_autosave_associations? - if valid_without_autosave_associations? - self.class.reflect_on_all_autosave_associations.all? do |reflection| - if (association = association_instance_get(reflection.name)) && association.loaded? - if association.is_a?(Array) - association.proxy_target.all? { |child| autosave_association_valid?(reflection, child) } - else - autosave_association_valid?(reflection, association) - end - else - true # association not loaded yet, so it should be valid + case reflection.macro + when :has_many, :has_and_belongs_to_many + before_save :before_save_collection_association + + define_method(save_method) { save_collection_association(reflection) } + # Doesn't use after_save as that would save associations added in after_create/after_update twice + after_create save_method + after_update save_method + + define_method(validation_method) { validate_collection_association(reflection) } + else + case reflection.macro + when :has_one + define_method(save_method) { save_has_one_association(reflection) } + after_save save_method + when :belongs_to + define_method(save_method) { save_belongs_to_association(reflection) } + before_save save_method end + define_method(validation_method) { validate_single_association(reflection) } end - else - false # self was not valid - end - end - - # Returns whether or not the association is valid and applies any errors to the parent, self, if it wasn't. - def autosave_association_valid?(reflection, association) - returning(association.valid?) do |valid| - association.errors.each do |attribute, message| - errors.add "#{reflection.name}_#{attribute}", message - end unless valid end end @@ -221,5 +205,142 @@ def mark_for_destruction def marked_for_destruction? @marked_for_destruction end + + private + + # Returns the record for an association collection that should be validated + # or saved. If +autosave+ is +false+ only new records will be returned, + # unless the parent is/was a new record itself. + def associated_records_to_validate_or_save(association, new_record, autosave) + if new_record + association + elsif association.loaded? + autosave ? association : association.select { |record| record.new_record? } + else + autosave ? association.target : association.target.select { |record| record.new_record? } + end + end + + # Validate the association if :validate or :autosave is + # turned on for the association specified by +reflection+. + def validate_single_association(reflection) + if reflection.options[:validate] == true || reflection.options[:autosave] == true + if (association = association_instance_get(reflection.name)) && !association.target.nil? + association_valid?(reflection, association) + end + end + end + + # Validate the associated records if :validate or + # :autosave is turned on for the association specified by + # +reflection+. + def validate_collection_association(reflection) + if reflection.options[:validate] != false && association = association_instance_get(reflection.name) + if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave]) + records.each { |record| association_valid?(reflection, record) } + end + end + end + + # Returns whether or not the association is valid and applies any errors to + # the parent, self, if it wasn't. + def association_valid?(reflection, association) + unless valid = association.valid? + if reflection.options[:autosave] + association.errors.each do |attribute, message| + attribute = "#{reflection.name}_#{attribute}" + errors.add(attribute, message) unless errors.on(attribute) + end + else + errors.add(reflection.name) + end + end + valid + end + + # Is used as a before_save callback to check while saving a collection + # association whether or not the parent was a new record before saving. + def before_save_collection_association + @new_record_before_save = new_record? + true + end + + # Saves any new associated records, or all loaded autosave associations if + # :autosave is enabled on the association. + # + # In addition, it destroys all children that were marked for destruction + # with mark_for_destruction. + # + # This all happens inside a transaction, _if_ the Transactions module is included into + # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. + def save_collection_association(reflection) + if association = association_instance_get(reflection.name) + autosave = reflection.options[:autosave] + + if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave) + records.each do |record| + if autosave && record.marked_for_destruction? + record.destroy + elsif @new_record_before_save || record.new_record? + if autosave + association.send(:insert_record, record, false, false) + else + association.send(:insert_record, record) + end + elsif autosave + record.save(false) + end + end + end + + # reconstruct the SQL queries now that we know the owner's id + association.send(:construct_sql) if association.respond_to?(:construct_sql) + end + end + + # Saves the associated record if it's new or :autosave is enabled + # on the association. + # + # In addition, it will destroy the association if it was marked for + # destruction with mark_for_destruction. + # + # This all happens inside a transaction, _if_ the Transactions module is included into + # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. + def save_has_one_association(reflection) + if association = association_instance_get(reflection.name) + if reflection.options[:autosave] && association.marked_for_destruction? + association.destroy + elsif new_record? || association.new_record? || association[reflection.primary_key_name] != id || reflection.options[:autosave] + association[reflection.primary_key_name] = id + association.save(false) + end + end + end + + # Saves the associated record if it's new or :autosave is enabled + # on the association. + # + # In addition, it will destroy the association if it was marked for + # destruction with mark_for_destruction. + # + # This all happens inside a transaction, _if_ the Transactions module is included into + # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. + def save_belongs_to_association(reflection) + if association = association_instance_get(reflection.name) + if reflection.options[:autosave] && association.marked_for_destruction? + association.destroy + else + association.save(false) if association.new_record? || reflection.options[:autosave] + + if association.updated? + self[reflection.primary_key_name] = association.id + # TODO: Removing this code doesn't seem to matter… + if reflection.options[:polymorphic] + self[reflection.options[:foreign_type]] = association.class.base_class.name.to_s + end + end + end + end + end end end \ No newline at end of file diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 40a8503980ad0..92ed465ae749f 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -190,19 +190,6 @@ def test_belongs_to_counter_after_update_attributes assert_equal 1, Topic.find(topic.id).send(:read_attribute, "replies_count") end - def test_assignment_before_parent_saved - client = Client.find(:first) - apple = Firm.new("name" => "Apple") - client.firm = apple - assert_equal apple, client.firm - assert apple.new_record? - assert client.save - assert apple.save - assert !apple.new_record? - assert_equal apple, client.firm - assert_equal apple, client.firm(true) - end - def test_assignment_before_child_saved final_cut = Client.new("name" => "Final Cut") firm = Firm.find(1) @@ -215,19 +202,6 @@ def test_assignment_before_child_saved assert_equal firm, final_cut.firm(true) end - def test_assignment_before_either_saved - final_cut = Client.new("name" => "Final Cut") - apple = Firm.new("name" => "Apple") - final_cut.firm = apple - assert final_cut.new_record? - assert apple.new_record? - assert final_cut.save - assert !final_cut.new_record? - assert !apple.new_record? - assert_equal apple, final_cut.firm - assert_equal apple, final_cut.firm(true) - end - def test_new_record_with_foreign_key_but_no_object c = Client.new("firm_id" => 1) assert_equal Firm.find(:first), c.firm_with_basic_id @@ -274,90 +248,6 @@ def test_custom_counter_cache assert_equal 17, reply.replies.size end - def test_store_two_association_with_one_save - num_orders = Order.count - num_customers = Customer.count - order = Order.new - - customer1 = order.billing = Customer.new - customer2 = order.shipping = Customer.new - assert order.save - assert_equal customer1, order.billing - assert_equal customer2, order.shipping - - order.reload - - assert_equal customer1, order.billing - assert_equal customer2, order.shipping - - assert_equal num_orders +1, Order.count - assert_equal num_customers +2, Customer.count - end - - - def test_store_association_in_two_relations_with_one_save - num_orders = Order.count - num_customers = Customer.count - order = Order.new - - customer = order.billing = order.shipping = Customer.new - assert order.save - assert_equal customer, order.billing - assert_equal customer, order.shipping - - order.reload - - assert_equal customer, order.billing - assert_equal customer, order.shipping - - assert_equal num_orders +1, Order.count - assert_equal num_customers +1, Customer.count - end - - def test_store_association_in_two_relations_with_one_save_in_existing_object - num_orders = Order.count - num_customers = Customer.count - order = Order.create - - customer = order.billing = order.shipping = Customer.new - assert order.save - assert_equal customer, order.billing - assert_equal customer, order.shipping - - order.reload - - assert_equal customer, order.billing - assert_equal customer, order.shipping - - assert_equal num_orders +1, Order.count - assert_equal num_customers +1, Customer.count - end - - def test_store_association_in_two_relations_with_one_save_in_existing_object_with_values - num_orders = Order.count - num_customers = Customer.count - order = Order.create - - customer = order.billing = order.shipping = Customer.new - assert order.save - assert_equal customer, order.billing - assert_equal customer, order.shipping - - order.reload - - customer = order.billing = order.shipping = Customer.new - - assert order.save - order.reload - - assert_equal customer, order.billing - assert_equal customer, order.shipping - - assert_equal num_orders +1, Order.count - assert_equal num_customers +2, Customer.count - end - - def test_association_assignment_sticks post = Post.find(:first) @@ -410,25 +300,6 @@ def test_polymorphic_assignment_updates_foreign_id_field_for_new_and_saved_recor assert_equal nil, sponsor.sponsorable_id end - def test_save_fails_for_invalid_belongs_to - assert log = AuditLog.create(:developer_id=>0,:message=>"") - - log.developer = Developer.new - assert !log.developer.valid? - assert !log.valid? - assert !log.save - assert_equal "is invalid", log.errors.on("developer") - end - - def test_save_succeeds_for_invalid_belongs_to_with_validate_false - assert log = AuditLog.create(:developer_id=>0,:message=>"") - - log.unvalidated_developer = Developer.new - assert !log.unvalidated_developer.valid? - assert log.valid? - assert log.save - end - def test_belongs_to_proxy_should_not_respond_to_private_methods assert_raises(NoMethodError) { companies(:first_firm).private_method } assert_raises(NoMethodError) { companies(:second_client).firm.private_method } diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index a2525f1d70dc1..5efbc5bd0cbdd 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -317,81 +317,6 @@ def test_adding_a_collection assert_equal 3, companies(:first_firm).clients_of_firm(true).size end - def test_adding_before_save - no_of_firms = Firm.count - no_of_clients = Client.count - - new_firm = Firm.new("name" => "A New Firm, Inc") - c = Client.new("name" => "Apple") - - new_firm.clients_of_firm.push Client.new("name" => "Natural Company") - assert_equal 1, new_firm.clients_of_firm.size - new_firm.clients_of_firm << c - assert_equal 2, new_firm.clients_of_firm.size - - assert_equal no_of_firms, Firm.count # Firm was not saved to database. - assert_equal no_of_clients, Client.count # Clients were not saved to database. - assert new_firm.save - assert !new_firm.new_record? - assert !c.new_record? - assert_equal new_firm, c.firm - assert_equal no_of_firms+1, Firm.count # Firm was saved to database. - assert_equal no_of_clients+2, Client.count # Clients were saved to database. - - assert_equal 2, new_firm.clients_of_firm.size - assert_equal 2, new_firm.clients_of_firm(true).size - end - - def test_invalid_adding - firm = Firm.find(1) - assert !(firm.clients_of_firm << c = Client.new) - assert c.new_record? - assert !firm.valid? - assert !firm.save - assert c.new_record? - end - - def test_invalid_adding_before_save - no_of_firms = Firm.count - no_of_clients = Client.count - new_firm = Firm.new("name" => "A New Firm, Inc") - new_firm.clients_of_firm.concat([c = Client.new, Client.new("name" => "Apple")]) - assert c.new_record? - assert !c.valid? - assert !new_firm.valid? - assert !new_firm.save - assert c.new_record? - assert new_firm.new_record? - end - - def test_invalid_adding_with_validate_false - firm = Firm.find(:first) - client = Client.new - firm.unvalidated_clients_of_firm << client - - assert firm.valid? - assert !client.valid? - assert firm.save - assert client.new_record? - end - - def test_valid_adding_with_validate_false - no_of_clients = Client.count - - firm = Firm.find(:first) - client = Client.new("name" => "Apple") - - assert firm.valid? - assert client.valid? - assert client.new_record? - - firm.unvalidated_clients_of_firm << client - - assert firm.save - assert !client.new_record? - assert_equal no_of_clients+1, Client.count - end - def test_build company = companies(:first_firm) new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") } @@ -400,10 +325,6 @@ def test_build assert_equal "Another Client", new_client.name assert new_client.new_record? assert_equal new_client, company.clients_of_firm.last - company.name += '-changed' - assert_queries(2) { assert company.save } - assert !new_client.new_record? - assert_equal 2, company.clients_of_firm(true).size end def test_collection_size_after_building @@ -428,11 +349,7 @@ def test_collection_size_twice_for_regressions def test_build_many company = companies(:first_firm) new_clients = assert_no_queries { company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) } - assert_equal 2, new_clients.size - company.name += '-changed' - assert_queries(3) { assert company.save } - assert_equal 3, company.clients_of_firm(true).size end def test_build_followed_by_save_does_not_load_target @@ -463,10 +380,6 @@ def test_build_via_block assert_equal "Another Client", new_client.name assert new_client.new_record? assert_equal new_client, company.clients_of_firm.last - company.name += '-changed' - assert_queries(2) { assert company.save } - assert !new_client.new_record? - assert_equal 2, company.clients_of_firm(true).size end def test_build_many_via_block @@ -480,10 +393,6 @@ def test_build_many_via_block assert_equal 2, new_clients.size assert_equal "changed", new_clients.first.name assert_equal "changed", new_clients.last.name - - company.name += '-changed' - assert_queries(3) { assert company.save } - assert_equal 3, company.clients_of_firm(true).size end def test_create_without_loading_association @@ -501,16 +410,6 @@ def test_create_without_loading_association assert_equal 2, first_firm.clients_of_firm.size end - def test_invalid_build - new_client = companies(:first_firm).clients_of_firm.build - assert new_client.new_record? - assert !new_client.valid? - assert_equal new_client, companies(:first_firm).clients_of_firm.last - assert !companies(:first_firm).save - assert new_client.new_record? - assert_equal 1, companies(:first_firm).clients_of_firm(true).size - end - def test_create force_signal37_to_load_all_clients_of_firm new_client = companies(:first_firm).clients_of_firm.create("name" => "Another Client") @@ -843,15 +742,6 @@ def test_replace_with_new assert !firm.clients.include?(:first_client) end - def test_replace_on_new_object - firm = Firm.new("name" => "New Firm") - firm.clients = [companies(:second_client), Client.new("name" => "New Client")] - assert firm.save - firm.reload - assert_equal 2, firm.clients.length - assert firm.clients.include?(Client.find_by_name("New Client")) - end - def test_get_ids assert_equal [companies(:first_client).id, companies(:second_client).id], companies(:first_firm).client_ids end @@ -879,15 +769,6 @@ def test_get_ids_for_unloaded_finder_sql_associations_loads_them assert company.clients_using_sql.loaded? end - def test_assign_ids - firm = Firm.new("name" => "Apple") - firm.client_ids = [companies(:first_client).id, companies(:second_client).id] - firm.save - firm.reload - assert_equal 2, firm.clients.length - assert firm.clients.include?(companies(:second_client)) - end - def test_assign_ids_ignoring_blanks firm = Firm.create!(:name => 'Apple') firm.client_ids = [companies(:first_client).id, nil, companies(:second_client).id, ''] @@ -910,16 +791,6 @@ def test_modifying_a_through_a_has_many_should_raise ].each {|block| assert_raise(ActiveRecord::HasManyThroughCantAssociateThroughHasManyReflection, &block) } end - - def test_assign_ids_for_through_a_belongs_to - post = Post.new(:title => "Assigning IDs works!", :body => "You heared it here first, folks!") - post.person_ids = [people(:david).id, people(:michael).id] - post.save - post.reload - assert_equal 2, post.people.length - assert post.people.include?(people(:david)) - end - def test_dynamic_find_should_respect_association_order_for_through assert_equal Comment.find(10), authors(:david).comments_desc.find(:first, :conditions => "comments.type = 'SpecialComment'") assert_equal Comment.find(10), authors(:david).comments_desc.find_by_type('SpecialComment') diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 14032a67c033c..e81bba790a04a 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -193,28 +193,6 @@ def test_build assert_equal account, firm.account end - def test_build_before_child_saved - firm = Firm.find(1) - - account = firm.account.build("credit_limit" => 1000) - assert_equal account, firm.account - assert account.new_record? - assert firm.save - assert_equal account, firm.account - assert !account.new_record? - end - - def test_build_before_either_saved - firm = Firm.new("name" => "GlobalMegaCorp") - - firm.account = account = Account.new("credit_limit" => 1000) - assert_equal account, firm.account - assert account.new_record? - assert firm.save - assert_equal account, firm.account - assert !account.new_record? - end - def test_failing_build_association firm = Firm.new("name" => "GlobalMegaCorp") firm.save @@ -253,16 +231,6 @@ def test_dependence_with_missing_association_and_nullify firm.destroy end - def test_assignment_before_parent_saved - firm = Firm.new("name" => "GlobalMegaCorp") - firm.account = a = Account.find(1) - assert firm.new_record? - assert_equal a, firm.account - assert firm.save - assert_equal a, firm.account - assert_equal a, firm.account(true) - end - def test_finding_with_interpolated_condition firm = Firm.find(:first) superior = firm.clients.create(:name => 'SuperiorCo') @@ -279,61 +247,6 @@ def test_assignment_before_child_saved assert_equal a, firm.account assert_equal a, firm.account(true) end - - def test_save_fails_for_invalid_has_one - firm = Firm.find(:first) - assert firm.valid? - - firm.account = Account.new - - assert !firm.account.valid? - assert !firm.valid? - assert !firm.save - assert_equal "is invalid", firm.errors.on("account") - end - - - def test_save_succeeds_for_invalid_has_one_with_validate_false - firm = Firm.find(:first) - assert firm.valid? - - firm.unvalidated_account = Account.new - - assert !firm.unvalidated_account.valid? - assert firm.valid? - assert firm.save - end - - def test_assignment_before_either_saved - firm = Firm.new("name" => "GlobalMegaCorp") - firm.account = a = Account.new("credit_limit" => 1000) - assert firm.new_record? - assert a.new_record? - assert_equal a, firm.account - assert firm.save - assert !firm.new_record? - assert !a.new_record? - assert_equal a, firm.account - assert_equal a, firm.account(true) - end - - def test_not_resaved_when_unchanged - firm = Firm.find(:first, :include => :account) - firm.name += '-changed' - assert_queries(1) { firm.save! } - - firm = Firm.find(:first) - firm.account = Account.find(:first) - assert_queries(Firm.partial_updates? ? 0 : 1) { firm.save! } - - firm = Firm.find(:first).clone - firm.account = Account.find(:first) - assert_queries(2) { firm.save! } - - firm = Firm.find(:first).clone - firm.account = Account.find(:first).clone - assert_queries(2) { firm.save! } - end def test_save_still_works_after_accessing_nil_has_one jp = Company.new :name => 'Jaded Pixel' diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 381249c0c27e1..6ced84e0b6cc4 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -1,10 +1,20 @@ -require "cases/helper" -require "models/pirate" -require "models/ship" -require "models/ship_part" -require "models/bird" -require "models/parrot" -require "models/treasure" +require 'cases/helper' +require 'models/bird' +require 'models/company' +require 'models/customer' +require 'models/developer' +require 'models/order' +require 'models/parrot' +require 'models/person' +require 'models/pirate' +require 'models/post' +require 'models/reader' +require 'models/ship' +require 'models/ship_part' +require 'models/treasure' + +# TODO: +# - add test case for new parent and children with invalid data and saving with validate = false class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase def test_autosave_should_be_a_valid_option_for_has_one @@ -30,6 +40,383 @@ def base end end +class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase + def test_save_fails_for_invalid_has_one + firm = Firm.find(:first) + assert firm.valid? + + firm.account = Account.new + + assert !firm.account.valid? + assert !firm.valid? + assert !firm.save + assert_equal "is invalid", firm.errors.on("account") + end + + def test_save_succeeds_for_invalid_has_one_with_validate_false + firm = Firm.find(:first) + assert firm.valid? + + firm.unvalidated_account = Account.new + + assert !firm.unvalidated_account.valid? + assert firm.valid? + assert firm.save + end + + def test_build_before_child_saved + firm = Firm.find(1) + + account = firm.account.build("credit_limit" => 1000) + assert_equal account, firm.account + assert account.new_record? + assert firm.save + assert_equal account, firm.account + assert !account.new_record? + end + + def test_build_before_either_saved + firm = Firm.new("name" => "GlobalMegaCorp") + + firm.account = account = Account.new("credit_limit" => 1000) + assert_equal account, firm.account + assert account.new_record? + assert firm.save + assert_equal account, firm.account + assert !account.new_record? + end + + def test_assignment_before_parent_saved + firm = Firm.new("name" => "GlobalMegaCorp") + firm.account = a = Account.find(1) + assert firm.new_record? + assert_equal a, firm.account + assert firm.save + assert_equal a, firm.account + assert_equal a, firm.account(true) + end + + def test_assignment_before_either_saved + firm = Firm.new("name" => "GlobalMegaCorp") + firm.account = a = Account.new("credit_limit" => 1000) + assert firm.new_record? + assert a.new_record? + assert_equal a, firm.account + assert firm.save + assert !firm.new_record? + assert !a.new_record? + assert_equal a, firm.account + assert_equal a, firm.account(true) + end + + def test_not_resaved_when_unchanged + firm = Firm.find(:first, :include => :account) + firm.name += '-changed' + assert_queries(1) { firm.save! } + + firm = Firm.find(:first) + firm.account = Account.find(:first) + assert_queries(Firm.partial_updates? ? 0 : 1) { firm.save! } + + firm = Firm.find(:first).clone + firm.account = Account.find(:first) + assert_queries(2) { firm.save! } + + firm = Firm.find(:first).clone + firm.account = Account.find(:first).clone + assert_queries(2) { firm.save! } + end +end + +class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase + def test_save_fails_for_invalid_belongs_to + assert log = AuditLog.create(:developer_id => 0, :message => "") + + log.developer = Developer.new + assert !log.developer.valid? + assert !log.valid? + assert !log.save + assert_equal "is invalid", log.errors.on("developer") + end + + def test_save_succeeds_for_invalid_belongs_to_with_validate_false + assert log = AuditLog.create(:developer_id => 0, :message=> "") + + log.unvalidated_developer = Developer.new + assert !log.unvalidated_developer.valid? + assert log.valid? + assert log.save + end + + def test_assignment_before_parent_saved + client = Client.find(:first) + apple = Firm.new("name" => "Apple") + client.firm = apple + assert_equal apple, client.firm + assert apple.new_record? + assert client.save + assert apple.save + assert !apple.new_record? + assert_equal apple, client.firm + assert_equal apple, client.firm(true) + end + + def test_assignment_before_either_saved + final_cut = Client.new("name" => "Final Cut") + apple = Firm.new("name" => "Apple") + final_cut.firm = apple + assert final_cut.new_record? + assert apple.new_record? + assert final_cut.save + assert !final_cut.new_record? + assert !apple.new_record? + assert_equal apple, final_cut.firm + assert_equal apple, final_cut.firm(true) + end + + def test_store_two_association_with_one_save + num_orders = Order.count + num_customers = Customer.count + order = Order.new + + customer1 = order.billing = Customer.new + customer2 = order.shipping = Customer.new + assert order.save + assert_equal customer1, order.billing + assert_equal customer2, order.shipping + + order.reload + + assert_equal customer1, order.billing + assert_equal customer2, order.shipping + + assert_equal num_orders +1, Order.count + assert_equal num_customers +2, Customer.count + end + + def test_store_association_in_two_relations_with_one_save + num_orders = Order.count + num_customers = Customer.count + order = Order.new + + customer = order.billing = order.shipping = Customer.new + assert order.save + assert_equal customer, order.billing + assert_equal customer, order.shipping + + order.reload + + assert_equal customer, order.billing + assert_equal customer, order.shipping + + assert_equal num_orders +1, Order.count + assert_equal num_customers +1, Customer.count + end + + def test_store_association_in_two_relations_with_one_save_in_existing_object + num_orders = Order.count + num_customers = Customer.count + order = Order.create + + customer = order.billing = order.shipping = Customer.new + assert order.save + assert_equal customer, order.billing + assert_equal customer, order.shipping + + order.reload + + assert_equal customer, order.billing + assert_equal customer, order.shipping + + assert_equal num_orders +1, Order.count + assert_equal num_customers +1, Customer.count + end + + def test_store_association_in_two_relations_with_one_save_in_existing_object_with_values + num_orders = Order.count + num_customers = Customer.count + order = Order.create + + customer = order.billing = order.shipping = Customer.new + assert order.save + assert_equal customer, order.billing + assert_equal customer, order.shipping + + order.reload + + customer = order.billing = order.shipping = Customer.new + + assert order.save + order.reload + + assert_equal customer, order.billing + assert_equal customer, order.shipping + + assert_equal num_orders +1, Order.count + assert_equal num_customers +2, Customer.count + end +end + +class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase + fixtures :companies, :people + + def test_invalid_adding + firm = Firm.find(1) + assert !(firm.clients_of_firm << c = Client.new) + assert c.new_record? + assert !firm.valid? + assert !firm.save + assert c.new_record? + end + + def test_invalid_adding_before_save + no_of_firms = Firm.count + no_of_clients = Client.count + new_firm = Firm.new("name" => "A New Firm, Inc") + new_firm.clients_of_firm.concat([c = Client.new, Client.new("name" => "Apple")]) + assert c.new_record? + assert !c.valid? + assert !new_firm.valid? + assert !new_firm.save + assert c.new_record? + assert new_firm.new_record? + end + + def test_invalid_adding_with_validate_false + firm = Firm.find(:first) + client = Client.new + firm.unvalidated_clients_of_firm << client + + assert firm.valid? + assert !client.valid? + assert firm.save + assert client.new_record? + end + + def test_valid_adding_with_validate_false + no_of_clients = Client.count + + firm = Firm.find(:first) + client = Client.new("name" => "Apple") + + assert firm.valid? + assert client.valid? + assert client.new_record? + + firm.unvalidated_clients_of_firm << client + + assert firm.save + assert !client.new_record? + assert_equal no_of_clients+1, Client.count + end + + def test_invalid_build + new_client = companies(:first_firm).clients_of_firm.build + assert new_client.new_record? + assert !new_client.valid? + assert_equal new_client, companies(:first_firm).clients_of_firm.last + assert !companies(:first_firm).save + assert new_client.new_record? + assert_equal 1, companies(:first_firm).clients_of_firm(true).size + end + + def test_adding_before_save + no_of_firms = Firm.count + no_of_clients = Client.count + + new_firm = Firm.new("name" => "A New Firm, Inc") + c = Client.new("name" => "Apple") + + new_firm.clients_of_firm.push Client.new("name" => "Natural Company") + assert_equal 1, new_firm.clients_of_firm.size + new_firm.clients_of_firm << c + assert_equal 2, new_firm.clients_of_firm.size + + assert_equal no_of_firms, Firm.count # Firm was not saved to database. + assert_equal no_of_clients, Client.count # Clients were not saved to database. + assert new_firm.save + assert !new_firm.new_record? + assert !c.new_record? + assert_equal new_firm, c.firm + assert_equal no_of_firms+1, Firm.count # Firm was saved to database. + assert_equal no_of_clients+2, Client.count # Clients were saved to database. + + assert_equal 2, new_firm.clients_of_firm.size + assert_equal 2, new_firm.clients_of_firm(true).size + end + + def test_assign_ids + firm = Firm.new("name" => "Apple") + firm.client_ids = [companies(:first_client).id, companies(:second_client).id] + firm.save + firm.reload + assert_equal 2, firm.clients.length + assert firm.clients.include?(companies(:second_client)) + end + + def test_assign_ids_for_through_a_belongs_to + post = Post.new(:title => "Assigning IDs works!", :body => "You heared it here first, folks!") + post.person_ids = [people(:david).id, people(:michael).id] + post.save + post.reload + assert_equal 2, post.people.length + assert post.people.include?(people(:david)) + end + + def test_build_before_save + company = companies(:first_firm) + new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") } + assert !company.clients_of_firm.loaded? + + company.name += '-changed' + assert_queries(2) { assert company.save } + assert !new_client.new_record? + assert_equal 2, company.clients_of_firm(true).size + end + + def test_build_many_before_save + company = companies(:first_firm) + new_clients = assert_no_queries { company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) } + + company.name += '-changed' + assert_queries(3) { assert company.save } + assert_equal 3, company.clients_of_firm(true).size + end + + def test_build_via_block_before_save + company = companies(:first_firm) + new_client = assert_no_queries { company.clients_of_firm.build {|client| client.name = "Another Client" } } + assert !company.clients_of_firm.loaded? + + company.name += '-changed' + assert_queries(2) { assert company.save } + assert !new_client.new_record? + assert_equal 2, company.clients_of_firm(true).size + end + + def test_build_many_via_block_before_save + company = companies(:first_firm) + new_clients = assert_no_queries do + company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) do |client| + client.name = "changed" + end + end + + company.name += '-changed' + assert_queries(3) { assert company.save } + assert_equal 3, company.clients_of_firm(true).size + end + + def test_replace_on_new_object + firm = Firm.new("name" => "New Firm") + firm.clients = [companies(:second_client), Client.new("name" => "New Client")] + assert firm.save + firm.reload + assert_equal 2, firm.clients.length + assert firm.clients.include?(Client.find_by_name("New Client")) + end +end + class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase self.use_transactional_fixtures = false @@ -181,6 +568,14 @@ def test_should_automatically_validate_the_associated_model assert !@pirate.errors.on(:ship_name).blank? end + def test_should_merge_errors_on_the_associated_models_onto_the_parent_even_if_it_is_not_valid + @pirate.ship.name = nil + @pirate.catchphrase = nil + assert !@pirate.valid? + assert !@pirate.errors.on(:ship_name).blank? + assert !@pirate.errors.on(:catchphrase).blank? + end + def test_should_still_allow_to_bypass_validations_on_the_associated_model @pirate.catchphrase = '' @pirate.ship.name = '' @@ -263,6 +658,14 @@ def test_should_automatically_validate_the_associated_model assert !@ship.errors.on(:pirate_catchphrase).blank? end + def test_should_merge_errors_on_the_associated_model_onto_the_parent_even_if_it_is_not_valid + @ship.name = nil + @ship.pirate.catchphrase = nil + assert !@ship.valid? + assert !@ship.errors.on(:name).blank? + assert !@ship.errors.on(:pirate_catchphrase).blank? + end + def test_should_still_allow_to_bypass_validations_on_the_associated_model @ship.pirate.catchphrase = '' @ship.name = '' @@ -326,7 +729,24 @@ def test_should_automatically_validate_the_associated_models assert @pirate.errors.on(@association_name).blank? end - def test_should_still_allow_to_bypass_validations_on_the_associated_models + def test_should_not_use_default_invalid_error_on_associated_models + @pirate.send(@association_name).build(:name => '') + + assert !@pirate.valid? + assert_equal "can't be blank", @pirate.errors.on("#{@association_name}_name") + assert @pirate.errors.on(@association_name).blank? + end + + def test_should_merge_errors_on_the_associated_models_onto_the_parent_even_if_it_is_not_valid + @pirate.send(@association_name).each { |child| child.name = '' } + @pirate.catchphrase = nil + + assert !@pirate.valid? + assert_equal "can't be blank", @pirate.errors.on("#{@association_name}_name") + assert !@pirate.errors.on(:catchphrase).blank? + end + + def test_should_allow_to_bypass_validations_on_the_associated_models_on_update @pirate.catchphrase = '' @pirate.send(@association_name).each { |child| child.name = '' } @@ -338,6 +758,20 @@ def test_should_still_allow_to_bypass_validations_on_the_associated_models ] end + def test_should_validation_the_associated_models_on_create + assert_no_difference("#{ @association_name == :birds ? 'Bird' : 'Parrot' }.count") do + 2.times { @pirate.send(@association_name).build } + @pirate.save(true) + end + end + + def test_should_allow_to_bypass_validations_on_the_associated_models_on_create + assert_difference("#{ @association_name == :birds ? 'Bird' : 'Parrot' }.count", +2) do + 2.times { @pirate.send(@association_name).build } + @pirate.save(false) + end + end + def test_should_rollback_any_changes_if_an_exception_occurred_while_saving before = [@pirate.catchphrase, *@pirate.send(@association_name).map(&:name)] new_names = ['Grace OMalley', 'Privateers Greed'] From 3d1d422b8be027417cb640416d154171e75f03d1 Mon Sep 17 00:00:00 2001 From: Eloy Duran Date: Thu, 26 Feb 2009 12:26:17 +0100 Subject: [PATCH 2/8] Pass a custom form builder on to nested fields_for calls. [#2023 status:committed] Signed-off-by: David Heinemeier Hansson --- actionpack/CHANGELOG | 2 + .../lib/action_view/helpers/form_helper.rb | 132 +++++++++++------- actionpack/test/template/form_helper_test.rb | 41 ++++++ 3 files changed, 126 insertions(+), 49 deletions(-) diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index 546adeb61db64..3038d2b60b0d1 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,5 +1,7 @@ *Edge* +* Fixed that passing a custom form builder would be forwarded to nested fields_for calls #2023 [Eloy Duran/Nate Wiger] + * Added partial scoping to TranslationHelper#translate, so if you call translate(".foo") from the people/index.html.erb template, you'll actually be calling I18n.translate("people.index.foo") [DHH] * Fix a syntax error in current_page?() that was prevent matches against URL's with multiple query parameters #1385, #1868 [chris finne/Andrew White] diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index 4fef2b443e15a..a589bcba2aeef 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -5,17 +5,24 @@ module ActionView module Helpers - # Form helpers are designed to make working with models much easier compared to using just standard HTML - # elements by providing a set of methods for creating forms based on your models. This helper generates the HTML - # for forms, providing a method for each sort of input (e.g., text, password, select, and so on). When the form - # is submitted (i.e., when the user hits the submit button or form.submit is called via JavaScript), the form inputs will be bundled into the params object and passed back to the controller. + # Form helpers are designed to make working with models much easier + # compared to using just standard HTML elements by providing a set of + # methods for creating forms based on your models. This helper generates + # the HTML for forms, providing a method for each sort of input + # (e.g., text, password, select, and so on). When the form is submitted + # (i.e., when the user hits the submit button or form.submit is + # called via JavaScript), the form inputs will be bundled into the + # params object and passed back to the controller. # - # There are two types of form helpers: those that specifically work with model attributes and those that don't. - # This helper deals with those that work with model attributes; to see an example of form helpers that don't work - # with model attributes, check the ActionView::Helpers::FormTagHelper documentation. + # There are two types of form helpers: those that specifically work with + # model attributes and those that don't. This helper deals with those that + # work with model attributes; to see an example of form helpers that don't + # work with model attributes, check the ActionView::Helpers::FormTagHelper + # documentation. # - # The core method of this helper, form_for, gives you the ability to create a form for a model instance; - # for example, let's say that you have a model Person and want to create a new instance of it: + # The core method of this helper, form_for, gives you the ability to create + # a form for a model instance; for example, let's say that you have a model + # Person and want to create a new instance of it: # # # Note: a @person variable will have been created in the controller. # # For example: @person = Person.new @@ -40,17 +47,22 @@ module Helpers # <%= submit_tag 'Create' %> # <% end %> # - # This example will render the people/_form partial, setting a local variable called form which references the yielded FormBuilder. - # - # The params object created when this form is submitted would look like: + # This example will render the people/_form partial, setting a + # local variable called form which references the yielded + # FormBuilder. The params object created when this form is + # submitted would look like: # # {"action"=>"create", "controller"=>"persons", "person"=>{"first_name"=>"William", "last_name"=>"Smith"}} # - # The params hash has a nested person value, which can therefore be accessed with params[:person] in the controller. - # If were editing/updating an instance (e.g., Person.find(1) rather than Person.new in the controller), the objects - # attribute values are filled into the form (e.g., the person_first_name field would have that person's first name in it). + # The params hash has a nested person value, which can therefore + # be accessed with params[:person] in the controller. If were + # editing/updating an instance (e.g., Person.find(1) rather than + # Person.new in the controller), the objects attribute values are + # filled into the form (e.g., the person_first_name field would + # have that person's first name in it). # - # If the object name contains square brackets the id for the object will be inserted. For example: + # If the object name contains square brackets the id for the object will be + # inserted. For example: # # <%= text_field "person[]", "name" %> # @@ -58,8 +70,10 @@ module Helpers # # # - # If the helper is being used to generate a repetitive sequence of similar form elements, for example in a partial - # used by render_collection_of_partials, the index option may come in handy. Example: + # If the helper is being used to generate a repetitive sequence of similar + # form elements, for example in a partial used by + # render_collection_of_partials, the index option may + # come in handy. Example: # # <%= text_field "person", "name", "index" => 1 %> # @@ -67,14 +81,17 @@ module Helpers # # # - # An index option may also be passed to form_for and fields_for. This automatically applies - # the index to all the nested fields. + # An index option may also be passed to form_for and + # fields_for. This automatically applies the index to + # all the nested fields. # - # There are also methods for helping to build form tags in link:classes/ActionView/Helpers/FormOptionsHelper.html, - # link:classes/ActionView/Helpers/DateHelper.html, and link:classes/ActionView/Helpers/ActiveRecordHelper.html + # There are also methods for helping to build form tags in + # link:classes/ActionView/Helpers/FormOptionsHelper.html, + # link:classes/ActionView/Helpers/DateHelper.html, and + # link:classes/ActionView/Helpers/ActiveRecordHelper.html module FormHelper - # Creates a form and a scope around a specific model object that is used as - # a base for questioning about values for the fields. + # Creates a form and a scope around a specific model object that is used + # as a base for questioning about values for the fields. # # Rails provides succinct resource-oriented form generation with +form_for+ # like this: @@ -86,13 +103,15 @@ module FormHelper # <%= f.text_field :author %>
# <% end %> # - # There, +form_for+ is able to generate the rest of RESTful form parameters - # based on introspection on the record, but to understand what it does we - # need to dig first into the alternative generic usage it is based upon. + # There, +form_for+ is able to generate the rest of RESTful form + # parameters based on introspection on the record, but to understand what + # it does we need to dig first into the alternative generic usage it is + # based upon. # # === Generic form_for # - # The generic way to call +form_for+ yields a form builder around a model: + # The generic way to call +form_for+ yields a form builder around a + # model: # # <% form_for :person, :url => { :action => "update" } do |f| %> # <%= f.error_messages %> @@ -103,8 +122,8 @@ module FormHelper # <% end %> # # There, the first argument is a symbol or string with the name of the - # object the form is about, and also the name of the instance variable the - # object is stored in. + # object the form is about, and also the name of the instance variable + # the object is stored in. # # The form builder acts as a regular form helper that somehow carries the # model. Thus, the idea is that @@ -137,17 +156,18 @@ module FormHelper # In any of its variants, the rightmost argument to +form_for+ is an # optional hash of options: # - # * :url - The URL the form is submitted to. It takes the same fields - # you pass to +url_for+ or +link_to+. In particular you may pass here a - # named route directly as well. Defaults to the current action. + # * :url - The URL the form is submitted to. It takes the same + # fields you pass to +url_for+ or +link_to+. In particular you may pass + # here a named route directly as well. Defaults to the current action. # * :html - Optional HTML attributes for the form tag. # - # Worth noting is that the +form_for+ tag is called in a ERb evaluation block, - # not an ERb output block. So that's <% %>, not <%= %>. + # Worth noting is that the +form_for+ tag is called in a ERb evaluation + # block, not an ERb output block. So that's <% %>, not + # <%= %>. # # Also note that +form_for+ doesn't create an exclusive scope. It's still - # possible to use both the stand-alone FormHelper methods and methods from - # FormTagHelper. For example: + # possible to use both the stand-alone FormHelper methods and methods + # from FormTagHelper. For example: # # <% form_for :person, @person, :url => { :action => "update" } do |f| %> # First name: <%= f.text_field :first_name %> @@ -156,16 +176,16 @@ module FormHelper # Admin? : <%= check_box_tag "person[admin]", @person.company.admin? %> # <% end %> # - # This also works for the methods in FormOptionHelper and DateHelper that are - # designed to work with an object as base, like FormOptionHelper#collection_select - # and DateHelper#datetime_select. + # This also works for the methods in FormOptionHelper and DateHelper that + # are designed to work with an object as base, like + # FormOptionHelper#collection_select and DateHelper#datetime_select. # # === Resource-oriented style # - # As we said above, in addition to manually configuring the +form_for+ call, - # you can rely on automated resource identification, which will use the conventions - # and named routes of that approach. This is the preferred way to use +form_for+ - # nowadays. + # As we said above, in addition to manually configuring the +form_for+ + # call, you can rely on automated resource identification, which will use + # the conventions and named routes of that approach. This is the + # preferred way to use +form_for+ nowadays. # # For example, if @post is an existing record you want to edit # @@ -205,8 +225,10 @@ module FormHelper # # === Customized form builders # - # You can also build forms using a customized FormBuilder class. Subclass FormBuilder and override or define some more helpers, - # then use your custom builder. For example, let's say you made a helper to automatically add labels to form inputs. + # You can also build forms using a customized FormBuilder class. Subclass + # FormBuilder and override or define some more helpers, then use your + # custom builder. For example, let's say you made a helper to + # automatically add labels to form inputs. # # <% form_for :person, @person, :url => { :action => "update" }, :builder => LabellingFormBuilder do |f| %> # <%= f.text_field :first_name %> @@ -219,16 +241,23 @@ module FormHelper # # <%= render :partial => f %> # - # The rendered template is people/_labelling_form and the local variable referencing the form builder is called labelling_form. + # The rendered template is people/_labelling_form and the local + # variable referencing the form builder is called + # labelling_form. + # + # The custom FormBuilder class is automatically merged with the options + # of a nested fields_for call, unless it's explicitely set. # - # In many cases you will want to wrap the above in another helper, so you could do something like the following: + # In many cases you will want to wrap the above in another helper, so you + # could do something like the following: # # def labelled_form_for(record_or_name_or_array, *args, &proc) # options = args.extract_options! # form_for(record_or_name_or_array, *(args << options.merge(:builder => LabellingFormBuilder)), &proc) # end # - # If you don't need to attach a form to a model instance, then check out FormTagHelper#form_tag. + # If you don't need to attach a form to a model instance, then check out + # FormTagHelper#form_tag. def form_for(record_or_name_or_array, *args, &proc) raise ArgumentError, "Missing block" unless block_given? @@ -910,6 +939,11 @@ def fields_for(record_or_name_or_array, *args, &block) index = "" end + if options[:builder] + args << {} unless args.last.is_a?(Hash) + args.last[:builder] ||= options[:builder] + end + case record_or_name_or_array when String, Symbol if nested_attributes_association?(record_or_name_or_array) diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb index 5cc81b4afb5b5..654eee40a3022 100644 --- a/actionpack/test/template/form_helper_test.rb +++ b/actionpack/test/template/form_helper_test.rb @@ -1001,6 +1001,47 @@ def test_fields_for_with_labelled_builder assert_dom_equal expected, output_buffer end + def test_form_for_with_labelled_builder_with_nested_fields_for_without_options_hash + klass = nil + + form_for(:post, @post, :builder => LabelledFormBuilder) do |f| + f.fields_for(:comments, Comment.new) do |nested_fields| + klass = nested_fields.class + '' + end + end + + assert_equal LabelledFormBuilder, klass + end + + def test_form_for_with_labelled_builder_with_nested_fields_for_with_options_hash + klass = nil + + form_for(:post, @post, :builder => LabelledFormBuilder) do |f| + f.fields_for(:comments, Comment.new, :index => 'foo') do |nested_fields| + klass = nested_fields.class + '' + end + end + + assert_equal LabelledFormBuilder, klass + end + + class LabelledFormBuilderSubclass < LabelledFormBuilder; end + + def test_form_for_with_labelled_builder_with_nested_fields_for_with_custom_builder + klass = nil + + form_for(:post, @post, :builder => LabelledFormBuilder) do |f| + f.fields_for(:comments, Comment.new, :builder => LabelledFormBuilderSubclass) do |nested_fields| + klass = nested_fields.class + '' + end + end + + assert_equal LabelledFormBuilderSubclass, klass + end + def test_form_for_with_html_options_adds_options_to_form_tag form_for(:post, @post, :html => {:id => 'some_form', :class => 'some_class'}) do |f| end expected = "
" From e2b925fa68203607fb4f9a9aa457c0435c969f9a Mon Sep 17 00:00:00 2001 From: Eloy Duran Date: Thu, 26 Feb 2009 11:56:27 +0100 Subject: [PATCH 3/8] Don't validate records of an :autosave association if they're marked for destruction. [#2064 status:resolved] Signed-off-by: David Heinemeier Hansson --- .../lib/active_record/autosave_association.rb | 11 ++++--- .../test/cases/autosave_association_test.rb | 30 +++++++++++++++++-- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 6e09b13150fa0..1c3d0567c1bb1 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -243,13 +243,16 @@ def validate_collection_association(reflection) end # Returns whether or not the association is valid and applies any errors to - # the parent, self, if it wasn't. + # the parent, self, if it wasn't. Skips any :autosave + # enabled records if they're marked_for_destruction?. def association_valid?(reflection, association) unless valid = association.valid? if reflection.options[:autosave] - association.errors.each do |attribute, message| - attribute = "#{reflection.name}_#{attribute}" - errors.add(attribute, message) unless errors.on(attribute) + unless association.marked_for_destruction? + association.errors.each do |attribute, message| + attribute = "#{reflection.name}_#{attribute}" + errors.add(attribute, message) unless errors.on(attribute) + end end else errors.add(reflection.name) diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 6ced84e0b6cc4..b179bd827a57c 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -13,9 +13,6 @@ require 'models/ship_part' require 'models/treasure' -# TODO: -# - add test case for new parent and children with invalid data and saving with validate = false - class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase def test_autosave_should_be_a_valid_option_for_has_one assert base.valid_keys_for_has_one_association.include?(:autosave) @@ -449,6 +446,14 @@ def test_should_destroy_a_child_association_as_part_of_the_save_transaction_if_i assert_nil Ship.find_by_id(id) end + def test_should_skip_validation_on_a_child_association_if_marked_for_destruction + @pirate.ship.name = '' + assert !@pirate.valid? + + @pirate.ship.mark_for_destruction + assert_difference('Ship.count', -1) { @pirate.save! } + end + def test_should_rollback_destructions_if_an_exception_occurred_while_saving_a_child # Stub the save method of the @pirate.ship instance to destroy and then raise an exception class << @pirate.ship @@ -478,6 +483,14 @@ def test_should_destroy_a_parent_association_as_part_of_the_save_transaction_if_ assert_nil Pirate.find_by_id(id) end + def test_should_skip_validation_on_a_parent_association_if_marked_for_destruction + @ship.pirate.catchphrase = '' + assert !@ship.valid? + + @ship.pirate.mark_for_destruction + assert_difference('Pirate.count', -1) { @ship.save! } + end + def test_should_rollback_destructions_if_an_exception_occurred_while_saving_a_parent # Stub the save method of the @ship.pirate instance to destroy and then raise an exception class << @ship.pirate @@ -511,6 +524,17 @@ def save(*args) ids.each { |id| assert_nil klass.find_by_id(id) } end + define_method("test_should_skip_validation_on_the_#{association_name}_association_if_marked_for_destruction") do + 2.times { |i| @pirate.send(association_name).create!(:name => "#{association_name}_#{i}") } + children = @pirate.send(association_name) + + children.each { |child| child.name = '' } + assert !@pirate.valid? + + children.each { |child| child.mark_for_destruction } + assert_difference("#{association_name.classify}.count", -2) { @pirate.save! } + end + define_method("test_should_rollback_destructions_if_an_exception_occurred_while_saving_#{association_name}") do 2.times { |i| @pirate.send(association_name).create!(:name => "#{association_name}_#{i}") } before = @pirate.send(association_name).map { |c| c } From 6de83562f91028629bd24447aa521bc72ef8277a Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 27 Feb 2009 14:22:39 +0100 Subject: [PATCH 4/8] Force all internal calls to Array#to_sentence to use English [#2010 state:resolved] --- actionpack/lib/action_controller/base.rb | 4 ++-- actionpack/lib/action_controller/request.rb | 2 +- activerecord/lib/active_record/associations.rb | 2 +- .../lib/active_support/core_ext/array/conversions.rb | 5 ++--- activesupport/lib/active_support/duration.rb | 2 +- railties/lib/rails/plugin/loader.rb | 2 +- railties/lib/tasks/testing.rake | 2 +- 7 files changed, 9 insertions(+), 10 deletions(-) diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 1eda6e3f04dee..5df94b289a45f 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -22,7 +22,7 @@ class MethodNotAllowed < ActionControllerError #:nodoc: attr_reader :allowed_methods def initialize(*allowed_methods) - super("Only #{allowed_methods.to_sentence} requests are allowed.") + super("Only #{allowed_methods.to_sentence(:locale => :en)} requests are allowed.") @allowed_methods = allowed_methods end @@ -1298,7 +1298,7 @@ def perform_action rescue ActionView::MissingTemplate => e # Was the implicit template missing, or was it another template? if e.path == default_template_name - raise UnknownAction, "No action responded to #{action_name}. Actions: #{action_methods.sort.to_sentence}", caller + raise UnknownAction, "No action responded to #{action_name}. Actions: #{action_methods.sort.to_sentence(:locale => :en)}", caller else raise e end diff --git a/actionpack/lib/action_controller/request.rb b/actionpack/lib/action_controller/request.rb index 0e95cfc147de8..2cabab9ec846d 100755 --- a/actionpack/lib/action_controller/request.rb +++ b/actionpack/lib/action_controller/request.rb @@ -32,7 +32,7 @@ def key?(key) # :get. If the request \method is not listed in the HTTP_METHODS # constant above, an UnknownHttpMethod exception is raised. def request_method - @request_method ||= HTTP_METHOD_LOOKUP[super] || raise(UnknownHttpMethod, "#{super}, accepted HTTP methods are #{HTTP_METHODS.to_sentence}") + @request_method ||= HTTP_METHOD_LOOKUP[super] || raise(UnknownHttpMethod, "#{super}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}") end # Returns the HTTP request \method used for action processing as a diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index b6f87a0486199..6e88c89737ebd 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -22,7 +22,7 @@ def initialize(reflection) through_reflection = reflection.through_reflection source_reflection_names = reflection.source_reflection_names source_associations = reflection.through_reflection.klass.reflect_on_all_associations.collect { |a| a.name.inspect } - super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence :two_words_connector => ' or ', :last_word_connector => ', or '} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => '. Is it one of #{source_associations.to_sentence :two_words_connector => ' or ', :last_word_connector => ', or '}?") + super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => '. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?") end end diff --git a/activesupport/lib/active_support/core_ext/array/conversions.rb b/activesupport/lib/active_support/core_ext/array/conversions.rb index a9b50ca92df8e..ba8e022fb272d 100644 --- a/activesupport/lib/active_support/core_ext/array/conversions.rb +++ b/activesupport/lib/active_support/core_ext/array/conversions.rb @@ -7,10 +7,9 @@ module Conversions # * :two_words_connector - The sign or word used to join the elements in arrays with two elements (default: " and ") # * :last_word_connector - The sign or word used to join the last element in arrays with three or more elements (default: ", and ") def to_sentence(options = {}) - - default_words_connector = I18n.translate(:'support.array.words_connector', :locale => options[:locale]) + default_words_connector = I18n.translate(:'support.array.words_connector', :locale => options[:locale]) default_two_words_connector = I18n.translate(:'support.array.two_words_connector', :locale => options[:locale]) - default_last_word_connector = I18n.translate(:'support.array.last_word_connector', :locale => options[:locale]) + default_last_word_connector = I18n.translate(:'support.array.last_word_connector', :locale => options[:locale]) # Try to emulate to_senteces previous to 2.3 if options.has_key?(:connector) || options.has_key?(:skip_last_comma) diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb index c41e86dfd19da..f64661c5b1861 100644 --- a/activesupport/lib/active_support/duration.rb +++ b/activesupport/lib/active_support/duration.rb @@ -70,7 +70,7 @@ def inspect #:nodoc: [:years, :months, :days, :minutes, :seconds].map do |length| n = consolidated[length] "#{n} #{n == 1 ? length.to_s.singularize : length.to_s}" if n.nonzero? - end.compact.to_sentence + end.compact.to_sentence(:locale => :en) end protected diff --git a/railties/lib/rails/plugin/loader.rb b/railties/lib/rails/plugin/loader.rb index 7f85bb89666ea..f86589a6893fd 100644 --- a/railties/lib/rails/plugin/loader.rb +++ b/railties/lib/rails/plugin/loader.rb @@ -178,7 +178,7 @@ def ensure_all_registered_plugins_are_loaded! if explicit_plugin_loading_order? if configuration.plugins.detect {|plugin| plugin != :all && !loaded?(plugin) } missing_plugins = configuration.plugins - (plugins.map{|p| p.name.to_sym} + [:all]) - raise LoadError, "Could not locate the following plugins: #{missing_plugins.to_sentence}" + raise LoadError, "Could not locate the following plugins: #{missing_plugins.to_sentence(:locale => :en)}" end end end diff --git a/railties/lib/tasks/testing.rake b/railties/lib/tasks/testing.rake index 4242458672422..fd5e52a05bc2a 100644 --- a/railties/lib/tasks/testing.rake +++ b/railties/lib/tasks/testing.rake @@ -48,7 +48,7 @@ task :test do task end end.compact - abort "Errors running #{errors.to_sentence}!" if errors.any? + abort "Errors running #{errors.to_sentence(:locale => :en)}!" if errors.any? end namespace :test do From 319106d09c0ec2daf8b5345f525f1c97b6368ce2 Mon Sep 17 00:00:00 2001 From: Matthew Rudy Jacobs Date: Sun, 22 Feb 2009 19:23:04 +0000 Subject: [PATCH 5/8] Metal can now line in plugins under app/metal [#2045 state:committed] Signed-off-by: David Heinemeier Hansson --- railties/CHANGELOG | 7 +++++++ railties/lib/initializer.rb | 2 ++ railties/lib/rails/plugin.rb | 8 ++++++-- railties/lib/rails/plugin/loader.rb | 5 ++++- railties/lib/rails/rack/metal.rb | 15 +++++++++------ .../engines/engine/app/metal/engine_metal.rb | 10 ++++++++++ railties/test/plugin_loader_test.rb | 4 ++-- 7 files changed, 40 insertions(+), 11 deletions(-) create mode 100644 railties/test/fixtures/plugins/engines/engine/app/metal/engine_metal.rb diff --git a/railties/CHANGELOG b/railties/CHANGELOG index 38c6f808e6370..a0d752b26350f 100644 --- a/railties/CHANGELOG +++ b/railties/CHANGELOG @@ -1,5 +1,12 @@ +*Edge* + +* Allow metal to live in plugins #2045 [Matthew Rudy] + + *2.3.0 [RC1] (February 1st, 2009)* +* Added metal [Josh Peek] + * Remove script/performance/request in favour of the performance integration tests. [Pratik Naik] To continue using script/performance/request, install the request_profiler plugin : diff --git a/railties/lib/initializer.rb b/railties/lib/initializer.rb index 09affe9e3615e..24ce3e75ff20d 100644 --- a/railties/lib/initializer.rb +++ b/railties/lib/initializer.rb @@ -559,6 +559,8 @@ def initialize_i18n end def initialize_metal + Rails::Rack::Metal.metal_paths += plugin_loader.engine_metal_paths + configuration.middleware.insert_before( :"ActionController::RewindableInput", Rails::Rack::Metal, :if => Rails::Rack::Metal.metals.any?) diff --git a/railties/lib/rails/plugin.rb b/railties/lib/rails/plugin.rb index 4901abe808b78..80deb73bbb5fc 100644 --- a/railties/lib/rails/plugin.rb +++ b/railties/lib/rails/plugin.rb @@ -80,6 +80,10 @@ def controller_path File.join(directory, 'app', 'controllers') end + def metal_path + File.join(directory, 'app', 'metal') + end + def routing_file File.join(directory, 'config', 'routes.rb') end @@ -100,7 +104,7 @@ def report_nonexistant_or_empty_plugin! def app_paths - [ File.join(directory, 'app', 'models'), File.join(directory, 'app', 'helpers'), controller_path ] + [ File.join(directory, 'app', 'models'), File.join(directory, 'app', 'helpers'), controller_path, metal_path ] end def lib_path @@ -160,4 +164,4 @@ def init_path File.join(directory, 'rails', 'init.rb') end end -end \ No newline at end of file +end diff --git a/railties/lib/rails/plugin/loader.rb b/railties/lib/rails/plugin/loader.rb index f86589a6893fd..66e01d70da045 100644 --- a/railties/lib/rails/plugin/loader.rb +++ b/railties/lib/rails/plugin/loader.rb @@ -65,6 +65,9 @@ def add_plugin_load_paths $LOAD_PATH.uniq! end + def engine_metal_paths + engines.collect(&:metal_path) + end protected def configure_engines @@ -185,4 +188,4 @@ def ensure_all_registered_plugins_are_loaded! end end -end \ No newline at end of file +end diff --git a/railties/lib/rails/rack/metal.rb b/railties/lib/rails/rack/metal.rb index b1852272345d4..8dac992ef2ab6 100644 --- a/railties/lib/rails/rack/metal.rb +++ b/railties/lib/rails/rack/metal.rb @@ -6,14 +6,17 @@ class Metal NotFoundResponse = [404, {}, []].freeze NotFound = lambda { NotFoundResponse } + cattr_accessor :metal_paths + self.metal_paths = ["#{Rails.root}/app/metal"] + def self.metals - base = "#{Rails.root}/app/metal" - matcher = /\A#{Regexp.escape(base)}\/(.*)\.rb\Z/ + matcher = /#{Regexp.escape('/app/metal/')}(.*)\.rb\Z/ + metal_glob = metal_paths.map{ |base| "#{base}/**/*.rb" } - Dir["#{base}/**/*.rb"].sort.map do |file| - file.sub!(matcher, '\1') - require file - file.classify.constantize + Dir[*metal_glob].sort.map do |file| + path = file.match(matcher)[1] + require path + path.classify.constantize end end diff --git a/railties/test/fixtures/plugins/engines/engine/app/metal/engine_metal.rb b/railties/test/fixtures/plugins/engines/engine/app/metal/engine_metal.rb new file mode 100644 index 0000000000000..d67a127ca7828 --- /dev/null +++ b/railties/test/fixtures/plugins/engines/engine/app/metal/engine_metal.rb @@ -0,0 +1,10 @@ +class EngineMetal + def self.call(env) + if env["PATH_INFO"] =~ /^\/metal/ + [200, {"Content-Type" => "text/html"}, ["Engine metal"]] + else + [404, {"Content-Type" => "text/html"}, ["Not Found"]] + end + end +end + diff --git a/railties/test/plugin_loader_test.rb b/railties/test/plugin_loader_test.rb index e802b1ace77d9..b270748dd6285 100644 --- a/railties/test/plugin_loader_test.rb +++ b/railties/test/plugin_loader_test.rb @@ -120,7 +120,7 @@ def test_should_add_engine_load_paths_to_Dependencies_load_paths @loader.add_plugin_load_paths - %w( models controllers helpers ).each do |app_part| + %w( models controllers metal helpers ).each do |app_part| assert ActiveSupport::Dependencies.load_paths.include?( File.join(plugin_fixture_path('engines/engine'), 'app', app_part) ), "Couldn't find #{app_part} in load path" @@ -161,4 +161,4 @@ def reset_load_path! $LOAD_PATH.clear ORIGINAL_LOAD_PATH.each { |path| $LOAD_PATH << path } end -end \ No newline at end of file +end From ff894b55dd787af1d6284567f2a898090aae64ca Mon Sep 17 00:00:00 2001 From: Zack Hobson Date: Fri, 13 Feb 2009 11:23:24 -0800 Subject: [PATCH 6/8] Update rdoc: all is not a named_scope [#1959 state:committed] Signed-off-by: David Heinemeier Hansson --- activerecord/lib/active_record/named_scope.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb index 022fdaf9b2ded..8a7449f95b5d7 100644 --- a/activerecord/lib/active_record/named_scope.rb +++ b/activerecord/lib/active_record/named_scope.rb @@ -1,11 +1,12 @@ module ActiveRecord module NamedScope - # All subclasses of ActiveRecord::Base have two named \scopes: - # * all - which is similar to a find(:all) query, and + # All subclasses of ActiveRecord::Base have one named scope: # * scoped - which allows for the creation of anonymous \scopes, on the fly: Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions) # # These anonymous \scopes tend to be useful when procedurally generating complex queries, where passing # intermediate values (scopes) around as first-class objects is convenient. + # + # You can define a scope that applies to all finders using ActiveRecord::Base.default_scope. def self.included(base) base.class_eval do extend ClassMethods From 77b0994c7835610982d708ce7ce5cd95e6e99e5a Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 27 Feb 2009 14:46:23 +0100 Subject: [PATCH 7/8] Prep for RC2 later today --- actionmailer/CHANGELOG | 2 +- actionmailer/Rakefile | 2 +- actionmailer/lib/action_mailer/version.rb | 2 +- actionpack/CHANGELOG | 2 +- actionpack/Rakefile | 2 +- actionpack/lib/action_pack/version.rb | 2 +- activerecord/CHANGELOG | 2 +- activerecord/Rakefile | 2 +- activerecord/lib/active_record/version.rb | 2 +- activeresource/CHANGELOG | 5 +++++ activeresource/Rakefile | 2 +- activeresource/lib/active_resource/version.rb | 2 +- activesupport/CHANGELOG | 2 +- activesupport/lib/active_support/version.rb | 2 +- railties/CHANGELOG | 2 +- railties/Rakefile | 10 +++++----- railties/lib/rails/version.rb | 2 +- 17 files changed, 25 insertions(+), 20 deletions(-) diff --git a/actionmailer/CHANGELOG b/actionmailer/CHANGELOG index 457e9aca94d1a..0749ff4dab662 100644 --- a/actionmailer/CHANGELOG +++ b/actionmailer/CHANGELOG @@ -1,4 +1,4 @@ -*Edge* +*2.3.1 [RC2] (February 27th, 2009)* * Fixed that ActionMailer should send correctly formatted Return-Path in MAIL FROM for SMTP #1842 [Matt Jones] diff --git a/actionmailer/Rakefile b/actionmailer/Rakefile index c3826e3a27718..7c27ef450fdcc 100644 --- a/actionmailer/Rakefile +++ b/actionmailer/Rakefile @@ -55,7 +55,7 @@ spec = Gem::Specification.new do |s| s.rubyforge_project = "actionmailer" s.homepage = "http://www.rubyonrails.org" - s.add_dependency('actionpack', '= 2.3.0' + PKG_BUILD) + s.add_dependency('actionpack', '= 2.3.1' + PKG_BUILD) s.has_rdoc = true s.requirements << 'none' diff --git a/actionmailer/lib/action_mailer/version.rb b/actionmailer/lib/action_mailer/version.rb index 9cd7a14b73ae6..ac843ae6aca9a 100644 --- a/actionmailer/lib/action_mailer/version.rb +++ b/actionmailer/lib/action_mailer/version.rb @@ -2,7 +2,7 @@ module ActionMailer module VERSION #:nodoc: MAJOR = 2 MINOR = 3 - TINY = 0 + TINY = 1 STRING = [MAJOR, MINOR, TINY].join('.') end diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index 3038d2b60b0d1..516f28142801d 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,4 +1,4 @@ -*Edge* +*2.3.1 [RC2] (February 27th, 2009)* * Fixed that passing a custom form builder would be forwarded to nested fields_for calls #2023 [Eloy Duran/Nate Wiger] diff --git a/actionpack/Rakefile b/actionpack/Rakefile index c389e5a8d6051..0d673c617dd2a 100644 --- a/actionpack/Rakefile +++ b/actionpack/Rakefile @@ -80,7 +80,7 @@ spec = Gem::Specification.new do |s| s.has_rdoc = true s.requirements << 'none' - s.add_dependency('activesupport', '= 2.3.0' + PKG_BUILD) + s.add_dependency('activesupport', '= 2.3.1' + PKG_BUILD) s.add_dependency('rack', '>= 0.9.0') s.require_path = 'lib' diff --git a/actionpack/lib/action_pack/version.rb b/actionpack/lib/action_pack/version.rb index f20e44a7d5c6f..f03a2a7605c70 100644 --- a/actionpack/lib/action_pack/version.rb +++ b/actionpack/lib/action_pack/version.rb @@ -2,7 +2,7 @@ module ActionPack #:nodoc: module VERSION #:nodoc: MAJOR = 2 MINOR = 3 - TINY = 0 + TINY = 1 STRING = [MAJOR, MINOR, TINY].join('.') end diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index fc6986912cc6a..231402c51c991 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,4 +1,4 @@ -*Edge* +*2.3.1 [RC2] (February 27th, 2009)* * Added ActiveRecord::Base.each and ActiveRecord::Base.find_in_batches for batch processing [DHH/Jamis Buck] diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 8b3b97bac4012..aec9b3825bfba 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -177,7 +177,7 @@ spec = Gem::Specification.new do |s| s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) } end - s.add_dependency('activesupport', '= 2.3.0' + PKG_BUILD) + s.add_dependency('activesupport', '= 2.3.1' + PKG_BUILD) s.files.delete FIXTURES_ROOT + "/fixture_database.sqlite" s.files.delete FIXTURES_ROOT + "/fixture_database_2.sqlite" diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb index 6ac4bdc905c33..809e91897e27e 100644 --- a/activerecord/lib/active_record/version.rb +++ b/activerecord/lib/active_record/version.rb @@ -2,7 +2,7 @@ module ActiveRecord module VERSION #:nodoc: MAJOR = 2 MINOR = 3 - TINY = 0 + TINY = 1 STRING = [MAJOR, MINOR, TINY].join('.') end diff --git a/activeresource/CHANGELOG b/activeresource/CHANGELOG index fcb5dd5f467b6..8f6101acd6199 100644 --- a/activeresource/CHANGELOG +++ b/activeresource/CHANGELOG @@ -1,3 +1,8 @@ +*2.3.1 [RC2] (February 27th, 2009)* + +* Nothing new, just included in 2.3.1 + + *2.3.0 [RC1] (February 1st, 2009)* * Nothing new, just included in 2.3.0 diff --git a/activeresource/Rakefile b/activeresource/Rakefile index 6af2e6cfba296..fc67b5208905b 100644 --- a/activeresource/Rakefile +++ b/activeresource/Rakefile @@ -67,7 +67,7 @@ spec = Gem::Specification.new do |s| s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) } end - s.add_dependency('activesupport', '= 2.3.0' + PKG_BUILD) + s.add_dependency('activesupport', '= 2.3.1' + PKG_BUILD) s.require_path = 'lib' s.autorequire = 'active_resource' diff --git a/activeresource/lib/active_resource/version.rb b/activeresource/lib/active_resource/version.rb index c420ac813ea8a..4ac7eb1c06e4a 100644 --- a/activeresource/lib/active_resource/version.rb +++ b/activeresource/lib/active_resource/version.rb @@ -2,7 +2,7 @@ module ActiveResource module VERSION #:nodoc: MAJOR = 2 MINOR = 3 - TINY = 0 + TINY = 1 STRING = [MAJOR, MINOR, TINY].join('.') end diff --git a/activesupport/CHANGELOG b/activesupport/CHANGELOG index 70bea27cd1741..5d9b77bbf501f 100644 --- a/activesupport/CHANGELOG +++ b/activesupport/CHANGELOG @@ -1,4 +1,4 @@ -*Edge* +*2.3.1 [RC2] (February 27th, 2009)* * Update bundled memcache-client from 1.5.0.5 to 1.6.4.99. See http://www.mikeperham.com/2009/02/15/memcache-client-performance/ [Mike Perham] diff --git a/activesupport/lib/active_support/version.rb b/activesupport/lib/active_support/version.rb index 3e2b29b327d8f..8b65fffa08595 100644 --- a/activesupport/lib/active_support/version.rb +++ b/activesupport/lib/active_support/version.rb @@ -2,7 +2,7 @@ module ActiveSupport module VERSION #:nodoc: MAJOR = 2 MINOR = 3 - TINY = 0 + TINY = 1 STRING = [MAJOR, MINOR, TINY].join('.') end diff --git a/railties/CHANGELOG b/railties/CHANGELOG index a0d752b26350f..962869f9e1195 100644 --- a/railties/CHANGELOG +++ b/railties/CHANGELOG @@ -1,4 +1,4 @@ -*Edge* +*2.3.1 [RC2] (February 27th, 2009)* * Allow metal to live in plugins #2045 [Matthew Rudy] diff --git a/railties/Rakefile b/railties/Rakefile index 05f767dc3b0a4..62dc62502f184 100644 --- a/railties/Rakefile +++ b/railties/Rakefile @@ -310,11 +310,11 @@ spec = Gem::Specification.new do |s| EOF s.add_dependency('rake', '>= 0.8.3') - s.add_dependency('activesupport', '= 2.3.0' + PKG_BUILD) - s.add_dependency('activerecord', '= 2.3.0' + PKG_BUILD) - s.add_dependency('actionpack', '= 2.3.0' + PKG_BUILD) - s.add_dependency('actionmailer', '= 2.3.0' + PKG_BUILD) - s.add_dependency('activeresource', '= 2.3.0' + PKG_BUILD) + s.add_dependency('activesupport', '= 2.3.1' + PKG_BUILD) + s.add_dependency('activerecord', '= 2.3.1' + PKG_BUILD) + s.add_dependency('actionpack', '= 2.3.1' + PKG_BUILD) + s.add_dependency('actionmailer', '= 2.3.1' + PKG_BUILD) + s.add_dependency('activeresource', '= 2.3.1' + PKG_BUILD) s.rdoc_options << '--exclude' << '.' s.has_rdoc = false diff --git a/railties/lib/rails/version.rb b/railties/lib/rails/version.rb index 9bb4b2a96d399..fd38705e75d33 100644 --- a/railties/lib/rails/version.rb +++ b/railties/lib/rails/version.rb @@ -2,7 +2,7 @@ module Rails module VERSION #:nodoc: MAJOR = 2 MINOR = 3 - TINY = 0 + TINY = 1 STRING = [MAJOR, MINOR, TINY].join('.') end From 943d64b1c857944666bff2572857abe866615274 Mon Sep 17 00:00:00 2001 From: Sven Fuchs Date: Fri, 27 Feb 2009 17:07:06 +0100 Subject: [PATCH 8/8] Vendorize i18n 0.1.3 gem (fixes issues with incompatible character encodings in Ruby 1.9) (Akira Matsuda) [#2038 state:committed] Signed-off-by: David Heinemeier Hansson --- activesupport/CHANGELOG | 2 + activesupport/lib/active_support/vendor.rb | 4 +- .../vendor/i18n-0.1.1/.gitignore | 3 -- .../{i18n-0.1.1 => i18n-0.1.3}/MIT-LICENSE | 0 .../{i18n-0.1.1 => i18n-0.1.3}/README.textile | 0 .../{i18n-0.1.1 => i18n-0.1.3}/Rakefile | 0 .../{i18n-0.1.1 => i18n-0.1.3}/i18n.gemspec | 4 +- .../{i18n-0.1.1 => i18n-0.1.3}/lib/i18n.rb | 0 .../lib/i18n/backend/simple.rb | 10 +--- .../lib/i18n/exceptions.rb | 0 .../{i18n-0.1.1 => i18n-0.1.3}/test/all.rb | 0 .../test/i18n_exceptions_test.rb | 0 .../test/i18n_test.rb | 0 .../test/locale/en.rb | 0 .../test/locale/en.yml | 0 .../test/simple_backend_test.rb | 46 ++++++++++++++++++- 16 files changed, 52 insertions(+), 17 deletions(-) delete mode 100644 activesupport/lib/active_support/vendor/i18n-0.1.1/.gitignore rename activesupport/lib/active_support/vendor/{i18n-0.1.1 => i18n-0.1.3}/MIT-LICENSE (100%) rename activesupport/lib/active_support/vendor/{i18n-0.1.1 => i18n-0.1.3}/README.textile (100%) rename activesupport/lib/active_support/vendor/{i18n-0.1.1 => i18n-0.1.3}/Rakefile (100%) rename activesupport/lib/active_support/vendor/{i18n-0.1.1 => i18n-0.1.3}/i18n.gemspec (94%) rename activesupport/lib/active_support/vendor/{i18n-0.1.1 => i18n-0.1.3}/lib/i18n.rb (100%) rename activesupport/lib/active_support/vendor/{i18n-0.1.1 => i18n-0.1.3}/lib/i18n/backend/simple.rb (96%) rename activesupport/lib/active_support/vendor/{i18n-0.1.1 => i18n-0.1.3}/lib/i18n/exceptions.rb (100%) rename activesupport/lib/active_support/vendor/{i18n-0.1.1 => i18n-0.1.3}/test/all.rb (100%) rename activesupport/lib/active_support/vendor/{i18n-0.1.1 => i18n-0.1.3}/test/i18n_exceptions_test.rb (100%) rename activesupport/lib/active_support/vendor/{i18n-0.1.1 => i18n-0.1.3}/test/i18n_test.rb (100%) rename activesupport/lib/active_support/vendor/{i18n-0.1.1 => i18n-0.1.3}/test/locale/en.rb (100%) rename activesupport/lib/active_support/vendor/{i18n-0.1.1 => i18n-0.1.3}/test/locale/en.yml (100%) rename activesupport/lib/active_support/vendor/{i18n-0.1.1 => i18n-0.1.3}/test/simple_backend_test.rb (90%) diff --git a/activesupport/CHANGELOG b/activesupport/CHANGELOG index 5d9b77bbf501f..c0f24a103d961 100644 --- a/activesupport/CHANGELOG +++ b/activesupport/CHANGELOG @@ -1,5 +1,7 @@ *2.3.1 [RC2] (February 27th, 2009)* +* Vendorize i18n 0.1.3 gem (fixes issues with incompatible character encodings in Ruby 1.9) #2038 [Akira Matsuda] + * Update bundled memcache-client from 1.5.0.5 to 1.6.4.99. See http://www.mikeperham.com/2009/02/15/memcache-client-performance/ [Mike Perham] * Ruby 1.9.1p0 fix: URI.unescape can decode multibyte chars. #2033 [MOROHASHI Kyosuke] diff --git a/activesupport/lib/active_support/vendor.rb b/activesupport/lib/active_support/vendor.rb index 39da70a9f36ef..28852e65c8abe 100644 --- a/activesupport/lib/active_support/vendor.rb +++ b/activesupport/lib/active_support/vendor.rb @@ -22,8 +22,8 @@ # TODO I18n gem has not been released yet # begin -# gem 'i18n', '~> 0.1.1' +# gem 'i18n', '~> 0.1.3' # rescue Gem::LoadError - $:.unshift "#{File.dirname(__FILE__)}/vendor/i18n-0.1.1/lib" + $:.unshift "#{File.dirname(__FILE__)}/vendor/i18n-0.1.3/lib" require 'i18n' # end diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/.gitignore b/activesupport/lib/active_support/vendor/i18n-0.1.1/.gitignore deleted file mode 100644 index 0f41a39f89f54..0000000000000 --- a/activesupport/lib/active_support/vendor/i18n-0.1.1/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -.DS_Store -test/rails/fixtures -doc diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/MIT-LICENSE b/activesupport/lib/active_support/vendor/i18n-0.1.3/MIT-LICENSE similarity index 100% rename from activesupport/lib/active_support/vendor/i18n-0.1.1/MIT-LICENSE rename to activesupport/lib/active_support/vendor/i18n-0.1.3/MIT-LICENSE diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/README.textile b/activesupport/lib/active_support/vendor/i18n-0.1.3/README.textile similarity index 100% rename from activesupport/lib/active_support/vendor/i18n-0.1.1/README.textile rename to activesupport/lib/active_support/vendor/i18n-0.1.3/README.textile diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/Rakefile b/activesupport/lib/active_support/vendor/i18n-0.1.3/Rakefile similarity index 100% rename from activesupport/lib/active_support/vendor/i18n-0.1.1/Rakefile rename to activesupport/lib/active_support/vendor/i18n-0.1.3/Rakefile diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/i18n.gemspec b/activesupport/lib/active_support/vendor/i18n-0.1.3/i18n.gemspec similarity index 94% rename from activesupport/lib/active_support/vendor/i18n-0.1.1/i18n.gemspec rename to activesupport/lib/active_support/vendor/i18n-0.1.3/i18n.gemspec index 14294606bd14e..f102689a6f4a7 100644 --- a/activesupport/lib/active_support/vendor/i18n-0.1.1/i18n.gemspec +++ b/activesupport/lib/active_support/vendor/i18n-0.1.3/i18n.gemspec @@ -1,7 +1,7 @@ Gem::Specification.new do |s| s.name = "i18n" - s.version = "0.1.1" - s.date = "2008-10-26" + s.version = "0.1.3" + s.date = "2009-01-09" s.summary = "Internationalization support for Ruby" s.email = "rails-i18n@googlegroups.com" s.homepage = "http://rails-i18n.org" diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n.rb b/activesupport/lib/active_support/vendor/i18n-0.1.3/lib/i18n.rb similarity index 100% rename from activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n.rb rename to activesupport/lib/active_support/vendor/i18n-0.1.3/lib/i18n.rb diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n/backend/simple.rb b/activesupport/lib/active_support/vendor/i18n-0.1.3/lib/i18n/backend/simple.rb similarity index 96% rename from activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n/backend/simple.rb rename to activesupport/lib/active_support/vendor/i18n-0.1.3/lib/i18n/backend/simple.rb index b54164d496587..c09acd7d2ddd1 100644 --- a/activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n/backend/simple.rb +++ b/activesupport/lib/active_support/vendor/i18n-0.1.3/lib/i18n/backend/simple.rb @@ -151,12 +151,7 @@ def pluralize(locale, entry, count) def interpolate(locale, string, values = {}) return string unless string.is_a?(String) - if string.respond_to?(:force_encoding) - original_encoding = string.encoding - string.force_encoding(Encoding::BINARY) - end - - result = string.gsub(MATCH) do + string.gsub(MATCH) do escaped, pattern, key = $1, $2, $2.to_sym if escaped @@ -169,9 +164,6 @@ def interpolate(locale, string, values = {}) values[key].to_s end end - - result.force_encoding(original_encoding) if original_encoding - result end # Loads a single translations file by delegating to #load_rb or diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n/exceptions.rb b/activesupport/lib/active_support/vendor/i18n-0.1.3/lib/i18n/exceptions.rb similarity index 100% rename from activesupport/lib/active_support/vendor/i18n-0.1.1/lib/i18n/exceptions.rb rename to activesupport/lib/active_support/vendor/i18n-0.1.3/lib/i18n/exceptions.rb diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/all.rb b/activesupport/lib/active_support/vendor/i18n-0.1.3/test/all.rb similarity index 100% rename from activesupport/lib/active_support/vendor/i18n-0.1.1/test/all.rb rename to activesupport/lib/active_support/vendor/i18n-0.1.3/test/all.rb diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/i18n_exceptions_test.rb b/activesupport/lib/active_support/vendor/i18n-0.1.3/test/i18n_exceptions_test.rb similarity index 100% rename from activesupport/lib/active_support/vendor/i18n-0.1.1/test/i18n_exceptions_test.rb rename to activesupport/lib/active_support/vendor/i18n-0.1.3/test/i18n_exceptions_test.rb diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/i18n_test.rb b/activesupport/lib/active_support/vendor/i18n-0.1.3/test/i18n_test.rb similarity index 100% rename from activesupport/lib/active_support/vendor/i18n-0.1.1/test/i18n_test.rb rename to activesupport/lib/active_support/vendor/i18n-0.1.3/test/i18n_test.rb diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/locale/en.rb b/activesupport/lib/active_support/vendor/i18n-0.1.3/test/locale/en.rb similarity index 100% rename from activesupport/lib/active_support/vendor/i18n-0.1.1/test/locale/en.rb rename to activesupport/lib/active_support/vendor/i18n-0.1.3/test/locale/en.rb diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/locale/en.yml b/activesupport/lib/active_support/vendor/i18n-0.1.3/test/locale/en.yml similarity index 100% rename from activesupport/lib/active_support/vendor/i18n-0.1.1/test/locale/en.yml rename to activesupport/lib/active_support/vendor/i18n-0.1.3/test/locale/en.yml diff --git a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/simple_backend_test.rb b/activesupport/lib/active_support/vendor/i18n-0.1.3/test/simple_backend_test.rb similarity index 90% rename from activesupport/lib/active_support/vendor/i18n-0.1.1/test/simple_backend_test.rb rename to activesupport/lib/active_support/vendor/i18n-0.1.3/test/simple_backend_test.rb index 8ba7036abfb88..7b7b1375798cf 100644 --- a/activesupport/lib/active_support/vendor/i18n-0.1.1/test/simple_backend_test.rb +++ b/activesupport/lib/active_support/vendor/i18n-0.1.3/test/simple_backend_test.rb @@ -253,6 +253,32 @@ def test_interpolate_given_a_value_hash_interpolates_into_unicode_string assert_equal 'Häi David!', @backend.send(:interpolate, nil, 'Häi {{name}}!', :name => 'David') end + def test_interpolate_given_an_unicode_value_hash_interpolates_to_the_string + assert_equal 'Hi ゆきひろ!', @backend.send(:interpolate, nil, 'Hi {{name}}!', :name => 'ゆきひろ') + end + + def test_interpolate_given_an_unicode_value_hash_interpolates_into_unicode_string + assert_equal 'こんにちは、ゆきひろさん!', @backend.send(:interpolate, nil, 'こんにちは、{{name}}さん!', :name => 'ゆきひろ') + end + + if Kernel.const_defined?(:Encoding) + def test_interpolate_given_a_non_unicode_multibyte_value_hash_interpolates_into_a_string_with_the_same_encoding + assert_equal euc_jp('Hi ゆきひろ!'), @backend.send(:interpolate, nil, 'Hi {{name}}!', :name => euc_jp('ゆきひろ')) + end + + def test_interpolate_given_an_unicode_value_hash_into_a_non_unicode_multibyte_string_raises_encoding_compatibility_error + assert_raises(Encoding::CompatibilityError) do + @backend.send(:interpolate, nil, euc_jp('こんにちは、{{name}}さん!'), :name => 'ゆきひろ') + end + end + + def test_interpolate_given_a_non_unicode_multibyte_value_hash_into_an_unicode_string_raises_encoding_compatibility_error + assert_raises(Encoding::CompatibilityError) do + @backend.send(:interpolate, nil, 'こんにちは、{{name}}さん!', :name => euc_jp('ゆきひろ')) + end + end + end + def test_interpolate_given_nil_as_a_string_returns_nil assert_nil @backend.send(:interpolate, nil, nil, :name => 'David') end @@ -272,6 +298,12 @@ def test_interpolate_given_an_empty_values_hash_raises_missing_interpolation_arg def test_interpolate_given_a_string_containing_a_reserved_key_raises_reserved_interpolation_key assert_raises(I18n::ReservedInterpolationKey) { @backend.send(:interpolate, nil, '{{default}}', {:default => nil}) } end + + private + + def euc_jp(string) + string.encode!(Encoding::EUC_JP) + end end class I18nSimpleBackendLocalizeDateTest < Test::Unit::TestCase @@ -485,6 +517,10 @@ def test_load_translations_loads_from_different_file_formats class I18nSimpleBackendLoadPathTest < Test::Unit::TestCase include I18nSimpleBackendTestSetup + def teardown + I18n.load_path = [] + end + def test_nested_load_paths_do_not_break_locale_loading @backend = I18n::Backend::Simple.new I18n.load_path = [[File.dirname(__FILE__) + '/locale/en.yml']] @@ -492,6 +528,14 @@ def test_nested_load_paths_do_not_break_locale_loading assert_nothing_raised { @backend.send :init_translations } assert_not_nil backend_get_translations end + + def test_adding_arrays_of_filenames_to_load_path_do_not_break_locale_loading + @backend = I18n::Backend::Simple.new + I18n.load_path << Dir[File.dirname(__FILE__) + '/locale/*.{rb,yml}'] + assert_nil backend_get_translations + assert_nothing_raised { @backend.send :init_translations } + assert_not_nil backend_get_translations + end end class I18nSimpleBackendReloadTranslationsTest < Test::Unit::TestCase @@ -521,4 +565,4 @@ def test_reload_translations_uninitializes_translations @backend.reload! assert_equal @backend.initialized?, false end -end \ No newline at end of file +end