diff options
31 files changed, 706 insertions, 254 deletions
diff --git a/actionpack/CHANGELOG b/actionpack/CHANGELOG index aab26b411c..75de1fe2a6 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,5 +1,7 @@ *Edge* +* Make sure javascript_include_tag/stylesheet_link_tag does not append ".js" or ".css" onto external urls. #1664 [Matthew Rudy Jacobs] + * Ruby 1.9: fix Content-Length for multibyte send_data streaming. #2661 [Sava Chankov] * Ruby 1.9: ERB template encoding using a magic comment at the top of the file. [Jeremy Kemper] diff --git a/actionpack/lib/action_view/helpers/asset_tag_helper.rb b/actionpack/lib/action_view/helpers/asset_tag_helper.rb index 081003bcf3..c71840d41f 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helper.rb @@ -171,7 +171,7 @@ module ActionView end # Computes the path to a javascript asset in the public javascripts directory. - # If the +source+ filename has no extension, .js will be appended. + # If the +source+ filename has no extension, .js will be appended (except for explicit URIs) # Full paths from the document root will be passed through. # Used internally by javascript_include_tag to build the script path. # @@ -179,7 +179,7 @@ module ActionView # javascript_path "xmlhr" # => /javascripts/xmlhr.js # javascript_path "dir/xmlhr.js" # => /javascripts/dir/xmlhr.js # javascript_path "/dir/xmlhr" # => /dir/xmlhr.js - # javascript_path "http://www.railsapplication.com/js/xmlhr" # => http://www.railsapplication.com/js/xmlhr.js + # javascript_path "http://www.railsapplication.com/js/xmlhr" # => http://www.railsapplication.com/js/xmlhr # javascript_path "http://www.railsapplication.com/js/xmlhr.js" # => http://www.railsapplication.com/js/xmlhr.js def javascript_path(source) compute_public_path(source, 'javascripts', 'js') @@ -337,7 +337,7 @@ module ActionView end # Computes the path to a stylesheet asset in the public stylesheets directory. - # If the +source+ filename has no extension, <tt>.css</tt> will be appended. + # If the +source+ filename has no extension, <tt>.css</tt> will be appended (except for explicit URIs). # Full paths from the document root will be passed through. # Used internally by +stylesheet_link_tag+ to build the stylesheet path. # @@ -345,8 +345,8 @@ module ActionView # stylesheet_path "style" # => /stylesheets/style.css # stylesheet_path "dir/style.css" # => /stylesheets/dir/style.css # stylesheet_path "/dir/style.css" # => /dir/style.css - # stylesheet_path "http://www.railsapplication.com/css/style" # => http://www.railsapplication.com/css/style.css - # stylesheet_path "http://www.railsapplication.com/css/style.js" # => http://www.railsapplication.com/css/style.css + # stylesheet_path "http://www.railsapplication.com/css/style" # => http://www.railsapplication.com/css/style + # stylesheet_path "http://www.railsapplication.com/css/style.css" # => http://www.railsapplication.com/css/style.css def stylesheet_path(source) compute_public_path(source, 'stylesheets', 'css') end @@ -629,11 +629,11 @@ module ActionView has_request = @controller.respond_to?(:request) source_ext = File.extname(source)[1..-1] - if ext && (source_ext.blank? || (ext != source_ext && File.exist?(File.join(ASSETS_DIR, dir, "#{source}.#{ext}")))) + if ext && !is_uri?(source) && (source_ext.blank? || (ext != source_ext && File.exist?(File.join(ASSETS_DIR, dir, "#{source}.#{ext}")))) source += ".#{ext}" end - unless source =~ %r{^[-a-z]+://} + unless is_uri?(source) source = "/#{dir}/#{source}" unless source[0] == ?/ source = rewrite_asset_path(source) @@ -645,10 +645,10 @@ module ActionView end end - if include_host && source !~ %r{^[-a-z]+://} + if include_host && !is_uri?(source) host = compute_asset_host(source) - if has_request && !host.blank? && host !~ %r{^[-a-z]+://} + if has_request && !host.blank? && !is_uri?(host) host = "#{@controller.request.protocol}#{host}" end @@ -658,6 +658,10 @@ module ActionView end end + def is_uri?(path) + path =~ %r{^[-a-z]+://} + end + # Pick an asset host for this source. Returns +nil+ if no host is set, # the host if no wildcard is set, the host interpolated with the # numbers 0-3 if it contains <tt>%d</tt> (the number is the source hash mod 4), @@ -798,7 +802,7 @@ module ActionView end def asset_file_path!(path) - unless path =~ %r{^[-a-z]+://} + unless is_uri?(path) absolute_path = asset_file_path(path) raise(Errno::ENOENT, "Asset file not found at '#{absolute_path}'" ) unless File.exist?(absolute_path) return absolute_path diff --git a/actionpack/test/template/asset_tag_helper_test.rb b/actionpack/test/template/asset_tag_helper_test.rb index 08963946ab..28f9d48671 100644 --- a/actionpack/test/template/asset_tag_helper_test.rb +++ b/actionpack/test/template/asset_tag_helper_test.rb @@ -83,7 +83,10 @@ class AssetTagHelperTest < ActionView::TestCase %(javascript_include_tag(:all)) => %(<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>\n<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/robber.js" type="text/javascript"></script>\n<script src="/javascripts/version.1.0.js" type="text/javascript"></script>), %(javascript_include_tag(:all, :recursive => true)) => %(<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>\n<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/robber.js" type="text/javascript"></script>\n<script src="/javascripts/subdir/subdir.js" type="text/javascript"></script>\n<script src="/javascripts/version.1.0.js" type="text/javascript"></script>), %(javascript_include_tag(:defaults, "bank")) => %(<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>), - %(javascript_include_tag("bank", :defaults)) => %(<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>) + %(javascript_include_tag("bank", :defaults)) => %(<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>), + + %(javascript_include_tag("http://example.com/all")) => %(<script src="http://example.com/all" type="text/javascript"></script>), + %(javascript_include_tag("http://example.com/all.js")) => %(<script src="http://example.com/all.js" type="text/javascript"></script>), } StylePathToTag = { @@ -111,7 +114,8 @@ class AssetTagHelperTest < ActionView::TestCase %(stylesheet_link_tag(:all, :media => "all")) => %(<link href="/stylesheets/bank.css" media="all" rel="stylesheet" type="text/css" />\n<link href="/stylesheets/robber.css" media="all" rel="stylesheet" type="text/css" />\n<link href="/stylesheets/version.1.0.css" media="all" rel="stylesheet" type="text/css" />), %(stylesheet_link_tag("random.styles", "/elsewhere/file")) => %(<link href="/stylesheets/random.styles" media="screen" rel="stylesheet" type="text/css" />\n<link href="/elsewhere/file.css" media="screen" rel="stylesheet" type="text/css" />), - %(stylesheet_link_tag("http://www.example.com/styles/style")) => %(<link href="http://www.example.com/styles/style.css" media="screen" rel="stylesheet" type="text/css" />) + %(stylesheet_link_tag("http://www.example.com/styles/style")) => %(<link href="http://www.example.com/styles/style" media="screen" rel="stylesheet" type="text/css" />), + %(stylesheet_link_tag("http://www.example.com/styles/style.css")) => %(<link href="http://www.example.com/styles/style.css" media="screen" rel="stylesheet" type="text/css" />), } ImagePathToTag = { diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index 2de19597b1..9bb4cf8b54 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -26,6 +26,7 @@ $:.unshift(activesupport_path) if File.directory?(activesupport_path) require 'active_support' module ActiveModel + autoload :AttributeMethods, 'active_model/attribute_methods' autoload :Conversion, 'active_model/conversion' autoload :DeprecatedErrorMethods, 'active_model/deprecated_error_methods' autoload :Errors, 'active_model/errors' diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb new file mode 100644 index 0000000000..de80559036 --- /dev/null +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -0,0 +1,267 @@ +module ActiveModel + class MissingAttributeError < NoMethodError + end + + module AttributeMethods + extend ActiveSupport::Concern + + # Declare and check for suffixed attribute methods. + module ClassMethods + # Defines an "attribute" method (like +inheritance_column+ or + # +table_name+). A new (class) method will be created with the + # given name. If a value is specified, the new method will + # return that value (as a string). Otherwise, the given block + # will be used to compute the value of the method. + # + # The original method will be aliased, with the new name being + # prefixed with "original_". This allows the new method to + # access the original value. + # + # Example: + # + # class A < ActiveRecord::Base + # define_attr_method :primary_key, "sysid" + # define_attr_method( :inheritance_column ) do + # original_inheritance_column + "_id" + # end + # end + def define_attr_method(name, value=nil, &block) + sing = metaclass + sing.send :alias_method, "original_#{name}", name + if block_given? + sing.send :define_method, name, &block + else + # use eval instead of a block to work around a memory leak in dev + # mode in fcgi + sing.class_eval "def #{name}; #{value.to_s.inspect}; end" + end + end + + # Declares a method available for all attributes with the given prefix. + # Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method. + # + # #{prefix}#{attr}(*args, &block) + # + # to + # + # #{prefix}attribute(#{attr}, *args, &block) + # + # An <tt>#{prefix}attribute</tt> instance method must exist and accept at least + # the +attr+ argument. + # + # For example: + # + # class Person < ActiveRecord::Base + # attribute_method_prefix 'clear_' + # + # private + # def clear_attribute(attr) + # ... + # end + # end + # + # person = Person.find(1) + # person.name # => 'Gem' + # person.clear_name + # person.name # => '' + def attribute_method_prefix(*prefixes) + attribute_method_matchers.concat(prefixes.map { |prefix| AttributeMethodMatcher.new :prefix => prefix }) + undefine_attribute_methods + end + + # Declares a method available for all attributes with the given suffix. + # Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method. + # + # #{attr}#{suffix}(*args, &block) + # + # to + # + # attribute#{suffix}(#{attr}, *args, &block) + # + # An <tt>attribute#{suffix}</tt> instance method must exist and accept at least + # the +attr+ argument. + # + # For example: + # + # class Person < ActiveRecord::Base + # attribute_method_suffix '_short?' + # + # private + # def attribute_short?(attr) + # ... + # end + # end + # + # person = Person.find(1) + # person.name # => 'Gem' + # person.name_short? # => true + def attribute_method_suffix(*suffixes) + attribute_method_matchers.concat(suffixes.map { |suffix| AttributeMethodMatcher.new :suffix => suffix }) + undefine_attribute_methods + end + + # Declares a method available for all attributes with the given prefix + # and suffix. Uses +method_missing+ and <tt>respond_to?</tt> to rewrite + # the method. + # + # #{prefix}#{attr}#{suffix}(*args, &block) + # + # to + # + # #{prefix}attribute#{suffix}(#{attr}, *args, &block) + # + # An <tt>#{prefix}attribute#{suffix}</tt> instance method must exist and + # accept at least the +attr+ argument. + # + # For example: + # + # class Person < ActiveRecord::Base + # attribute_method_affix :prefix => 'reset_', :suffix => '_to_default!' + # + # private + # def reset_attribute_to_default!(attr) + # ... + # end + # end + # + # person = Person.find(1) + # person.name # => 'Gem' + # person.reset_name_to_default! + # person.name # => 'Gemma' + def attribute_method_affix(*affixes) + attribute_method_matchers.concat(affixes.map { |affix| AttributeMethodMatcher.new :prefix => affix[:prefix], :suffix => affix[:suffix] }) + undefine_attribute_methods + end + + def define_attribute_methods(attr_names) + return if attribute_methods_generated? + attr_names.each do |name| + attribute_method_matchers.each do |method| + method_name = "#{method.prefix}#{name}#{method.suffix}" + unless instance_method_already_implemented?(method_name) + generate_method = "define_method_#{method.prefix}attribute#{method.suffix}" + + if respond_to?(generate_method) + send(generate_method, name) + else + generated_attribute_methods.module_eval("def #{method_name}(*args); send(:#{method.prefix}attribute#{method.suffix}, '#{name}', *args); end", __FILE__, __LINE__) + end + end + end + end + end + + def undefine_attribute_methods + generated_attribute_methods.module_eval do + instance_methods.each { |m| undef_method(m) } + end + @attribute_methods_generated = nil + end + + def generated_attribute_methods #:nodoc: + @generated_attribute_methods ||= begin + @attribute_methods_generated = true + mod = Module.new + include mod + mod + end + end + + def attribute_methods_generated? + @attribute_methods_generated ? true : false + end + + protected + def instance_method_already_implemented?(method_name) + method_defined?(method_name) + end + + private + class AttributeMethodMatcher + attr_reader :prefix, :suffix + + AttributeMethodMatch = Struct.new(:prefix, :base, :suffix) + + def initialize(options = {}) + options.symbolize_keys! + @prefix, @suffix = options[:prefix] || '', options[:suffix] || '' + @regex = /^(#{Regexp.escape(@prefix)})(.+?)(#{Regexp.escape(@suffix)})$/ + end + + def match(method_name) + if matchdata = @regex.match(method_name) + AttributeMethodMatch.new(matchdata[1], matchdata[2], matchdata[3]) + else + nil + end + end + end + + def attribute_method_matchers #:nodoc: + @@attribute_method_matchers ||= [] + end + end + + # Allows access to the object attributes, which are held in the <tt>@attributes</tt> hash, as though they + # were first-class methods. So a Person class with a name attribute can use Person#name and + # Person#name= and never directly use the attributes hash -- except for multiple assigns with + # ActiveRecord#attributes=. A Milestone class can also ask Milestone#completed? to test that + # the completed attribute is not +nil+ or 0. + # + # It's also possible to instantiate related objects, so a Client class belonging to the clients + # table with a +master_id+ foreign key can instantiate master through Client#master. + def method_missing(method_id, *args, &block) + method_name = method_id.to_s + if match = match_attribute_method?(method_name) + guard_private_attribute_method!(method_name, args) + return __send__("#{match.prefix}attribute#{match.suffix}", match.base, *args, &block) + end + super + end + + # A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>, + # <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt> + # which will all return +true+. + alias :respond_to_without_attributes? :respond_to? + def respond_to?(method, include_private_methods = false) + if super + return true + elsif !include_private_methods && super(method, true) + # If we're here then we haven't found among non-private methods + # but found among all methods. Which means that given method is private. + return false + elsif match_attribute_method?(method.to_s) + return true + end + super + end + + protected + def attribute_method?(attr_name) + attributes.include?(attr_name) + end + + private + # Returns a struct representing the matching attribute method. + # The struct's attributes are prefix, base and suffix. + def match_attribute_method?(method_name) + self.class.send(:attribute_method_matchers).each do |method| + if (match = method.match(method_name)) && attribute_method?(match.base) + return match + end + end + nil + end + + # 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 ActiveModel::MissingAttributeError, "missing attribute: #{attr_name}", stack + end + end +end diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index a4cf700231..a47d89ac0e 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -68,7 +68,7 @@ module ActiveModel # Will add an error message to each of the attributes in +attributes+ that is empty. def add_on_empty(attributes, custom_message = nil) [attributes].flatten.each do |attribute| - value = @base.send(attribute) + value = @base.instance_eval { read_attribute_for_validation(attribute) } is_empty = value.respond_to?(:empty?) ? value.empty? : false add(attribute, :empty, :default => custom_message) unless !value.nil? && !is_empty end @@ -77,7 +77,7 @@ module ActiveModel # Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?). def add_on_blank(attributes, custom_message = nil) [attributes].flatten.each do |attribute| - value = @base.send(attribute) + value = @base.instance_eval { read_attribute_for_validation(attribute) } add(attribute, :blank, :default => custom_message) if value.blank? end end @@ -146,7 +146,7 @@ module ActiveModel defaults = defaults.compact.flatten << :"messages.#{message}" key = defaults.shift - value = @base.send(attribute) + value = @base.instance_eval { read_attribute_for_validation(attribute) } options = { :default => defaults, :model => @base.class.name.humanize, diff --git a/activemodel/lib/active_model/state_machine.rb b/activemodel/lib/active_model/state_machine.rb index 1172d31ea3..527794b34d 100644 --- a/activemodel/lib/active_model/state_machine.rb +++ b/activemodel/lib/active_model/state_machine.rb @@ -5,12 +5,9 @@ module ActiveModel autoload :State, 'active_model/state_machine/state' autoload :StateTransition, 'active_model/state_machine/state_transition' - class InvalidTransition < Exception - end + extend ActiveSupport::Concern - def self.included(base) - require 'active_model/state_machine/machine' - base.extend ClassMethods + class InvalidTransition < Exception end module ClassMethods diff --git a/activemodel/lib/active_model/state_machine/event.rb b/activemodel/lib/active_model/state_machine/event.rb index 3eb656b6d6..30e9601dc2 100644 --- a/activemodel/lib/active_model/state_machine/event.rb +++ b/activemodel/lib/active_model/state_machine/event.rb @@ -1,5 +1,3 @@ -require 'active_model/state_machine/state_transition' - module ActiveModel module StateMachine class Event @@ -53,12 +51,12 @@ module ActiveModel self end - private - def transitions(trans_opts) - Array(trans_opts[:from]).each do |s| - @transitions << StateTransition.new(trans_opts.merge({:from => s.to_sym})) + private + def transitions(trans_opts) + Array(trans_opts[:from]).each do |s| + @transitions << StateTransition.new(trans_opts.merge({:from => s.to_sym})) + end end - end end end end diff --git a/activemodel/lib/active_model/state_machine/machine.rb b/activemodel/lib/active_model/state_machine/machine.rb index a5ede021b1..777531213e 100644 --- a/activemodel/lib/active_model/state_machine/machine.rb +++ b/activemodel/lib/active_model/state_machine/machine.rb @@ -1,6 +1,3 @@ -require 'active_model/state_machine/state' -require 'active_model/state_machine/event' - module ActiveModel module StateMachine class Machine @@ -57,22 +54,22 @@ module ActiveModel "@#{@name}_current_state" end - private - def state(name, options = {}) - @states << (state_index[name] ||= State.new(name, :machine => self)).update(options) - end + private + def state(name, options = {}) + @states << (state_index[name] ||= State.new(name, :machine => self)).update(options) + end - def event(name, options = {}, &block) - (@events[name] ||= Event.new(self, name)).update(options, &block) - end + def event(name, options = {}, &block) + (@events[name] ||= Event.new(self, name)).update(options, &block) + end - def event_fired_callback - @event_fired_callback ||= (@name == :default ? '' : "#{@name}_") + 'event_fired' - end + def event_fired_callback + @event_fired_callback ||= (@name == :default ? '' : "#{@name}_") + 'event_fired' + end - def event_failed_callback - @event_failed_callback ||= (@name == :default ? '' : "#{@name}_") + 'event_failed' - end + def event_failed_callback + @event_failed_callback ||= (@name == :default ? '' : "#{@name}_") + 'event_failed' + end end end end diff --git a/activemodel/lib/active_model/state_machine/state_transition.rb b/activemodel/lib/active_model/state_machine/state_transition.rb index f9df998ea4..b0c5504de7 100644 --- a/activemodel/lib/active_model/state_machine/state_transition.rb +++ b/activemodel/lib/active_model/state_machine/state_transition.rb @@ -18,7 +18,7 @@ module ActiveModel true end end - + def execute(obj, *args) case @on_transition when Symbol, String diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 54a869396d..0fca96e5cc 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -66,7 +66,7 @@ module ActiveModel # Declare the validation. send(validation_method(options[:on]), options) do |record| attrs.each do |attr| - value = record.send(attr) + value = record.instance_eval { read_attribute_for_validation(attr) } next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank]) yield record, attr, value end @@ -95,6 +95,28 @@ module ActiveModel def invalid? !valid? end + + protected + # Hook method defining how an attribute value should be retieved. By default this is assumed + # to be an instance named after the attribute. Override this method in subclasses should you + # need to retrieve the value for a given attribute differently e.g. + # class MyClass + # include ActiveModel::Validations + # + # def initialize(data = {}) + # @data = data + # end + # + # private + # + # def read_attribute_for_validation(key) + # @data[key] + # end + # end + # + def read_attribute_for_validation(key) + send(key) + end end end diff --git a/activemodel/test/cases/validations/presence_validation_test.rb b/activemodel/test/cases/validations/presence_validation_test.rb index aa5bdf1e62..bb6fb91774 100644 --- a/activemodel/test/cases/validations/presence_validation_test.rb +++ b/activemodel/test/cases/validations/presence_validation_test.rb @@ -54,4 +54,18 @@ class PresenceValidationTest < ActiveModel::TestCase assert p.valid? end end + + def test_validates_presence_of_for_ruby_class_with_custom_reader + repair_validations(Person) do + CustomReader.validates_presence_of :karma + + p = CustomReader.new + assert p.invalid? + + assert_equal ["can't be blank"], p.errors[:karma] + + p[:karma] = "Cold" + assert p.valid? + end + end end diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb index 8c89494247..0b340e68bf 100644 --- a/activemodel/test/cases/validations_test.rb +++ b/activemodel/test/cases/validations_test.rb @@ -5,6 +5,7 @@ require 'cases/tests_database' require 'models/topic' require 'models/reply' require 'models/developer' +require 'models/custom_reader' class ValidationsTest < ActiveModel::TestCase include ActiveModel::TestsDatabase @@ -97,6 +98,19 @@ class ValidationsTest < ActiveModel::TestCase assert_equal %w(gotcha gotcha), t.errors[:title] assert_equal %w(gotcha gotcha), t.errors[:content] end + + def test_validates_each_custom_reader + hits = 0 + CustomReader.validates_each(:title, :content, [:title, :content]) do |record, attr| + record.errors.add attr, 'gotcha' + hits += 1 + end + t = CustomReader.new("title" => "valid", "content" => "whatever") + assert !t.valid? + assert_equal 4, hits + assert_equal %w(gotcha gotcha), t.errors[:title] + assert_equal %w(gotcha gotcha), t.errors[:content] + end def test_validate_block Topic.validate { |topic| topic.errors.add("title", "will never be valid") } diff --git a/activemodel/test/models/custom_reader.rb b/activemodel/test/models/custom_reader.rb new file mode 100644 index 0000000000..7ac70e6167 --- /dev/null +++ b/activemodel/test/models/custom_reader.rb @@ -0,0 +1,17 @@ +class CustomReader + include ActiveModel::Validations + + def initialize(data = {}) + @data = data + end + + def []=(key, value) + @data[key] = value + end + + private + + def read_attribute_for_validation(key) + @data[key] + end +end
\ No newline at end of file diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 5515cf6b1a..9adc6b887f 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,7 @@ *Edge* +* quoted_date converts time-like objects to ActiveRecord::Base.default_timezone before serialization. This allows you to use Time.now in find conditions and have it correctly be serialized as the current time in UTC when default_timezone == :utc. #2946 [Geoff Buesing] + * SQLite: drop support for 'dbfile' option in favor of 'database.' #2363 [Paul Hinze, Jeremy Kemper] * Added :primary_key option to belongs_to associations. #765 [Szymon Nowak, Philip Hallstrom, Noel Rocha] diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index ab271291c5..f768c57ace 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -70,6 +70,7 @@ module ActiveRecord autoload :SchemaDumper, 'active_record/schema_dumper' autoload :Serialization, 'active_record/serialization' autoload :SessionStore, 'active_record/session_store' + autoload :StateMachine, 'active_record/state_machine' autoload :TestCase, 'active_record/test_case' autoload :Timestamp, 'active_record/timestamp' autoload :Transactions, 'active_record/transactions' diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 211b77f514..ab7ad34b9e 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -3,109 +3,13 @@ require 'active_support/core_ext/enumerable' module ActiveRecord module AttributeMethods #:nodoc: extend ActiveSupport::Concern + include ActiveModel::AttributeMethods - # Declare and check for suffixed attribute methods. module ClassMethods - # Declares a method available for all attributes with the given suffix. - # Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method - # - # #{attr}#{suffix}(*args, &block) - # - # to - # - # attribute#{suffix}(#{attr}, *args, &block) - # - # An <tt>attribute#{suffix}</tt> instance method must exist and accept at least - # the +attr+ argument. - # - # For example: - # - # class Person < ActiveRecord::Base - # attribute_method_suffix '_changed?' - # - # private - # def attribute_changed?(attr) - # ... - # end - # end - # - # person = Person.find(1) - # person.name_changed? # => false - # person.name = 'Hubert' - # person.name_changed? # => true - def attribute_method_suffix(*suffixes) - attribute_method_suffixes.concat(suffixes) - rebuild_attribute_method_regexp - undefine_attribute_methods - end - - # Defines an "attribute" method (like +inheritance_column+ or - # +table_name+). A new (class) method will be created with the - # given name. If a value is specified, the new method will - # return that value (as a string). Otherwise, the given block - # will be used to compute the value of the method. - # - # The original method will be aliased, with the new name being - # prefixed with "original_". This allows the new method to - # access the original value. - # - # Example: - # - # class A < ActiveRecord::Base - # define_attr_method :primary_key, "sysid" - # define_attr_method( :inheritance_column ) do - # original_inheritance_column + "_id" - # end - # end - def define_attr_method(name, value=nil, &block) - sing = metaclass - sing.send :alias_method, "original_#{name}", name - if block_given? - sing.send :define_method, name, &block - else - # use eval instead of a block to work around a memory leak in dev - # mode in fcgi - sing.class_eval "def #{name}; #{value.to_s.inspect}; end" - end - end - - # Returns MatchData if method_name is an attribute method. - def match_attribute_method?(method_name) - rebuild_attribute_method_regexp unless defined?(@@attribute_method_regexp) && @@attribute_method_regexp - @@attribute_method_regexp.match(method_name) - end - - def generated_methods #:nodoc: - @generated_methods ||= begin - mod = Module.new - include mod - mod - end - end - # Generates all the attribute related methods for columns in the database # accessors, mutators and query methods. def define_attribute_methods - return unless generated_methods.instance_methods.empty? - columns_hash.keys.each do |name| - attribute_method_suffixes.each do |suffix| - method_name = "#{name}#{suffix}" - unless instance_method_already_implemented?(method_name) - generate_method = "define_attribute_method#{suffix}" - if respond_to?(generate_method) - send(generate_method, name) - else - generated_methods.module_eval("def #{method_name}(*args); send(:attribute#{suffix}, '#{name}', *args); end", __FILE__, __LINE__) - end - end - end - end - end - - def undefine_attribute_methods - generated_methods.module_eval do - instance_methods.each { |m| undef_method(m) } - end + super(columns_hash.keys) end # Checks whether the method is defined in the model or any of its subclasses @@ -118,85 +22,30 @@ module ActiveRecord raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" if @@_defined_activerecord_methods.include?(method_name) @_defined_class_methods.include?(method_name) end - - private - # Suffixes a, ?, c become regexp /(a|\?|c)$/ - def rebuild_attribute_method_regexp - suffixes = attribute_method_suffixes.map { |s| Regexp.escape(s) } - @@attribute_method_regexp = /(#{suffixes.join('|')})$/.freeze - end - - def attribute_method_suffixes - @@attribute_method_suffixes ||= [] - end end - # Allows access to the object attributes, which are held in the <tt>@attributes</tt> hash, as though they - # were first-class methods. So a Person class with a name attribute can use Person#name and - # Person#name= and never directly use the attributes hash -- except for multiple assigns with - # ActiveRecord#attributes=. A Milestone class can also ask Milestone#completed? to test that - # the completed attribute is not +nil+ or 0. - # - # It's also possible to instantiate related objects, so a Client class belonging to the clients - # table with a +master_id+ foreign key can instantiate master through Client#master. def method_missing(method_id, *args, &block) - method_name = method_id.to_s - # 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.instance_methods.empty? + if !self.class.attribute_methods_generated? self.class.define_attribute_methods + method_name = method_id.to_s guard_private_attribute_method!(method_name, args) - if self.class.generated_methods.instance_methods.include?(method_name) + if self.class.generated_attribute_methods.instance_methods.include?(method_name) return self.send(method_id, *args, &block) end end - - if md = self.class.match_attribute_method?(method_name) - attribute_name, method_type = md.pre_match, md.to_s - if attribute_name == 'id' || @attributes.include?(attribute_name) - guard_private_attribute_method!(method_name, args) - return __send__("attribute#{method_type}", attribute_name, *args, &block) - end - end super end - # A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>, - # <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt> - # which will all return +true+. - alias :respond_to_without_attributes? :respond_to? - def respond_to?(method, include_private_methods = false) - method_name = method.to_s - if super - return true - elsif !include_private_methods && super(method, true) - # If we're here than we haven't found among non-private methods - # but found among all methods. Which means that given method is private. - return false - elsif self.class.generated_methods.instance_methods.empty? - self.class.define_attribute_methods - if self.class.generated_methods.instance_methods.include?(method_name) - return true - end - end - - if md = self.class.match_attribute_method?(method_name) - return true if md.pre_match == 'id' || @attributes.include?(md.pre_match) - end + def respond_to?(*args) + self.class.define_attribute_methods super 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 + protected + def attribute_method?(attr_name) + attr_name == 'id' || attributes.include?(attr_name) end end end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index b88c84938d..9ec1fbeee1 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -3,17 +3,17 @@ module ActiveRecord # Track unsaved attribute changes. # # A newly instantiated object is unchanged: - # person = Person.find_by_name('uncle bob') + # person = Person.find_by_name('Uncle Bob') # person.changed? # => false # # Change the name: # person.name = 'Bob' # person.changed? # => true # person.name_changed? # => true - # person.name_was # => 'uncle bob' - # person.name_change # => ['uncle bob', 'Bob'] + # person.name_was # => 'Uncle Bob' + # person.name_change # => ['Uncle Bob', 'Bob'] # person.name = 'Bill' - # person.name_change # => ['uncle bob', 'Bill'] + # person.name_change # => ['Uncle Bob', 'Bill'] # # Save the changes: # person.save @@ -26,21 +26,33 @@ module ActiveRecord # person.name_change # => nil # # Which attributes have changed? - # person.name = 'bob' + # person.name = 'Bob' # person.changed # => ['name'] - # person.changes # => { 'name' => ['Bill', 'bob'] } + # person.changes # => { 'name' => ['Bill', 'Bob'] } + # + # Resetting an attribute returns it to its original state: + # person.reset_name! # => 'Bill' + # person.changed? # => false + # person.name_changed? # => false + # person.name # => 'Bill' # # Before modifying an attribute in-place: # person.name_will_change! - # person.name << 'by' - # person.name_change # => ['uncle bob', 'uncle bobby'] + # person.name << 'y' + # person.name_change # => ['Bill', 'Billy'] module Dirty extend ActiveSupport::Concern - DIRTY_SUFFIXES = ['_changed?', '_change', '_will_change!', '_was'] + DIRTY_AFFIXES = [ + { :suffix => '_changed?' }, + { :suffix => '_change' }, + { :suffix => '_will_change!' }, + { :suffix => '_was' }, + { :prefix => 'reset_', :suffix => '!' } + ] included do - attribute_method_suffix *DIRTY_SUFFIXES + attribute_method_affix *DIRTY_AFFIXES alias_method_chain :save, :dirty alias_method_chain :save!, :dirty @@ -118,6 +130,11 @@ module ActiveRecord attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr) end + # Handle <tt>reset_*!</tt> for +method_missing+. + def reset_attribute!(attr) + self[attr] = changed_attributes[attr] if attribute_changed?(attr) + end + # Handle <tt>*_will_change!</tt> for +method_missing+. def attribute_will_change!(attr) changed_attributes[attr] = clone_attribute_value(:read_attribute, attr) @@ -175,9 +192,9 @@ module ActiveRecord def alias_attribute_with_dirty(new_name, old_name) alias_attribute_without_dirty(new_name, old_name) - DIRTY_SUFFIXES.each do |suffix| + DIRTY_AFFIXES.each do |affixes| module_eval <<-STR, __FILE__, __LINE__+1 - def #{new_name}#{suffix}; self.#{old_name}#{suffix}; end # def subject_changed?; self.title_changed?; end + def #{affixes[:prefix]}#{new_name}#{affixes[:suffix]}; self.#{affixes[:prefix]}#{old_name}#{affixes[:suffix]}; end # def reset_subject!; self.reset_title!; end STR end end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 6c0bf072e9..0b7d6d9094 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -36,7 +36,7 @@ module ActiveRecord end protected - def define_attribute_method(attr_name) + def define_method_attribute(attr_name) if self.serialized_attributes[attr_name] define_read_method_for_serialized_attribute(attr_name) else @@ -51,7 +51,7 @@ module ActiveRecord private # Define read method for serialized attribute. def define_read_method_for_serialized_attribute(attr_name) - generated_methods.module_eval("def #{attr_name}; unserialize_attribute('#{attr_name}'); end", __FILE__, __LINE__) + generated_attribute_methods.module_eval("def #{attr_name}; unserialize_attribute('#{attr_name}'); end", __FILE__, __LINE__) end # Define an attribute reader method. Cope with nil column. @@ -66,7 +66,7 @@ module ActiveRecord if cache_attribute?(attr_name) access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})" end - generated_methods.module_eval("def #{symbol}; #{access_code}; end", __FILE__, __LINE__) + generated_attribute_methods.module_eval("def #{symbol}; #{access_code}; end", __FILE__, __LINE__) end end diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index 4438c7543e..a8e3e28a7a 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -15,7 +15,7 @@ module ActiveRecord protected # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled. # This enhanced read method automatically converts the UTC time stored in the database to the time zone stored in Time.zone. - def define_attribute_method(attr_name) + def define_method_attribute(attr_name) if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) method_body = <<-EOV def #{attr_name}(reload = false) @@ -25,7 +25,7 @@ module ActiveRecord @attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time end EOV - generated_methods.module_eval(method_body, __FILE__, __LINE__) + generated_attribute_methods.module_eval(method_body, __FILE__, __LINE__) else super end @@ -33,7 +33,7 @@ module ActiveRecord # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled. # This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone. - def define_attribute_method=(attr_name) + def define_method_attribute=(attr_name) if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) method_body = <<-EOV def #{attr_name}=(time) @@ -44,7 +44,7 @@ module ActiveRecord write_attribute(:#{attr_name}, time) end EOV - generated_methods.module_eval(method_body, __FILE__, __LINE__) + generated_attribute_methods.module_eval(method_body, __FILE__, __LINE__) else super end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index c75745c00d..e31acac050 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -9,8 +9,8 @@ module ActiveRecord module ClassMethods protected - def define_attribute_method=(attr_name) - generated_methods.module_eval("def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", __FILE__, __LINE__) + def define_method_attribute=(attr_name) + generated_attribute_methods.module_eval("def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", __FILE__, __LINE__) end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 2f1f6175af..06bd2dab95 100755 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -148,11 +148,6 @@ module ActiveRecord #:nodoc: class DangerousAttributeError < ActiveRecordError end - # Raised when you've tried to access a column which wasn't loaded by your finder. - # Typically this is because <tt>:select</tt> has been specified. - class MissingAttributeError < NoMethodError - end - # Raised when unknown attributes are supplied via mass assignment. class UnknownAttributeError < NoMethodError end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 720fba29e9..8649f96498 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -60,7 +60,12 @@ module ActiveRecord end def quoted_date(value) - value.to_s(:db) + if value.acts_like?(:time) + zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal + value.respond_to?(zone_conversion_method) ? value.send(zone_conversion_method) : value + else + value + end.to_s(:db) end def quoted_string_prefix diff --git a/activerecord/lib/active_record/state_machine.rb b/activerecord/lib/active_record/state_machine.rb new file mode 100644 index 0000000000..aebd03344a --- /dev/null +++ b/activerecord/lib/active_record/state_machine.rb @@ -0,0 +1,24 @@ +module ActiveRecord + module StateMachine #:nodoc: + extend ActiveSupport::Concern + include ActiveModel::StateMachine + + included do + before_validation :set_initial_state + validates_presence_of :state + end + + protected + def write_state(state_machine, state) + update_attributes! :state => state.to_s + end + + def read_state(state_machine) + self.state.to_sym + end + + def set_initial_state + self.state ||= self.class.state_machine.initial_state.to_s + end + end +end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 168b617fbc..ab8768ea3e 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -4,40 +4,43 @@ require 'models/minimalistic' class AttributeMethodsTest < ActiveRecord::TestCase fixtures :topics + def setup - @old_suffixes = ActiveRecord::Base.send(:attribute_method_suffixes).dup + @old_matchers = ActiveRecord::Base.send(:attribute_method_matchers).dup @target = Class.new(ActiveRecord::Base) @target.table_name = 'topics' end def teardown - ActiveRecord::Base.send(:attribute_method_suffixes).clear - ActiveRecord::Base.attribute_method_suffix *@old_suffixes + ActiveRecord::Base.send(:attribute_method_matchers).clear + ActiveRecord::Base.send(:attribute_method_matchers).concat(@old_matchers) end - def test_match_attribute_method_query_returns_match_data - assert_not_nil md = @target.match_attribute_method?('title=') - assert_equal 'title', md.pre_match - assert_equal ['='], md.captures - - %w(_hello_world ist! _maybe?).each do |suffix| - @target.class_eval "def attribute#{suffix}(*args) args end" - @target.attribute_method_suffix suffix - - assert_not_nil md = @target.match_attribute_method?("title#{suffix}") - assert_equal 'title', md.pre_match - assert_equal [suffix], md.captures - end - end - - def test_declared_attribute_method_affects_respond_to_and_method_missing + def test_undeclared_attribute_method_does_not_affect_respond_to_and_method_missing topic = @target.new(:title => 'Budget') assert topic.respond_to?('title') assert_equal 'Budget', topic.title assert !topic.respond_to?('title_hello_world') assert_raise(NoMethodError) { topic.title_hello_world } + end + + def test_declared_prefixed_attribute_method_affects_respond_to_and_method_missing + topic = @target.new(:title => 'Budget') + %w(default_ title_).each do |prefix| + @target.class_eval "def #{prefix}attribute(*args) args end" + @target.attribute_method_prefix prefix - %w(_hello_world _it! _candidate= able?).each do |suffix| + meth = "#{prefix}title" + assert topic.respond_to?(meth) + assert_equal ['title'], topic.send(meth) + assert_equal ['title', 'a'], topic.send(meth, 'a') + assert_equal ['title', 1, 2, 3], topic.send(meth, 1, 2, 3) + end + end + + def test_declared_suffixed_attribute_method_affects_respond_to_and_method_missing + topic = @target.new(:title => 'Budget') + %w(_default _title_default _it! _candidate= able?).each do |suffix| @target.class_eval "def attribute#{suffix}(*args) args end" @target.attribute_method_suffix suffix @@ -49,6 +52,20 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end + def test_declared_affixed_attribute_method_affects_respond_to_and_method_missing + topic = @target.new(:title => 'Budget') + [['mark_', '_for_update'], ['reset_', '!'], ['default_', '_value?']].each do |prefix, suffix| + @target.class_eval "def #{prefix}attribute#{suffix}(*args) args end" + @target.attribute_method_affix({ :prefix => prefix, :suffix => suffix }) + + meth = "#{prefix}title#{suffix}" + assert topic.respond_to?(meth) + assert_equal ['title'], topic.send(meth) + assert_equal ['title', 'a'], topic.send(meth, 'a') + assert_equal ['title', 1, 2, 3], topic.send(meth, 1, 2, 3) + end + end + def test_should_unserialize_attributes_for_frozen_records myobj = {:value1 => :value2} topic = Topic.create("content" => myobj) diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 6bfe05b4fd..d5b0d58b8a 100755 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -464,6 +464,60 @@ class BasicsTest < ActiveRecord::TestCase end end + def test_preserving_time_objects_with_local_time_conversion_to_default_timezone_utc + with_env_tz 'America/New_York' do + with_active_record_default_timezone :utc do + time = Time.local(2000) + topic = Topic.create('written_on' => time) + saved_time = Topic.find(topic.id).written_on + assert_equal time, saved_time + assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "EST"], time.to_a + assert_equal [0, 0, 5, 1, 1, 2000, 6, 1, false, "UTC"], saved_time.to_a + end + end + end + + def test_preserving_time_objects_with_time_with_zone_conversion_to_default_timezone_utc + with_env_tz 'America/New_York' do + with_active_record_default_timezone :utc do + Time.use_zone 'Central Time (US & Canada)' do + time = Time.zone.local(2000) + topic = Topic.create('written_on' => time) + saved_time = Topic.find(topic.id).written_on + assert_equal time, saved_time + assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "CST"], time.to_a + assert_equal [0, 0, 6, 1, 1, 2000, 6, 1, false, "UTC"], saved_time.to_a + end + end + end + end + + def test_preserving_time_objects_with_utc_time_conversion_to_default_timezone_local + with_env_tz 'America/New_York' do + time = Time.utc(2000) + topic = Topic.create('written_on' => time) + saved_time = Topic.find(topic.id).written_on + assert_equal time, saved_time + assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "UTC"], time.to_a + assert_equal [0, 0, 19, 31, 12, 1999, 5, 365, false, "EST"], saved_time.to_a + end + end + + def test_preserving_time_objects_with_time_with_zone_conversion_to_default_timezone_local + with_env_tz 'America/New_York' do + with_active_record_default_timezone :local do + Time.use_zone 'Central Time (US & Canada)' do + time = Time.zone.local(2000) + topic = Topic.create('written_on' => time) + saved_time = Topic.find(topic.id).written_on + assert_equal time, saved_time + assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "CST"], time.to_a + assert_equal [0, 0, 1, 1, 1, 2000, 6, 1, false, "EST"], saved_time.to_a + end + end + end + end + def test_custom_mutator topic = Topic.find(1) # This mutator is protected in the class definition @@ -2115,4 +2169,19 @@ class BasicsTest < ActiveRecord::TestCase def test_dup assert !Minimalistic.new.freeze.dup.frozen? end + + protected + def with_env_tz(new_tz = 'US/Eastern') + old_tz, ENV['TZ'] = ENV['TZ'], new_tz + yield + ensure + old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') + end + + def with_active_record_default_timezone(zone) + old_zone, ActiveRecord::Base.default_timezone = ActiveRecord::Base.default_timezone, zone + yield + ensure + ActiveRecord::Base.default_timezone = old_zone + end end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index ac95bac4ad..1441421a80 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -62,6 +62,16 @@ class DirtyTest < ActiveRecord::TestCase assert_equal parrot.name_change, parrot.title_change end + def test_reset_attribute! + pirate = Pirate.create!(:catchphrase => 'Yar!') + pirate.catchphrase = 'Ahoy!' + + pirate.reset_catchphrase! + assert_equal "Yar!", pirate.catchphrase + assert_equal Hash.new, pirate.changes + assert !pirate.catchphrase_changed? + end + def test_nullable_number_not_marked_as_changed_if_new_value_is_blank pirate = Pirate.new diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index f8c8d3648a..600a9fe451 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -251,7 +251,7 @@ class FinderTest < ActiveRecord::TestCase def test_find_only_some_columns topic = Topic.find(1, :select => "author_name") - assert_raise(ActiveRecord::MissingAttributeError) {topic.title} + assert_raise(ActiveModel::MissingAttributeError) {topic.title} assert_equal "David", topic.author_name assert !topic.attribute_present?("title") #assert !topic.respond_to?("title") @@ -423,6 +423,42 @@ class FinderTest < ActiveRecord::TestCase assert_equal customers(:david), found_customer end + def test_condition_utc_time_interpolation_with_default_timezone_local + with_env_tz 'America/New_York' do + with_active_record_default_timezone :local do + topic = Topic.first + assert_equal topic, Topic.find(:first, :conditions => ['written_on = ?', topic.written_on.getutc]) + end + end + end + + def test_hash_condition_utc_time_interpolation_with_default_timezone_local + with_env_tz 'America/New_York' do + with_active_record_default_timezone :local do + topic = Topic.first + assert_equal topic, Topic.find(:first, :conditions => {:written_on => topic.written_on.getutc}) + end + end + end + + def test_condition_local_time_interpolation_with_default_timezone_utc + with_env_tz 'America/New_York' do + with_active_record_default_timezone :utc do + topic = Topic.first + assert_equal topic, Topic.find(:first, :conditions => ['written_on = ?', topic.written_on.getlocal]) + end + end + end + + def test_hash_condition_local_time_interpolation_with_default_timezone_utc + with_env_tz 'America/New_York' do + with_active_record_default_timezone :utc do + topic = Topic.first + assert_equal topic, Topic.find(:first, :conditions => {:written_on => topic.written_on.getlocal}) + end + end + end + def test_bind_variables assert_kind_of Firm, Company.find(:first, :conditions => ["name = ?", "37signals"]) assert_nil Company.find(:first, :conditions => ["name = ?", "37signals!"]) @@ -1087,4 +1123,18 @@ class FinderTest < ActiveRecord::TestCase ActiveRecord::Base.send(:replace_bind_variables, statement, vars) end end + + def with_env_tz(new_tz = 'US/Eastern') + old_tz, ENV['TZ'] = ENV['TZ'], new_tz + yield + ensure + old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') + end + + def with_active_record_default_timezone(zone) + old_zone, ActiveRecord::Base.default_timezone = ActiveRecord::Base.default_timezone, zone + yield + ensure + ActiveRecord::Base.default_timezone = old_zone + end end diff --git a/activerecord/test/cases/state_machine_test.rb b/activerecord/test/cases/state_machine_test.rb new file mode 100644 index 0000000000..5d13668bab --- /dev/null +++ b/activerecord/test/cases/state_machine_test.rb @@ -0,0 +1,42 @@ +require 'cases/helper' +require 'models/traffic_light' + +class StateMachineTest < ActiveRecord::TestCase + def setup + @light = TrafficLight.create! + end + + test "states initial state" do + assert @light.off? + assert_equal :off, @light.current_state + end + + test "transition to a valid state" do + @light.reset + assert @light.red? + assert_equal :red, @light.current_state + + @light.green_on + assert @light.green? + assert_equal :green, @light.current_state + end + + test "transition does not persist state" do + @light.reset + assert_equal :red, @light.current_state + @light.reload + assert_equal "off", @light.state + end + + test "transition does persists state" do + @light.reset! + assert_equal :red, @light.current_state + @light.reload + assert_equal "red", @light.state + end + + test "transition to an invalid state" do + assert_raise(ActiveModel::StateMachine::InvalidTransition) { @light.yellow_on } + assert_equal :off, @light.current_state + end +end diff --git a/activerecord/test/models/traffic_light.rb b/activerecord/test/models/traffic_light.rb new file mode 100644 index 0000000000..f8cfddbef9 --- /dev/null +++ b/activerecord/test/models/traffic_light.rb @@ -0,0 +1,27 @@ +class TrafficLight < ActiveRecord::Base + include ActiveRecord::StateMachine + + state_machine do + state :off + + state :red + state :green + state :yellow + + event :red_on do + transitions :to => :red, :from => [:yellow] + end + + event :green_on do + transitions :to => :green, :from => [:red] + end + + event :yellow_on do + transitions :to => :yellow, :from => [:green] + end + + event :reset do + transitions :to => :red, :from => [:off] + end + end +end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 2b7d3856b7..1e47cdbaf6 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -448,6 +448,13 @@ ActiveRecord::Schema.define do t.integer :pet_id, :integer end + create_table :traffic_lights, :force => true do |t| + t.string :location + t.string :state + t.datetime :created_at + t.datetime :updated_at + end + create_table :treasures, :force => true do |t| t.column :name, :string t.column :looter_id, :integer |