1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Merge commit 'rails/master'

This commit is contained in:
Emilio Tagua 2009-08-10 18:19:24 -03:00
commit 10af9fae4a
11 changed files with 247 additions and 186 deletions

View file

@ -1,5 +1,11 @@
*Edge*
* Add validates_format_of :without => /regexp/ option. #430 [Elliot Winkler, Peer Allan]
Example :
validates_format_of :subdomain, :without => /www|admin|mail/
* Introduce validates_with to encapsulate attribute validations in a class. #2630 [Jeff Dean]
* Extracted from Active Record and Active Resource.

View file

@ -29,6 +29,7 @@ module ActiveModel
autoload :AttributeMethods, 'active_model/attribute_methods'
autoload :Conversion, 'active_model/conversion'
autoload :DeprecatedErrorMethods, 'active_model/deprecated_error_methods'
autoload :Dirty, 'active_model/dirty'
autoload :Errors, 'active_model/errors'
autoload :Name, 'active_model/naming'
autoload :Naming, 'active_model/naming'

View file

@ -133,18 +133,31 @@ module ActiveModel
undefine_attribute_methods
end
def alias_attribute(new_name, old_name)
attribute_method_matchers.each do |matcher|
module_eval <<-STR, __FILE__, __LINE__+1
def #{matcher.method_name(new_name)}(*args)
send(:#{matcher.method_name(old_name)}, *args)
end
STR
end
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}"
attr_names.each do |attr_name|
attribute_method_matchers.each do |matcher|
unless instance_method_already_implemented?(matcher.method_name(attr_name))
generate_method = "define_method_#{matcher.prefix}attribute#{matcher.suffix}"
if respond_to?(generate_method)
send(generate_method, name)
send(generate_method, attr_name)
else
generated_attribute_methods.module_eval("def #{method_name}(*args); send(:#{method.prefix}attribute#{method.suffix}, '#{name}', *args); end", __FILE__, __LINE__)
generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__+1
def #{matcher.method_name(attr_name)}(*args)
send(:#{matcher.method_missing_target}, '#{attr_name}', *args)
end
STR
end
end
end
@ -180,7 +193,7 @@ module ActiveModel
class AttributeMethodMatcher
attr_reader :prefix, :suffix
AttributeMethodMatch = Struct.new(:prefix, :base, :suffix)
AttributeMethodMatch = Struct.new(:target, :attr_name)
def initialize(options = {})
options.symbolize_keys!
@ -190,11 +203,19 @@ module ActiveModel
def match(method_name)
if matchdata = @regex.match(method_name)
AttributeMethodMatch.new(matchdata[1], matchdata[2], matchdata[3])
AttributeMethodMatch.new(method_missing_target, matchdata[2])
else
nil
end
end
def method_name(attr_name)
"#{prefix}#{attr_name}#{suffix}"
end
def method_missing_target
:"#{prefix}attribute#{suffix}"
end
end
def attribute_method_matchers #:nodoc:
@ -214,7 +235,7 @@ module ActiveModel
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)
return __send__(match.target, match.attr_name, *args, &block)
end
super
end
@ -246,7 +267,7 @@ module ActiveModel
# 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)
if (match = method.match(method_name)) && attribute_method?(match.attr_name)
return match
end
end

View file

