Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge commit 'rails/master'

  • Loading branch information...
commit ae175a5354321fd64393b9e1f4080437ee3cdf74 2 parents f8eb443 + cfd421d
@miloops miloops authored
Showing with 706 additions and 254 deletions.
  1. +2 −0  actionpack/CHANGELOG
  2. +14 −10 actionpack/lib/action_view/helpers/asset_tag_helper.rb
  3. +6 −2 actionpack/test/template/asset_tag_helper_test.rb
  4. +1 −0  activemodel/lib/active_model.rb
  5. +267 −0 activemodel/lib/active_model/attribute_methods.rb
  6. +3 −3 activemodel/lib/active_model/errors.rb
  7. +2 −5 activemodel/lib/active_model/state_machine.rb
  8. +5 −7 activemodel/lib/active_model/state_machine/event.rb
  9. +13 −16 activemodel/lib/active_model/state_machine/machine.rb
  10. +1 −1  activemodel/lib/active_model/state_machine/state_transition.rb
  11. +23 −1 activemodel/lib/active_model/validations.rb
  12. +14 −0 activemodel/test/cases/validations/presence_validation_test.rb
  13. +14 −0 activemodel/test/cases/validations_test.rb
  14. +17 −0 activemodel/test/models/custom_reader.rb
  15. +2 −0  activerecord/CHANGELOG
  16. +1 −0  activerecord/lib/active_record.rb
  17. +10 −161 activerecord/lib/active_record/attribute_methods.rb
  18. +29 −12 activerecord/lib/active_record/attribute_methods/dirty.rb
  19. +3 −3 activerecord/lib/active_record/attribute_methods/read.rb
  20. +4 −4 activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
  21. +2 −2 activerecord/lib/active_record/attribute_methods/write.rb
  22. +0 −5 activerecord/lib/active_record/base.rb
  23. +6 −1 activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
  24. +24 −0 activerecord/lib/active_record/state_machine.rb
  25. +37 −20 activerecord/test/cases/attribute_methods_test.rb
  26. +69 −0 activerecord/test/cases/base_test.rb
  27. +10 −0 activerecord/test/cases/dirty_test.rb
  28. +51 −1 activerecord/test/cases/finder_test.rb
  29. +42 −0 activerecord/test/cases/state_machine_test.rb
  30. +27 −0 activerecord/test/models/traffic_light.rb
  31. +7 −0 activerecord/test/schema/schema.rb
View
2  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]
View
24 actionpack/lib/action_view/helpers/asset_tag_helper.rb
@@ -171,7 +171,7 @@ def auto_discovery_link_tag(type = :rss, url_options = {}, tag_options = {})
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 @@ def auto_discovery_link_tag(type = :rss, url_options = {}, tag_options = {})
# 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 @@ def self.reset_javascript_include_default #:nodoc:
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 @@ def self.reset_javascript_include_default #:nodoc:
# 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 @@ def compute_public_path(source, dir, ext = nil, include_host = true)
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 @@ def compute_public_path(source, dir, ext = nil, include_host = true)
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 @@ def compute_public_path(source, dir, ext = nil, include_host = true)
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 @@ def asset_file_path(path)
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
View
8 actionpack/test/template/asset_tag_helper_test.rb
@@ -83,7 +83,10 @@ def teardown
%(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 @@ def teardown
%(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 = {
View
1  activemodel/lib/active_model.rb
@@ -26,6 +26,7 @@
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'
View
267 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
View
6 activemodel/lib/active_model/errors.rb
@@ -68,7 +68,7 @@ def add(attribute, message = nil, options = {})
# 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 @@ def add_on_empty(attributes, custom_message = nil)
# 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 @@ def generate_message(attribute, message = :invalid, options = {})
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,
View
7 activemodel/lib/active_model/state_machine.rb
@@ -5,12 +5,9 @@ module StateMachine
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
View
12 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 @@ def update(options = {}, &block)
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
View
29 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 @@ def current_state_variable
"@#{@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
View
2  activemodel/lib/active_model/state_machine/state_transition.rb
@@ -18,7 +18,7 @@ def perform(obj)
true
end
end
-
+
def execute(obj, *args)
case @on_transition
when Symbol, String
View
24 activemodel/lib/active_model/validations.rb
@@ -66,7 +66,7 @@ def validates_each(*attrs)
# 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 @@ def valid?
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
View
14 activemodel/test/cases/validations/presence_validation_test.rb
@@ -54,4 +54,18 @@ def test_validates_presence_of_for_ruby_class
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
View
14 activemodel/test/cases/validations_test.rb
@@ -5,6 +5,7 @@
require 'models/topic'
require 'models/reply'
require 'models/developer'
+require 'models/custom_reader'
class ValidationsTest < ActiveModel::TestCase
include ActiveModel::TestsDatabase
@@ -97,6 +98,19 @@ def test_validates_each
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") }
View
17 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
View
2  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]
View
1  activerecord/lib/active_record.rb
@@ -70,6 +70,7 @@ def self.load_all!
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'
View
171 activerecord/lib/active_record/attribute_methods.rb
@@ -3,109 +3,13 @@
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 @@ def instance_method_already_implemented?(method_name)
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
View
41 activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -3,17 +3,17 @@ module AttributeMethods
# 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 AttributeMethods
# 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 @@ def attribute_was(attr)
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 @@ class << base
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
View
6 activerecord/lib/active_record/attribute_methods/read.rb
@@ -36,7 +36,7 @@ def cache_attribute?(attr_name)
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 @@ def define_attribute_method(attr_name)
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 @@ def define_read_method(symbol, attr_name, column)
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
View
8 activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -15,7 +15,7 @@ module ClassMethods
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 @@ def #{attr_name}(reload = false)
@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 @@ def #{attr_name}(reload = false)
# 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 @@ def #{attr_name}=(time)
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
View
4 activerecord/lib/active_record/attribute_methods/write.rb
@@ -9,8 +9,8 @@ module Write
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
View
5 activerecord/lib/active_record/base.rb
@@ -148,11 +148,6 @@ class Rollback < ActiveRecordError
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
View
7 activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -60,7 +60,12 @@ def quoted_false
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
View
24 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
View
57 activerecord/test/cases/attribute_methods_test.rb
@@ -4,40 +4,43 @@
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 @@ def test_declared_attribute_method_affects_respond_to_and_method_missing
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)
View
69 activerecord/test/cases/base_test.rb
@@ -464,6 +464,60 @@ def test_preserving_time_objects
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 @@ def test_create_with_custom_timestamps
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
View
10 activerecord/test/cases/dirty_test.rb
@@ -62,6 +62,16 @@ def test_aliased_attribute_changes
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
View
52 activerecord/test/cases/finder_test.rb
@@ -251,7 +251,7 @@ def test_unexisting_record_exception_handling
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 @@ def test_hash_condition_find_with_one_condition_being_aggregate_and_another_not
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 @@ def bind(statement, *vars)
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
View
42 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
View
27 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
View
7 activerecord/test/schema/schema.rb
@@ -448,6 +448,13 @@ def create_table(*args, &block)
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
Please sign in to comment.
Something went wrong with that request. Please try again.