diff options
author | Pratik Naik <pratiknaik@gmail.com> | 2009-07-25 12:55:01 +0100 |
---|---|---|
committer | Pratik Naik <pratiknaik@gmail.com> | 2009-07-25 12:55:01 +0100 |
commit | c6b16fc2aada37aab1949cd9a1e46a2d9ccc8381 (patch) | |
tree | ea13a4e420a94ed7b552529ca9dc4b02680203d2 /activerecord | |
parent | f2d65a456fd93fd3a220f85c1001f0180bfdd6be (diff) | |
parent | 0c68d23f19010379a9320690ca17a26743c8f071 (diff) | |
download | rails-c6b16fc2aada37aab1949cd9a1e46a2d9ccc8381.tar.gz rails-c6b16fc2aada37aab1949cd9a1e46a2d9ccc8381.tar.bz2 rails-c6b16fc2aada37aab1949cd9a1e46a2d9ccc8381.zip |
Merge commit 'mainstream/master'
Diffstat (limited to 'activerecord')
25 files changed, 258 insertions, 279 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 411b640c9e..659de99873 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,9 @@ *Edge* +* Added :primary_key option to belongs_to associations. #765 [Szymon Nowak, Philip Hallstrom, Noel Rocha] + # employees.company_name references companies.name + Employee.belongs_to :company, :primary_key => 'name', :foreign_key => 'company_name' + * Implement #many? for NamedScope and AssociationCollection using #size. #1500 [Chris Kampmeier] * Added :touch option to belongs_to associations that will touch the parent record when the current record is saved or destroyed [DHH] diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 860d1003a6..910b7d7730 100755 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1031,6 +1031,8 @@ module ActiveRecord # of the association with an "_id" suffix. So a class that defines a <tt>belongs_to :person</tt> association will use # "person_id" as the default <tt>:foreign_key</tt>. Similarly, <tt>belongs_to :favorite_person, :class_name => "Person"</tt> # will use a foreign key of "favorite_person_id". + # [:primary_key] + # Specify the method that returns the primary key of associated object used for the association. By default this is id. # [:dependent] # If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to # <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. This option should not be specified when @@ -1065,6 +1067,7 @@ module ActiveRecord # # Option examples: # belongs_to :firm, :foreign_key => "client_of" + # belongs_to :person, :primary_key => "name", :foreign_key => "person_name" # belongs_to :author, :class_name => "Person", :foreign_key => "author_id" # belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id", # :conditions => 'discounts > #{payments_count}' @@ -1345,9 +1348,16 @@ module ActiveRecord if send(reflection.name).loaded? || reflection.options[:finder_sql] send(reflection.name).map(&:id) else - send(reflection.name).all(:select => "#{reflection.quoted_table_name}.#{reflection.klass.primary_key}").map(&:id) + if reflection.through_reflection && reflection.source_reflection.belongs_to? + through = reflection.through_reflection + primary_key = reflection.source_reflection.primary_key_name + send(through.name).all(:select => "DISTINCT #{through.quoted_table_name}.#{primary_key}").map!(&:"#{primary_key}") + else + send(reflection.name).all(:select => "#{reflection.quoted_table_name}.#{reflection.klass.primary_key}").map!(&:id) + end end end + end def collection_accessor_methods(reflection, association_proxy_class, writer = true) @@ -1393,14 +1403,14 @@ module ActiveRecord method_name = "belongs_to_counter_cache_after_create_for_#{reflection.name}".to_sym define_method(method_name) do association = send(reflection.name) - association.class.increment_counter(cache_column, send(reflection.primary_key_name)) unless association.nil? + association.class.increment_counter(cache_column, association.id) unless association.nil? end after_create(method_name) method_name = "belongs_to_counter_cache_before_destroy_for_#{reflection.name}".to_sym define_method(method_name) do association = send(reflection.name) - association.class.decrement_counter(cache_column, send(reflection.primary_key_name)) unless association.nil? + association.class.decrement_counter(cache_column, association.id) unless association.nil? end before_destroy(method_name) @@ -1592,7 +1602,7 @@ module ActiveRecord mattr_accessor :valid_keys_for_belongs_to_association @@valid_keys_for_belongs_to_association = [ - :class_name, :foreign_key, :foreign_type, :remote, :select, :conditions, + :class_name, :primary_key, :foreign_key, :foreign_type, :remote, :select, :conditions, :include, :dependent, :counter_cache, :extend, :polymorphic, :readonly, :validate, :touch, :inverse_of ] diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index c88575048a..628033c87a 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -14,7 +14,7 @@ module ActiveRecord if record.nil? if counter_cache_name && !@owner.new_record? - @reflection.klass.decrement_counter(counter_cache_name, @owner[@reflection.primary_key_name]) if @owner[@reflection.primary_key_name] + @reflection.klass.decrement_counter(counter_cache_name, previous_record_id) if @owner[@reflection.primary_key_name] end @target = @owner[@reflection.primary_key_name] = nil @@ -27,7 +27,7 @@ module ActiveRecord end @target = (AssociationProxy === record ? record.target : record) - @owner[@reflection.primary_key_name] = record.id unless record.new_record? + @owner[@reflection.primary_key_name] = record_id(record) unless record.new_record? @updated = true end @@ -43,13 +43,18 @@ module ActiveRecord private def find_target - the_target = @reflection.klass.find( + find_method = if @reflection.options[:primary_key] + "find_by_#{@reflection.options[:primary_key]}" + else + "find" + end + the_target = @reflection.klass.send(find_method, @owner[@reflection.primary_key_name], :select => @reflection.options[:select], :conditions => conditions, :include => @reflection.options[:include], :readonly => @reflection.options[:readonly] - ) + ) if @owner[@reflection.primary_key_name] set_inverse_instance(the_target, @owner) the_target end @@ -63,6 +68,19 @@ module ActiveRecord def we_can_set_the_inverse_on_this?(record) @reflection.has_inverse? && @reflection.inverse_of.macro == :has_one end + + def record_id(record) + record.send(@reflection.options[:primary_key] || :id) + end + + def previous_record_id + @previous_record_id ||= if @reflection.options[:primary_key] + previous_record = @owner.send(@reflection.name) + previous_record.nil? ? nil : previous_record.id + else + @owner[@reflection.primary_key_name] + end + end end end end diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index d8146daa54..67e18d692d 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -7,7 +7,7 @@ module ActiveRecord else @target = (AssociationProxy === record ? record.target : record) - @owner[@reflection.primary_key_name] = record.id + @owner[@reflection.primary_key_name] = record_id(record) @owner[@reflection.options[:foreign_type]] = record.class.base_class.name.to_s @updated = true @@ -41,6 +41,10 @@ module ActiveRecord !@owner[@reflection.primary_key_name].nil? end + def record_id(record) + record.send(@reflection.options[:primary_key] || :id) + end + def association_class @owner[@reflection.options[:foreign_type]] ? @owner[@reflection.options[:foreign_type]].constantize : nil end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 15358979c2..ecd2d57a5a 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -133,6 +133,7 @@ module ActiveRecord end private + # Suffixes a, ?, c become regexp /(a|\?|c)$/ def rebuild_attribute_method_regexp suffixes = attribute_method_suffixes.map { |s| Regexp.escape(s) } @@ -238,19 +239,17 @@ module ActiveRecord def method_missing(method_id, *args, &block) method_name = method_id.to_s - if self.class.private_method_defined?(method_name) - raise NoMethodError.new("Attempt to call private method", method_name, args) - end - # If we haven't generated any methods yet, generate them, then # see if we've created the method we're looking for. if !self.class.generated_methods? self.class.define_attribute_methods + guard_private_attribute_method!(method_name, args) if self.class.generated_methods.include?(method_name) return self.send(method_id, *args, &block) end end + guard_private_attribute_method!(method_name, args) if self.class.primary_key.to_s == method_name id elsif md = self.class.match_attribute_method?(method_name) @@ -371,6 +370,12 @@ module ActiveRecord end private + # prevent method_missing from calling private methods with #send + def guard_private_attribute_method!(method_name, args) + if self.class.private_method_defined?(method_name) + raise NoMethodError.new("Attempt to call private method", method_name, args) + end + end def missing_attribute(attr_name, stack) raise ActiveRecord::MissingAttributeError, "missing attribute: #{attr_name}", stack diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index a540570f42..c1bc8423a9 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -339,7 +339,8 @@ module ActiveRecord association.save(!autosave) if association.new_record? || autosave if association.updated? - self[reflection.primary_key_name] = association.id + association_id = association.send(reflection.options[:primary_key] || :id) + 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 diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index bb7342ca6e..5a36ff5ba2 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -3215,6 +3215,7 @@ module ActiveRecord #:nodoc: include Dirty include Callbacks, ActiveModel::Observing, Timestamp include Associations, AssociationPreload, NamedScope + include ActiveModel::Conversion # AutosaveAssociation needs to be included before Transactions, because we want # #save_with_autosave_associations to be wrapped inside a transaction. diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 0ad135e378..4a2ec5bf95 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -365,7 +365,7 @@ module ActiveRecord result = send(method) end - notify(method) + notify_observers(method) return result end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 2d90ef35aa..5d88012e4f 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -78,11 +78,14 @@ HEADER begin tbl = StringIO.new + # first dump primary key column if @connection.respond_to?(:pk_and_sequence_for) pk, pk_seq = @connection.pk_and_sequence_for(table) + elsif @connection.respond_to?(:primary_key) + pk = @connection.primary_key(table) end pk ||= 'id' - + tbl.print " create_table #{table.inspect}" if columns.detect { |c| c.name == pk } if pk != 'id' @@ -94,6 +97,7 @@ HEADER tbl.print ", :force => true" tbl.puts " do |t|" + # then dump all non-primary key columns column_specs = columns.map do |column| raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" if @types[column.type].nil? next if column.name == pk diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb index 23d085bea9..94f1e8f1fd 100644 --- a/activerecord/lib/active_record/serialization.rb +++ b/activerecord/lib/active_record/serialization.rb @@ -1,42 +1,9 @@ module ActiveRecord #:nodoc: module Serialization - class Serializer #:nodoc: - attr_reader :options - - def initialize(record, options = nil) - @record = record - @options = options ? options.dup : {} - end - - # To replicate the behavior in ActiveRecord#attributes, - # <tt>:except</tt> takes precedence over <tt>:only</tt>. If <tt>:only</tt> is not set - # for a N level model but is set for the N+1 level models, - # then because <tt>:except</tt> is set to a default value, the second - # level model can have both <tt>:except</tt> and <tt>:only</tt> set. So if - # <tt>:only</tt> is set, always delete <tt>:except</tt>. - def serializable_attribute_names - attribute_names = @record.attribute_names - - if options[:only] - options.delete(:except) - attribute_names = attribute_names & Array.wrap(options[:only]).collect { |n| n.to_s } - else - options[:except] = Array.wrap(options[:except]) | Array.wrap(@record.class.inheritance_column) - attribute_names = attribute_names - options[:except].collect { |n| n.to_s } - end - - attribute_names - end - - def serializable_method_names - Array.wrap(options[:methods]).inject([]) do |method_attributes, name| - method_attributes << name if @record.respond_to?(name.to_s) - method_attributes - end - end - - def serializable_names - serializable_attribute_names + serializable_method_names + module RecordSerializer #:nodoc: + def initialize(*args) + super + options[:except] |= Array.wrap(@serializable.class.inheritance_column) end # Add associations specified via the <tt>:includes</tt> option. @@ -53,11 +20,11 @@ module ActiveRecord #:nodoc: associations = include_has_options ? include_associations.keys : Array.wrap(include_associations) for association in associations - records = case @record.class.reflect_on_association(association).macro + records = case @serializable.class.reflect_on_association(association).macro when :has_many, :has_and_belongs_to_many - @record.send(association).to_a + @serializable.send(association).to_a when :has_one, :belongs_to - @record.send(association) + @serializable.send(association) end unless records.nil? @@ -71,28 +38,19 @@ module ActiveRecord #:nodoc: end end - def serializable_record - record = {} - serializable_names.each { |name| record[name] = @record.send(name) } + def serializable_hash + hash = super add_includes do |association, records, opts| - record[association] = + hash[association] = if records.is_a?(Enumerable) - records.collect { |r| self.class.new(r, opts).serializable_record } + records.collect { |r| self.class.new(r, opts).serializable_hash } else - self.class.new(records, opts).serializable_record + self.class.new(records, opts).serializable_hash end end - record - end - - def serialize - # overwrite to implement - end - - def to_s(&block) - serialize(&block) + hash end end end diff --git a/activerecord/lib/active_record/serializers/json_serializer.rb b/activerecord/lib/active_record/serializers/json_serializer.rb index 21afcd6e5c..63bf42c09d 100644 --- a/activerecord/lib/active_record/serializers/json_serializer.rb +++ b/activerecord/lib/active_record/serializers/json_serializer.rb @@ -1,91 +1,14 @@ -require 'active_support/json' -require 'active_model/naming' - module ActiveRecord #:nodoc: module Serialization extend ActiveSupport::Concern + include ActiveModel::Serializers::JSON - included do - cattr_accessor :include_root_in_json, :instance_writer => false + class JSONSerializer < ActiveModel::Serializers::JSON::Serializer + include Serialization::RecordSerializer end - # Returns a JSON string representing the model. Some configuration is - # available through +options+. - # - # The option <tt>ActiveRecord::Base.include_root_in_json</tt> controls the - # top-level behavior of to_json. In a new Rails application, it is set to - # <tt>true</tt> in initializers/new_rails_defaults.rb. When it is <tt>true</tt>, - # to_json will emit a single root node named after the object's type. For example: - # - # konata = User.find(1) - # ActiveRecord::Base.include_root_in_json = true - # konata.to_json - # # => { "user": {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true} } - # - # ActiveRecord::Base.include_root_in_json = false - # konata.to_json - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true} - # - # The remainder of the examples in this section assume include_root_in_json is set to - # <tt>false</tt>. - # - # Without any +options+, the returned JSON string will include all - # the model's attributes. For example: - # - # konata = User.find(1) - # konata.to_json - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true} - # - # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the attributes - # included, and work similar to the +attributes+ method. For example: - # - # konata.to_json(:only => [ :id, :name ]) - # # => {"id": 1, "name": "Konata Izumi"} - # - # konata.to_json(:except => [ :id, :created_at, :age ]) - # # => {"name": "Konata Izumi", "awesome": true} - # - # To include any methods on the model, use <tt>:methods</tt>. - # - # konata.to_json(:methods => :permalink) - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true, - # "permalink": "1-konata-izumi"} - # - # To include associations, use <tt>:include</tt>. - # - # konata.to_json(:include => :posts) - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true, - # "posts": [{"id": 1, "author_id": 1, "title": "Welcome to the weblog"}, - # {"id": 2, author_id: 1, "title": "So I was thinking"}]} - # - # 2nd level and higher order associations work as well: - # - # konata.to_json(:include => { :posts => { - # :include => { :comments => { - # :only => :body } }, - # :only => :title } }) - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true, - # "posts": [{"comments": [{"body": "1st post!"}, {"body": "Second!"}], - # "title": "Welcome to the weblog"}, - # {"comments": [{"body": "Don't think too hard"}], - # "title": "So I was thinking"}]} def encode_json(encoder) - hash = Serializer.new(self, encoder.options).serializable_record - hash = { self.class.model_name.element => hash } if include_root_in_json - ActiveSupport::JSON.encode(hash) - end - - def as_json(options = nil) self end #:nodoc: - - def from_json(json) - self.attributes = ActiveSupport::JSON.decode(json) - self + JSONSerializer.new(self, encoder.options).to_s end end end diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index 4eaf9531e2..4e172bd2b6 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -71,6 +71,21 @@ module ActiveRecord #:nodoc: # </account> # </firm> # + # Additionally, the record being serialized will be passed to a Proc's second + # parameter. This allows for ad hoc additions to the resultant document that + # incorporate the context of the record being serialized. And by leveraging the + # closure created by a Proc, to_xml can be used to add elements that normally fall + # outside of the scope of the model -- for example, generating and appending URLs + # associated with models. + # + # proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) } + # firm.to_xml :procs => [ proc ] + # + # <firm> + # # ... normal attributes as shown above ... + # <name-reverse>slangis73</name-reverse> + # </firm> + # # To include deeper levels of associations pass a hash like this: # # firm.to_xml :include => {:account => {}, :clients => {:include => :address}} @@ -164,73 +179,20 @@ module ActiveRecord #:nodoc: end end - class XmlSerializer < ActiveRecord::Serialization::Serializer #:nodoc: - def builder - @builder ||= begin - require 'builder' unless defined? ::Builder - options[:indent] ||= 2 - builder = options[:builder] ||= ::Builder::XmlMarkup.new(:indent => options[:indent]) - - unless options[:skip_instruct] - builder.instruct! - options[:skip_instruct] = true - end - - builder - end - end - - def root - root = (options[:root] || @record.class.to_s.underscore).to_s - reformat_name(root) - end - - def dasherize? - !options.has_key?(:dasherize) || options[:dasherize] - end - - def camelize? - options.has_key?(:camelize) && options[:camelize] - end - - def reformat_name(name) - name = name.camelize if camelize? - dasherize? ? name.dasherize : name - end + class XmlSerializer < ActiveModel::Serializers::Xml::Serializer #:nodoc: + include Serialization::RecordSerializer def serializable_attributes - serializable_attribute_names.collect { |name| Attribute.new(name, @record) } + serializable_attribute_names.collect { |name| Attribute.new(name, @serializable) } end def serializable_method_attributes Array(options[:methods]).inject([]) do |method_attributes, name| - method_attributes << MethodAttribute.new(name.to_s, @record) if @record.respond_to?(name.to_s) + method_attributes << MethodAttribute.new(name.to_s, @serializable) if @serializable.respond_to?(name.to_s) method_attributes end end - def add_attributes - (serializable_attributes + serializable_method_attributes).each do |attribute| - add_tag(attribute) - end - end - - def add_procs - if procs = options.delete(:procs) - [ *procs ].each do |proc| - proc.call(options) - end - end - end - - def add_tag(attribute) - builder.tag!( - reformat_name(attribute.name), - attribute.value.to_s, - attribute.decorations(!options[:skip_types]) - ) - end - def add_associations(association, records, opts) if records.is_a?(Enumerable) tag = reformat_name(association.to_s) @@ -254,7 +216,7 @@ module ActiveRecord #:nodoc: end end else - if record = @record.send(association) + if record = @serializable.send(association) record.to_xml(opts.merge(:root => association)) end end @@ -280,50 +242,10 @@ module ActiveRecord #:nodoc: end end - class Attribute #:nodoc: - attr_reader :name, :value, :type - - def initialize(name, record) - @name, @record = name, record - - @type = compute_type - @value = compute_value - end - - # There is a significant speed improvement if the value - # does not need to be escaped, as <tt>tag!</tt> escapes all values - # to ensure that valid XML is generated. For known binary - # values, it is at least an order of magnitude faster to - # Base64 encode binary values and directly put them in the - # output XML than to pass the original value or the Base64 - # encoded value to the <tt>tag!</tt> method. It definitely makes - # no sense to Base64 encode the value and then give it to - # <tt>tag!</tt>, since that just adds additional overhead. - def needs_encoding? - ![ :binary, :date, :datetime, :boolean, :float, :integer ].include?(type) - end - - def decorations(include_types = true) - decorations = {} - - if type == :binary - decorations[:encoding] = 'base64' - end - - if include_types && type != :string - decorations[:type] = type - end - - if value.nil? - decorations[:nil] = true - end - - decorations - end - + class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc: protected def compute_type - type = @record.class.serialized_attributes.has_key?(name) ? :yaml : @record.class.columns_hash[name].type + type = @serializable.class.serialized_attributes.has_key?(name) ? :yaml : @serializable.class.columns_hash[name].type case type when :text @@ -334,22 +256,12 @@ module ActiveRecord #:nodoc: type end end - - def compute_value - value = @record.send(name) - - if formatter = Hash::XML_FORMATTING[type.to_s] - value ? formatter.call(value) : nil - else - value - end - end end class MethodAttribute < Attribute #:nodoc: protected def compute_type - Hash::XML_TYPE_NAMES[@record.send(name).class.name] || :string + Hash::XML_TYPE_NAMES[@serializable.send(name).class.name] || :string end end end diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 13a78a1890..ab6f752243 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -14,6 +14,7 @@ require 'models/tagging' require 'models/comment' require 'models/sponsor' require 'models/member' +require 'models/essay' class BelongsToAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :developers, :projects, :topics, @@ -25,6 +26,11 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert !Client.find(3).firm.nil?, "Microsoft should have a firm" end + def test_belongs_to_with_primary_key + client = Client.create(:name => "Primary key client", :firm_name => companies(:first_firm).name) + assert_equal companies(:first_firm).name, client.firm_with_primary_key.name + end + def test_proxy_assignment account = Account.find(1) assert_nothing_raised { account.firm = account.firm } @@ -47,6 +53,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal apple.id, citibank.firm_id end + def test_natural_assignment_with_primary_key + apple = Firm.create("name" => "Apple") + citibank = Client.create("name" => "Primary key client") + citibank.firm_with_primary_key = apple + assert_equal apple.name, citibank.firm_name + end + def test_no_unexpected_aliasing first_firm = companies(:first_firm) another_firm = companies(:another_firm) @@ -69,6 +82,15 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal apple, citibank.firm end + def test_creating_the_belonging_object_with_primary_key + client = Client.create(:name => "Primary key client") + apple = client.create_firm_with_primary_key("name" => "Apple") + assert_equal apple, client.firm_with_primary_key + client.save + client.reload + assert_equal apple, client.firm_with_primary_key + end + def test_building_the_belonging_object citibank = Account.create("credit_limit" => 10) apple = citibank.build_firm("name" => "Apple") @@ -76,6 +98,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal apple.id, citibank.firm_id end + def test_building_the_belonging_object_with_primary_key + client = Client.create(:name => "Primary key client") + apple = client.build_firm_with_primary_key("name" => "Apple") + client.save + assert_equal apple.name, client.firm_name + end + def test_natural_assignment_to_nil client = Client.find(3) client.firm = nil @@ -84,6 +113,14 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_nil client.client_of end + def test_natural_assignment_to_nil_with_primary_key + client = Client.create(:name => "Primary key client", :firm_name => companies(:first_firm).name) + client.firm_with_primary_key = nil + client.save + assert_nil client.firm_with_primary_key(true) + assert_nil client.client_of + end + def test_with_different_class_name assert_equal Company.find(1).name, Company.find(3).firm_with_other_name.name assert_not_nil Company.find(3).firm_with_other_name, "Microsoft should have a firm" @@ -110,6 +147,17 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal 0, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply deleted" end + def test_belongs_to_with_primary_key_counter + debate = Topic.create("title" => "debate") + assert_equal 0, debate.send(:read_attribute, "replies_count"), "No replies yet" + + trash = debate.replies_with_primary_key.create("title" => "blah!", "content" => "world around!") + assert_equal 1, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply created" + + trash.destroy + assert_equal 0, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply deleted" + end + def test_belongs_to_counter_with_assigning_nil p = Post.find(1) c = Comment.find(1) @@ -122,6 +170,18 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal 1, Post.find(p.id).comments.size end + def test_belongs_to_with_primary_key_counter_with_assigning_nil + debate = Topic.create("title" => "debate") + reply = Reply.create("title" => "blah!", "content" => "world around!", "parent_title" => "debate") + + assert_equal debate.title, reply.parent_title + assert_equal 1, Topic.find(debate.id).send(:read_attribute, "replies_count") + + reply.topic_with_primary_key = nil + + assert_equal 0, Topic.find(debate.id).send(:read_attribute, "replies_count") + end + def test_belongs_to_counter_with_reassigning t1 = Topic.create("title" => "t1") t2 = Topic.create("title" => "t2") @@ -219,6 +279,18 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal firm, final_cut.firm(true) end + def test_assignment_before_child_saved_with_primary_key + final_cut = Client.new("name" => "Final Cut") + firm = Firm.find(1) + final_cut.firm_with_primary_key = firm + assert final_cut.new_record? + assert final_cut.save + assert !final_cut.new_record? + assert !firm.new_record? + assert_equal firm, final_cut.firm_with_primary_key + assert_equal firm, final_cut.firm_with_primary_key(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 @@ -304,6 +376,20 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase sponsor.sponsorable = member assert_equal "Member", sponsor.sponsorable_type end + + def test_polymorphic_assignment_with_primary_key_foreign_type_field_updating + # should update when assigning a saved record + essay = Essay.new + writer = Author.create(:name => "David") + essay.writer = writer + assert_equal "Author", essay.writer_type + + # should update when assigning a new record + essay = Essay.new + writer = Author.new + essay.writer = writer + assert_equal "Author", essay.writer_type + end def test_polymorphic_assignment_updates_foreign_id_field_for_new_and_saved_records sponsor = Sponsor.new @@ -317,6 +403,18 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal nil, sponsor.sponsorable_id end + def test_polymorphic_assignment_with_primary_key_updates_foreign_id_field_for_new_and_saved_records + essay = Essay.new + saved_writer = Author.create(:name => "David") + new_writer = Author.new + + essay.writer = saved_writer + assert_equal saved_writer.name, essay.writer_id + + essay.writer = new_writer + assert_equal nil, essay.writer_id + end + def test_belongs_to_proxy_should_not_respond_to_private_methods assert_raise(NoMethodError) { companies(:first_firm).private_method } assert_raise(NoMethodError) { companies(:second_client).firm.private_method } diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 7a4712d7c8..8529ff0285 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -243,8 +243,12 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal 2, people(:michael).jobs.size end - def test_get_ids - assert_equal [posts(:welcome).id, posts(:authorless).id].sort, people(:michael).post_ids.sort + def test_get_ids_for_belongs_to_source + assert_sql(/DISTINCT/) { assert_equal [posts(:welcome).id, posts(:authorless).id].sort, people(:michael).post_ids.sort } + end + + def test_get_ids_for_has_many_source + assert_equal [comments(:eager_other_comment1).id], authors(:mary).comment_ids end def test_get_ids_for_loaded_associations diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 17ed302465..183be1e2f9 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -277,6 +277,22 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_raise(ActiveRecord::UnknownAttributeError) { @target.new.attributes = { :title => "Ants in pants" } } end + def test_read_attribute_overwrites_private_method_not_considered_implemented + # simulate a model with a db column that shares its name an inherited + # private method (e.g. Object#system) + # + Object.class_eval do + private + def title; "private!"; end + end + assert !@target.instance_method_already_implemented?(:title) + topic = @target.new + assert_equal nil, topic.title + + Object.send(:undef_method, :title) # remove test method from object + end + + private def time_related_columns_on_topic Topic.columns.select{|c| [:time, :date, :datetime, :timestamp].include?(c.type)}.map(&:name) diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index f9ac37cc87..e47f898485 100755 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -2026,7 +2026,7 @@ class BasicsTest < ActiveRecord::TestCase def test_inspect_instance topic = topics(:first) - assert_equal %(#<Topic id: 1, title: "The First Topic", author_name: "David", author_email_address: "david@loudthinking.com", written_on: "#{topic.written_on.to_s(:db)}", bonus_time: "#{topic.bonus_time.to_s(:db)}", last_read: "#{topic.last_read.to_s(:db)}", content: "Have a nice day", approved: false, replies_count: 1, parent_id: nil, type: nil>), topic.inspect + assert_equal %(#<Topic id: 1, title: "The First Topic", author_name: "David", author_email_address: "david@loudthinking.com", written_on: "#{topic.written_on.to_s(:db)}", bonus_time: "#{topic.bonus_time.to_s(:db)}", last_read: "#{topic.last_read.to_s(:db)}", content: "Have a nice day", approved: false, replies_count: 1, parent_id: nil, parent_title: nil, type: nil>), topic.inspect end def test_inspect_new_instance diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 194d5e9dff..4083b990d9 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -27,25 +27,25 @@ class ReflectionTest < ActiveRecord::TestCase def test_read_attribute_names assert_equal( - %w( id title author_name author_email_address bonus_time written_on last_read content approved replies_count parent_id type ).sort, + %w( id title author_name author_email_address bonus_time written_on last_read content approved replies_count parent_id parent_title type ).sort, @first.attribute_names ) end def test_columns - assert_equal 12, Topic.columns.length + assert_equal 13, Topic.columns.length end def test_columns_are_returned_in_the_order_they_were_declared column_names = Topic.columns.map { |column| column.name } - assert_equal %w(id title author_name author_email_address written_on bonus_time last_read content approved replies_count parent_id type), column_names + assert_equal %w(id title author_name author_email_address written_on bonus_time last_read content approved replies_count parent_id parent_title type), column_names end def test_content_columns content_columns = Topic.content_columns content_column_names = content_columns.map {|column| column.name} - assert_equal 8, content_columns.length - assert_equal %w(title author_name author_email_address written_on bonus_time last_read content approved).sort, content_column_names.sort + assert_equal 9, content_columns.length + assert_equal %w(title author_name author_email_address written_on bonus_time last_read content approved parent_title).sort, content_column_names.sort end def test_column_string_type_and_limit diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 4f02be3c06..9612b0beb6 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -156,6 +156,13 @@ class SchemaDumperTest < ActiveRecord::TestCase index_definition = standard_dump.split(/\n/).grep(/add_index.*companies/).first.strip assert_equal 'add_index "companies", ["firm_id", "type", "rating", "ruby_type"], :name => "company_index"', index_definition end + + def test_schema_dump_should_honor_nonstandard_primary_keys + output = standard_dump + match = output.match(%r{create_table "movies"(.*)do}) + assert_not_nil(match, "nonstandardpk table not found") + assert_match %r(:primary_key => "movieid"), match[1], "non-standard primary key not preserved" + end if current_adapter?(:MysqlAdapter) def test_schema_dump_should_not_add_default_value_for_mysql_text_field @@ -163,13 +170,6 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{t.text\s+"body",\s+:null => false$}, output end - def test_mysql_schema_dump_should_honor_nonstandard_primary_keys - output = standard_dump - match = output.match(%r{create_table "movies"(.*)do}) - assert_not_nil(match, "nonstandardpk table not found") - assert_match %r(:primary_key => "movieid"), match[1], "non-standard primary key not preserved" - end - def test_schema_dump_includes_length_for_mysql_blob_and_text_fields output = standard_dump assert_match %r{t.binary\s+"tiny_blob",\s+:limit => 255$}, output diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb index b49997669e..e1ad5c1685 100644 --- a/activerecord/test/cases/xml_serialization_test.rb +++ b/activerecord/test/cases/xml_serialization_test.rb @@ -174,6 +174,12 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase assert_match %r{<nationality>Danish</nationality>}, xml end + def test_dual_arity_procs_are_called_on_object + proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) } + xml = authors(:david).to_xml(:procs => [ proc ]) + assert_match %r{<name-reverse>divaD</name-reverse>}, xml + end + def test_top_level_procs_arent_applied_to_associations author_proc = Proc.new { |options| options[:builder].tag!('nationality', 'Danish') } xml = authors(:david).to_xml(:procs => [ author_proc ], :include => :posts, :indent => 2) diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 0d9ee36b20..b844c7cce0 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -87,6 +87,8 @@ class Author < ActiveRecord::Base has_many :tags, :through => :posts # through has_many :through has_many :post_categories, :through => :posts, :source => :categories + has_one :essay, :primary_key => :name, :as => :writer + belongs_to :author_address, :dependent => :destroy belongs_to :author_address_extra, :dependent => :delete, :class_name => "AuthorAddress" diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index 840527ddeb..22168468a6 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -88,6 +88,7 @@ class Client < Company belongs_to :firm_with_select, :class_name => "Firm", :foreign_key => "firm_id", :select => "id" belongs_to :firm_with_other_name, :class_name => "Firm", :foreign_key => "client_of" belongs_to :firm_with_condition, :class_name => "Firm", :foreign_key => "client_of", :conditions => ["1 = ?", 1] + belongs_to :firm_with_primary_key, :class_name => "Firm", :primary_key => "name", :foreign_key => "firm_name" belongs_to :readonly_firm, :class_name => "Firm", :foreign_key => "firm_id", :readonly => true # Record destruction so we can test whether firm.clients.clear has diff --git a/activerecord/test/models/essay.rb b/activerecord/test/models/essay.rb new file mode 100644 index 0000000000..6c28f5e49b --- /dev/null +++ b/activerecord/test/models/essay.rb @@ -0,0 +1,3 @@ +class Essay < ActiveRecord::Base + belongs_to :writer, :primary_key => :name, :polymorphic => true +end diff --git a/activerecord/test/models/reply.rb b/activerecord/test/models/reply.rb index 616c07687c..f5906dedd1 100644 --- a/activerecord/test/models/reply.rb +++ b/activerecord/test/models/reply.rb @@ -4,12 +4,13 @@ class Reply < Topic named_scope :base belongs_to :topic, :foreign_key => "parent_id", :counter_cache => true + belongs_to :topic_with_primary_key, :class_name => "Topic", :primary_key => "title", :foreign_key => "parent_title", :counter_cache => "replies_count" has_many :replies, :class_name => "SillyReply", :dependent => :destroy, :foreign_key => "parent_id" validate :errors_on_empty_content validate_on_create :title_is_wrong_create - attr_accessible :title, :author_name, :author_email_address, :written_on, :content, :last_read + attr_accessible :title, :author_name, :author_email_address, :written_on, :content, :last_read, :parent_title validate :check_empty_title validate_on_create :check_content_mismatch diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index 51012d22ed..201d96dcd7 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -39,6 +39,7 @@ class Topic < ActiveRecord::Base named_scope :by_rejected_ids, lambda {{ :conditions => { :id => all(:conditions => {:approved => false}).map(&:id) } }} has_many :replies, :dependent => :destroy, :foreign_key => "parent_id" + has_many :replies_with_primary_key, :class_name => "Reply", :dependent => :destroy, :primary_key => "title", :foreign_key => "parent_title" serialize :content before_create :default_written_on diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index d2d6d1f4b3..2b7d3856b7 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -161,6 +161,12 @@ ActiveRecord::Schema.define do t.integer :course_id, :null => false end + create_table :essays, :force => true do |t| + t.string :name + t.string :writer_id + t.string :writer_type + end + create_table :events, :force => true do |t| t.string :title, :limit => 5 end @@ -421,6 +427,7 @@ ActiveRecord::Schema.define do t.boolean :approved, :default => true t.integer :replies_count, :default => 0 t.integer :parent_id + t.string :parent_title t.string :type end |