@ -0,0 +1,112 @@
module ActiveModel
# Track unsaved attribute changes.
#
# A newly instantiated object is unchanged:
# 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 = 'Bill'
# person.name_change # => ['Uncle Bob', 'Bill']
#
# Save the changes:
# person.save
# person.changed? # => false
# person.name_changed? # => false
#
# Assigning the same value leaves the attribute unchanged:
# person.name = 'Bill'
# person.name_changed? # => false
# person.name_change # => nil
#
# Which attributes have changed?
# person.name = 'Bob'
# person.changed # => ['name']
# 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 << 'y'
# person.name_change # => ['Bill', 'Billy']
module Dirty
extend ActiveSupport::Concern
include ActiveModel::AttributeMethods
included do
attribute_method_suffix '_changed?', '_change', '_will_change!', '_was'
attribute_method_affix :prefix => 'reset_', :suffix => '!'
end
# Do any attributes have unsaved changes?
# person.changed? # => false
# person.name = 'bob'
# person.changed? # => true
def changed?
!changed_attributes.empty?
end
# List of attributes with unsaved changes.
# person.changed # => []
# person.name = 'bob'
# person.changed # => ['name']
def changed
changed_attributes.keys
end
# Map of changed attrs => [original value, new value].
# person.changes # => {}
# person.name = 'bob'
# person.changes # => { 'name' => ['bill', 'bob'] }
def changes
changed.inject({}) { |h, attr| h[attr] = attribute_change(attr); h }
end
private
# Map of change <tt>attr => original value</tt>.
def changed_attributes
@changed_attributes ||= {}
end
# Handle <tt>*_changed?</tt> for +method_missing+.
def attribute_changed?(attr)
changed_attributes.include?(attr)
end
# Handle <tt>*_change</tt> for +method_missing+.
def attribute_change(attr)
[changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
end
# Handle <tt>*_was</tt> for +method_missing+.
def attribute_was(attr)
attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
end
# Handle <tt>*_will_change!</tt> for +method_missing+.
def attribute_will_change!(attr)
begin
value = __send__(attr)
value = value.duplicable? ? value.clone : value
rescue TypeError, NoMethodError
end
changed_attributes[attr] = value
end
# Handle <tt>reset_*!</tt> for +method_missing+.
def reset_attribute!(attr)
__send__("#{attr}=", changed_attributes[attr]) if attribute_changed?(attr)
end
end
end

View file

@ -1,22 +1,30 @@
module ActiveModel
module Validations
module ClassMethods
# Validates whether the value of the specified attribute is of the correct form by matching it against the regular expression
# provided.
# Validates whether the value of the specified attribute is of the correct form, going by the regular expression provided.
# You can require that the attribute matches the regular expression:
#
# class Person < ActiveRecord::Base
# validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create
# end
#
# Alternatively, you can require that the specified attribute does _not_ match the regular expression:
#
# class Person < ActiveRecord::Base
# validates_format_of :email, :without => /NOSPAM/
# end
#
# Note: use <tt>\A</tt> and <tt>\Z</tt> to match the start and end of the string, <tt>^</tt> and <tt>$</tt> match the start/end of a line.
#
# A regular expression must be provided or else an exception will be raised.
# You must pass either <tt>:with</tt> or <tt>:without</tt> as an option. In addition, both must be a regular expression,
# or else an exception will be raised.
#
# Configuration options:
# * <tt>:message</tt> - A custom error message (default is: "is invalid").
# * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
# * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+).
# * <tt>:with</tt> - The regular expression used to validate the format with (note: must be supplied!).
# * <tt>:with</tt> - Regular expression that if the attribute matches will result in a successful validation.
# * <tt>:without</tt> - Regular expression that if the attribute does not match will result in a successful validation.
# * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
# occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
@ -25,14 +33,27 @@ module ActiveModel
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
def validates_format_of(*attr_names)
configuration = { :with => nil }
configuration.update(attr_names.extract_options!)
configuration = attr_names.extract_options!
raise(ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash") unless configuration[:with].is_a?(Regexp)
unless configuration.include?(:with) ^ configuration.include?(:without) # ^ == xor, or "exclusive or"
raise ArgumentError, "Either :with or :without must be supplied (but not both)"
end
validates_each(attr_names, configuration) do |record, attr_name, value|
unless value.to_s =~ configuration[:with]
record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value)
if configuration[:with] && !configuration[:with].is_a?(Regexp)
raise ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash"
end
if configuration[:without] && !configuration[:without].is_a?(Regexp)
raise ArgumentError, "A regular expression must be supplied as the :without option of the configuration hash"
end
if configuration[:with]
validates_each(attr_names, configuration) do |record, attr_name, value|
record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value) if value.to_s !~ configuration[:with]
end
elsif configuration[:without]
validates_each(attr_names, configuration) do |record, attr_name, value|
record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value) if value.to_s =~ configuration[:without]
end
end
end

View file

@ -71,6 +71,35 @@ class PresenceValidationTest < ActiveModel::TestCase
assert_equal ["can't be Invalid title"], t.errors[:title]
end
def test_validate_format_with_not_option
Topic.validates_format_of(:title, :without => /foo/, :message => "should not contain foo")
t = Topic.new
t.title = "foobar"
t.valid?
assert_equal ["should not contain foo"], t.errors[:title]
t.title = "something else"
t.valid?
assert_equal [], t.errors[:title]
end
def test_validate_format_of_without_any_regexp_should_raise_error
assert_raise(ArgumentError) { Topic.validates_format_of(:title) }
end
def test_validates_format_of_with_both_regexps_should_raise_error
assert_raise(ArgumentError) { Topic.validates_format_of(:title, :with => /this/, :without => /that/) }
end
def test_validates_format_of_when_with_isnt_a_regexp_should_raise_error
assert_raise(ArgumentError) { Topic.validates_format_of(:title, :with => "clearly not a regexp") }
end
def test_validates_format_of_when_not_isnt_a_regexp_should_raise_error
assert_raise(ArgumentError) { Topic.validates_format_of(:title, :without => "clearly not a regexp") }
end
def test_validates_format_of_with_custom_error_using_quotes
repair_validations(Developer) do
Developer.validates_format_of :name, :with => /^(A-Z*)$/, :message=> "format 'single' and \"double\" quotes"

View file

@ -2199,7 +2199,7 @@ during calendar reform. #7649, #7724 [fedot, Geoff Buesing]
* Escape database name in MySQL adapter when creating and dropping databases. #3409 [anna@wota.jp]
* Disambiguate table names for columns in validates_uniquness_of's WHERE clause. #3423 [alex.borovsky@gmail.com]
* Disambiguate table names for columns in validates_uniqueness_of's WHERE clause. #3423 [alex.borovsky@gmail.com]
* .with_scope imposed create parameters now bypass attr_protected [Tobias Lütke]
@ -3714,7 +3714,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio
* Escape database name in MySQL adapter when creating and dropping databases. #3409 [anna@wota.jp]
* Disambiguate table names for columns in validates_uniquness_of's WHERE clause. #3423 [alex.borovsky@gmail.com]
* Disambiguate table names for columns in validates_uniqueness_of's WHERE clause. #3423 [alex.borovsky@gmail.com]
* .with_scope imposed create parameters now bypass attr_protected [Tobias Lütke]

View file

@ -8,25 +8,11 @@ module ActiveRecord
alias_method :new, :build
def create!(attrs = nil)
ensure_owner_is_not_new
transaction do
self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.create_association! } : @reflection.create_association!)
object
end
create_record(attrs, true)
end
def create(attrs = nil)
ensure_owner_is_not_new
transaction do
object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.create_association } : @reflection.create_association
raise_on_type_mismatch(object)
add_record_to_target_with_callbacks(object) do |r|
insert_record(object, false)
end
object
end
create_record(attrs, false)
end
def destroy(*records)
@ -46,6 +32,16 @@ module ActiveRecord
end
protected
def create_record(attrs, force = true)
ensure_owner_is_not_new
transaction do
object = @reflection.klass.new(attrs)
add_record_to_target_with_callbacks(object) {|r| insert_record(object, force) }
object
end
end
def target_reflection_has_associated_record?
if @reflection.through_reflection.macro == :belongs_to && @owner[@reflection.through_reflection.primary_key_name].blank?
false
@ -69,9 +65,10 @@ module ActiveRecord
return false unless record.save(validate)
end
end
through_reflection = @reflection.through_reflection
klass = through_reflection.klass
@owner.send(@reflection.through_reflection.name).proxy_target << klass.send(:with_scope, :create => construct_join_attributes(record)) { through_reflection.create_association! }
through_association = @owner.send(@reflection.through_reflection.name)
through_record = through_association.create!(construct_join_attributes(record))
through_association.proxy_target << through_record
end
# TODO - add dependent option support

View file

@ -1,92 +1,21 @@
require 'active_support/core_ext/object/tap'
module ActiveRecord
module AttributeMethods
# Track unsaved attribute changes.
#
# A newly instantiated object is unchanged:
# 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 = 'Bill'
# person.name_change # => ['Uncle Bob', 'Bill']
#
# Save the changes:
# person.save
# person.changed? # => false
# person.name_changed? # => false
#
# Assigning the same value leaves the attribute unchanged:
# person.name = 'Bill'
# person.name_changed? # => false
# person.name_change # => nil
#
# Which attributes have changed?
# person.name = 'Bob'
# person.changed # => ['name']
# 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 << 'y'
# person.name_change # => ['Bill', 'Billy']
module Dirty
extend ActiveSupport::Concern
DIRTY_AFFIXES = [
{ :suffix => '_changed?' },
{ :suffix => '_change' },
{ :suffix => '_will_change!' },
{ :suffix => '_was' },
{ :prefix => 'reset_', :suffix => '!' }
]
include ActiveModel::Dirty
included do
attribute_method_affix *DIRTY_AFFIXES
alias_method_chain :save, :dirty
alias_method_chain :save!, :dirty
alias_method_chain :update, :dirty
alias_method_chain :reload, :dirty
alias_method_chain :save, :dirty
alias_method_chain :save!, :dirty
alias_method_chain :update, :dirty
alias_method_chain :reload, :dirty
superclass_delegating_accessor :partial_updates
self.partial_updates = true
end
# Do any attributes have unsaved changes?
# person.changed? # => false
# person.name = 'bob'
# person.changed? # => true
def changed?
!changed_attributes.empty?
end
# List of attributes with unsaved changes.
# person.changed # => []
# person.name = 'bob'
# person.changed # => ['name']
def changed
changed_attributes.keys
end
# Map of changed attrs => [original value, new value].
# person.changes # => {}
# person.name = 'bob'
# person.changes # => { 'name' => ['bill', 'bob'] }
def changes
changed.inject({}) { |h, attr| h[attr] = attribute_change(attr); h }
end
# Attempts to +save+ the record and clears changed attributes if successful.
def save_with_dirty(*args) #:nodoc:
if status = save_without_dirty(*args)
@ -97,49 +26,15 @@ module ActiveRecord
# Attempts to <tt>save!</tt> the record and clears changed attributes if successful.
def save_with_dirty!(*args) #:nodoc:
status = save_without_dirty!(*args)
changed_attributes.clear
status
save_without_dirty!(*args).tap { changed_attributes.clear }
end
# <tt>reload</tt> the record and clears changed attributes.
def reload_with_dirty(*args) #:nodoc:
record = reload_without_dirty(*args)
changed_attributes.clear
record
reload_without_dirty(*args).tap { changed_attributes.clear }
end
private
# Map of change <tt>attr => original value</tt>.
def changed_attributes
@changed_attributes ||= {}
end
# Handle <tt>*_changed?</tt> for +method_missing+.
def attribute_changed?(attr)
changed_attributes.include?(attr)
end
# Handle <tt>*_change</tt> for +method_missing+.
def attribute_change(attr)
[changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
end
# Handle <tt>*_was</tt> for +method_missing+.
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)
end
# Wrap write_attribute to remember original attribute value.
def write_attribute(attr, value)
attr = attr.to_s
@ -182,23 +77,6 @@ module ActiveRecord
old != value
end
module ClassMethods
def self.extended(base)
class << base
alias_method_chain :alias_attribute, :dirty
end
end
def alias_attribute_with_dirty(new_name, old_name)
alias_attribute_without_dirty(new_name, old_name)
DIRTY_AFFIXES.each do |affixes|
module_eval <<-STR, __FILE__, __LINE__+1
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
end
end
end
end

View file

@ -180,27 +180,23 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
def test_associate_with_create_and_invalid_options
peeps = companies(:first_firm).developers.count
assert_nothing_raised { companies(:first_firm).developers.create(:name => '0') }
assert_equal peeps, companies(:first_firm).developers.count
firm = companies(:first_firm)
assert_no_difference('firm.developers.count') { assert_nothing_raised { firm.developers.create(:name => '0') } }
end
def test_associate_with_create_and_valid_options
peeps = companies(:first_firm).developers.count
assert_nothing_raised { companies(:first_firm).developers.create(:name => 'developer') }
assert_equal peeps + 1, companies(:first_firm).developers.count
firm = companies(:first_firm)
assert_difference('firm.developers.count', 1) { firm.developers.create(:name => 'developer') }
end
def test_associate_with_create_bang_and_invalid_options
peeps = companies(:first_firm).developers.count
assert_raises(ActiveRecord::RecordInvalid) { companies(:first_firm).developers.create!(:name => '0') }
assert_equal peeps, companies(:first_firm).developers.count
firm = companies(:first_firm)
assert_no_difference('firm.developers.count') { assert_raises(ActiveRecord::RecordInvalid) { firm.developers.create!(:name => '0') } }
end
def test_associate_with_create_bang_and_valid_options
peeps = companies(:first_firm).developers.count
assert_nothing_raised { companies(:first_firm).developers.create!(:name => 'developer') }
assert_equal peeps + 1, companies(:first_firm).developers.count
firm = companies(:first_firm)
assert_difference('firm.developers.count', 1) { firm.developers.create!(:name => 'developer') }
end
def test_clear_associations

View file

@ -59,7 +59,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert t2.save, "Should now save t2 as unique"
end
def test_validates_uniquness_with_newline_chars
def test_validates_uniqueness_with_newline_chars
Topic.validates_uniqueness_of(:title, :case_sensitive => false)
t = Topic.new("title" => "new\nline")