diff options
author | Emilio Tagua <miloops@gmail.com> | 2010-12-20 11:23:07 -0300 |
---|---|---|
committer | Emilio Tagua <miloops@gmail.com> | 2010-12-20 11:23:07 -0300 |
commit | 02fc6fbccdd3345e95592cc14e7855e2f1ea14b3 (patch) | |
tree | b26b91e2b2fad62ec382c9cee4ca2ac318f09257 /activemodel | |
parent | 2ba06b48defaca940e7c878724e2fb1c090eaa92 (diff) | |
parent | 0cbfd6c28d327304432f7d0c067662b5c1e41a78 (diff) | |
download | rails-02fc6fbccdd3345e95592cc14e7855e2f1ea14b3.tar.gz rails-02fc6fbccdd3345e95592cc14e7855e2f1ea14b3.tar.bz2 rails-02fc6fbccdd3345e95592cc14e7855e2f1ea14b3.zip |
Merge remote branch 'rails/master' into identity_map
Conflicts:
activerecord/lib/active_record/associations/association_proxy.rb
activerecord/lib/active_record/autosave_association.rb
activerecord/lib/active_record/base.rb
activerecord/lib/active_record/persistence.rb
Diffstat (limited to 'activemodel')
22 files changed, 204 insertions, 39 deletions
diff --git a/activemodel/CHANGELOG b/activemodel/CHANGELOG index 4e963c77b0..9dd5e03685 100644 --- a/activemodel/CHANGELOG +++ b/activemodel/CHANGELOG @@ -1,15 +1,18 @@ *Rails 3.1.0 (unreleased)* -* No changes +* Added ActiveModel::SecurePassword to encapsulate dead-simple password usage with BCrypt encryption and salting [DHH] + *Rails 3.0.2 (unreleased)* * No changes + *Rails 3.0.1 (October 15, 2010)* * No Changes, just a version bump. + *Rails 3.0.0 (August 29, 2010)* * Added ActiveModel::MassAssignmentSecurity [Eric Chapweske, Josh Kalderimis] diff --git a/activemodel/Rakefile b/activemodel/Rakefile index 0372c7a03e..0372c7a03e 100644..100755 --- a/activemodel/Rakefile +++ b/activemodel/Rakefile diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec index 318d71a610..64aa7ad922 100644 --- a/activemodel/activemodel.gemspec +++ b/activemodel/activemodel.gemspec @@ -21,5 +21,7 @@ Gem::Specification.new do |s| s.add_dependency('activesupport', version) s.add_dependency('builder', '~> 3.0.0') - s.add_dependency('i18n', '~> 0.4.2') + s.add_dependency('i18n', '~> 0.5.0') + s.add_dependency('bcrypt-ruby', '~> 2.1.2') + end diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index be0f24ff92..dd6ee058cc 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -42,6 +42,7 @@ module ActiveModel autoload :Naming autoload :Observer, 'active_model/observing' autoload :Observing + autoload :SecurePassword autoload :Serialization autoload :TestCase autoload :Translation diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index c1c5640616..fc5f5c4c66 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -46,8 +46,8 @@ module ActiveModel # end # end # - # Notice that whenever you include ActiveModel::AttributeMethods in your class, - # it requires you to implement a <tt>attributes</tt> methods which returns a hash + # Note that whenever you include ActiveModel::AttributeMethods in your class, + # it requires you to implement an <tt>attributes</tt> method which returns a hash # with each attribute name in your model as hash key and the attribute value as # hash value. # diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index 1dfd0b6132..a479795d51 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -1,5 +1,4 @@ require 'active_model/attribute_methods' -require 'active_support/concern' require 'active_support/hash_with_indifferent_access' require 'active_support/core_ext/object/duplicable' diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 99f47f2cbe..fdca852c7a 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -165,7 +165,13 @@ module ActiveModel # Returns an ActiveSupport::OrderedHash that can be used as the JSON representation for this object. def as_json(options=nil) - self + to_hash + end + + def to_hash + hash = ActiveSupport::OrderedHash.new + each { |k, v| (hash[k] ||= []) << v } + hash end # Adds +message+ to the error messages on +attribute+, which will be returned on a call to diff --git a/activemodel/lib/active_model/lint.rb b/activemodel/lib/active_model/lint.rb index d7a6da48ca..957d1b9d70 100644 --- a/activemodel/lib/active_model/lint.rb +++ b/activemodel/lib/active_model/lint.rb @@ -1,19 +1,19 @@ -# == Active Model Lint Tests -# -# You can test whether an object is compliant with the Active Model API by -# including <tt>ActiveModel::Lint::Tests</tt> in your TestCase. It will include -# tests that tell you whether your object is fully compliant, or if not, -# which aspects of the API are not implemented. -# -# These tests do not attempt to determine the semantic correctness of the -# returned values. For instance, you could implement valid? to always -# return true, and the tests would pass. It is up to you to ensure that -# the values are semantically meaningful. -# -# Objects you pass in are expected to return a compliant object from a -# call to to_model. It is perfectly fine for to_model to return self. module ActiveModel module Lint + # == Active Model Lint Tests + # + # You can test whether an object is compliant with the Active Model API by + # including <tt>ActiveModel::Lint::Tests</tt> in your TestCase. It will include + # tests that tell you whether your object is fully compliant, or if not, + # which aspects of the API are not implemented. + # + # These tests do not attempt to determine the semantic correctness of the + # returned values. For instance, you could implement valid? to always + # return true, and the tests would pass. It is up to you to ensure that + # the values are semantically meaningful. + # + # Objects you pass in are expected to return a compliant object from a + # call to to_model. It is perfectly fine for to_model to return self. module Tests # == Responds to <tt>to_key</tt> diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb new file mode 100644 index 0000000000..52941942b8 --- /dev/null +++ b/activemodel/lib/active_model/secure_password.rb @@ -0,0 +1,58 @@ +require 'bcrypt' + +module ActiveModel + module SecurePassword + extend ActiveSupport::Concern + + module ClassMethods + # Adds methods to set and authenticate against a BCrypt password. + # This mechanism requires you to have a password_digest attribute. + # + # Validations for presence of password, confirmation of password (using + # a "password_confirmation" attribute) are automatically added. + # You can add more validations by hand if need be. + # + # Example using Active Record (which automatically includes ActiveModel::SecurePassword): + # + # # Schema: User(name:string, password_digest:string) + # class User < ActiveRecord::Base + # has_secure_password + # end + # + # user = User.new(:name => "david", :password => "", :password_confirmation => "nomatch") + # user.save # => false, password required + # user.password = "mUc3m00RsqyRe" + # user.save # => false, confirmation doesn't match + # user.password_confirmation = "mUc3m00RsqyRe" + # user.save # => true + # user.authenticate("notright") # => false + # user.authenticate("mUc3m00RsqyRe") # => user + # User.find_by_name("david").try(:authenticate, "notright") # => nil + # User.find_by_name("david").try(:authenticate, "mUc3m00RsqyRe") # => user + def has_secure_password + attr_reader :password + attr_accessor :password_confirmation + + attr_protected(:password_digest) if respond_to?(:attr_protected) + + validates_confirmation_of :password + validates_presence_of :password_digest + end + end + + # Returns self if the password is correct, otherwise false. + def authenticate(unencrypted_password) + if BCrypt::Password.new(password_digest) == unencrypted_password + self + else + false + end + end + + # Encrypts the password into the password_digest attribute. + def password=(unencrypted_password) + @password = unencrypted_password + self.password_digest = BCrypt::Password.create(unencrypted_password) + end + end +end diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb index 26a134568c..b897baa614 100644 --- a/activemodel/lib/active_model/serializers/xml.rb +++ b/activemodel/lib/active_model/serializers/xml.rb @@ -17,6 +17,7 @@ module ActiveModel def initialize(name, serializable, raw_value=nil) @name, @serializable = name, serializable + raw_value = raw_value.in_time_zone if raw_value.respond_to?(:in_time_zone) @value = raw_value || @serializable.send(name) @type = compute_type end diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index b044caa8d3..6cb015a144 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -104,7 +104,7 @@ module ActiveModel # end # end # - # Or with a block which is passed with the current record to be validated: + # With a block which is passed with the current record to be validated: # # class Comment # include ActiveModel::Validations @@ -118,6 +118,16 @@ module ActiveModel # end # end # + # Or with a block where self points to the current record to be validated: + # + # class Comment + # include ActiveModel::Validations + # + # validate do + # errors.add(:base, "Must be friends to leave a comment") unless commenter.friend_of?(commentee) + # end + # end + # def validate(*args, &block) options = args.extract_options! if options.key?(:on) diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index b6aff7aa6b..95fe20de75 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -24,7 +24,7 @@ module ActiveModel def validate_each(record, attr_name, value) before_type_cast = "#{attr_name}_before_type_cast" - raw_value = record.send("#{attr_name}_before_type_cast") if record.respond_to?(before_type_cast.to_sym) + raw_value = record.send(before_type_cast) if record.respond_to?(before_type_cast.to_sym) raw_value ||= value return if options[:allow_nil] && raw_value.nil? diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index 77c5073c6e..0132f68282 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -55,6 +55,10 @@ module ActiveModel # validates :name, :title => true # end # + # Additionally validator classes may be in another namespace and still used within any class. + # + # validates :name, :'file/title' => true + # # The validators hash can also handle regular expressions, ranges, # arrays and strings in shortcut form, e.g. # @@ -86,8 +90,10 @@ module ActiveModel defaults.merge!(:attributes => attributes) validations.each do |key, options| + key = "#{key.to_s.camelize}Validator" + begin - validator = const_get("#{key.to_s.camelize}Validator") + validator = key.include?('::') ? key.constantize : const_get(key) rescue NameError raise ArgumentError, "Unknown validator: '#{key}'" end diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index 79b45bb298..27821c333b 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -62,4 +62,9 @@ class ErrorsTest < ActiveModel::TestCase end + test 'to_hash should return an ordered hash' do + person = Person.new + person.errors.add(:name, "can not be blank") + assert_instance_of ActiveSupport::OrderedHash, person.errors.to_hash + end end diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb index a81584bbad..01f0158678 100644 --- a/activemodel/test/cases/helper.rb +++ b/activemodel/test/cases/helper.rb @@ -12,9 +12,3 @@ ActiveSupport::Deprecation.debug = true require 'rubygems' require 'test/unit' - -begin - require 'ruby-debug' - Debugger.start -rescue LoadError -end diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb new file mode 100644 index 0000000000..79be715730 --- /dev/null +++ b/activemodel/test/cases/secure_password_test.rb @@ -0,0 +1,32 @@ +require 'cases/helper' +require 'models/user' + +class SecurePasswordTest < ActiveModel::TestCase + + setup do + @user = User.new + end + + test "password must be present" do + assert !@user.valid? + assert_equal 1, @user.errors.size + end + + test "password must match confirmation" do + @user.password = "thiswillberight" + @user.password_confirmation = "wrong" + + assert !@user.valid? + + @user.password_confirmation = "thiswillberight" + + assert @user.valid? + end + + test "authenticate" do + @user.password = "secret" + + assert !@user.authenticate("wrong") + assert @user.authenticate("secret") + end +end diff --git a/activemodel/test/cases/serializeration/json_serialization_test.rb b/activemodel/test/cases/serializeration/json_serialization_test.rb index 20d123ef0b..500a5c575f 100644 --- a/activemodel/test/cases/serializeration/json_serialization_test.rb +++ b/activemodel/test/cases/serializeration/json_serialization_test.rb @@ -6,6 +6,7 @@ require 'active_support/core_ext/object/instance_variables' class Contact extend ActiveModel::Naming include ActiveModel::Serializers::JSON + include ActiveModel::Validations def attributes instance_values @@ -105,15 +106,15 @@ class JsonSerializationTest < ActiveModel::TestCase end test "should return OrderedHash for errors" do - car = Automobile.new - - # run the validation - car.valid? + contact = Contact.new + contact.errors.add :name, "can't be blank" + contact.errors.add :name, "is too short (minimum is 2 characters)" + contact.errors.add :age, "must be 16 or over" hash = ActiveSupport::OrderedHash.new - hash[:make] = "can't be blank" - hash[:model] = "is too short (minimum is 2 characters)" - assert_equal hash.to_json, car.errors.to_json + hash[:name] = ["can't be blank", "is too short (minimum is 2 characters)"] + hash[:age] = ["must be 16 or over"] + assert_equal hash.to_json, contact.errors.to_json end test "serializable_hash should not modify options passed in argument" do diff --git a/activemodel/test/cases/translation_test.rb b/activemodel/test/cases/translation_test.rb index ac2e56321e..c299d6eb5e 100644 --- a/activemodel/test/cases/translation_test.rb +++ b/activemodel/test/cases/translation_test.rb @@ -17,6 +17,23 @@ class ActiveModelI18nTests < ActiveModel::TestCase assert_equal 'name default attribute', Person.human_attribute_name('name') end + def test_translated_model_attributes_using_default_option + assert_equal 'name default attribute', Person.human_attribute_name('name', :default => "name default attribute") + end + + def test_translated_model_attributes_using_default_option_as_symbol + I18n.backend.store_translations 'en', :default_name => 'name default attribute' + assert_equal 'name default attribute', Person.human_attribute_name('name', :default => :default_name) + end + + def test_translated_model_attributes_falling_back_to_default + assert_equal 'Name', Person.human_attribute_name('name') + end + + def test_translated_model_attributes_using_default_option_as_symbol_and_falling_back_to_default + assert_equal 'Name', Person.human_attribute_name('name', :default => :default_name) + end + def test_translated_model_attributes_with_symbols I18n.backend.store_translations 'en', :activemodel => {:attributes => {:person => {:name => 'person name attribute'} } } assert_equal 'person name attribute', Person.human_attribute_name(:name) diff --git a/activemodel/test/cases/validations/validates_test.rb b/activemodel/test/cases/validations/validates_test.rb index 666c48c8a0..3a9900939e 100644 --- a/activemodel/test/cases/validations/validates_test.rb +++ b/activemodel/test/cases/validations/validates_test.rb @@ -3,6 +3,7 @@ require 'cases/helper' require 'models/person' require 'models/person_with_validator' require 'validators/email_validator' +require 'validators/namespace/email_validator' class ValidatesTest < ActiveModel::TestCase setup :reset_callbacks @@ -34,6 +35,13 @@ class ValidatesTest < ActiveModel::TestCase assert_equal ['is not an email'], person.errors[:karma] end + def test_validates_with_namespaced_validator_class + Person.validates :karma, :'namespace/email' => true + person = Person.new + person.valid? + assert_equal ['is not an email'], person.errors[:karma] + end + def test_validates_with_if_as_local_conditions Person.validates :karma, :presence => true, :email => { :unless => :condition_is_true } person = Person.new diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb index 4024002aaa..e90dc7d4e3 100644 --- a/activemodel/test/cases/validations_test.rb +++ b/activemodel/test/cases/validations_test.rb @@ -148,6 +148,14 @@ class ValidationsTest < ActiveModel::TestCase end def test_validate_block + Topic.validate { errors.add("title", "will never be valid") } + t = Topic.new("title" => "Title", "content" => "whatever") + assert t.invalid? + assert t.errors[:title].any? + assert_equal ["will never be valid"], t.errors["title"] + end + + def test_validate_block_with_params Topic.validate { |topic| topic.errors.add("title", "will never be valid") } t = Topic.new("title" => "Title", "content" => "whatever") assert t.invalid? @@ -174,8 +182,8 @@ class ValidationsTest < ActiveModel::TestCase assert_match %r{<error>Content can't be blank</error>}, xml hash = ActiveSupport::OrderedHash.new - hash[:title] = "can't be blank" - hash[:content] = "can't be blank" + hash[:title] = ["can't be blank"] + hash[:content] = ["can't be blank"] assert_equal t.errors.to_json, hash.to_json end @@ -187,7 +195,7 @@ class ValidationsTest < ActiveModel::TestCase assert t.invalid? assert_equal "can't be blank", t.errors["title"].first Topic.validates_presence_of :title, :author_name - Topic.validate {|topic| topic.errors.add('author_email_address', 'will never be valid')} + Topic.validate {errors.add('author_email_address', 'will never be valid')} Topic.validates_length_of :title, :content, :minimum => 2 t = Topic.new :title => '' diff --git a/activemodel/test/models/user.rb b/activemodel/test/models/user.rb new file mode 100644 index 0000000000..e221bb8091 --- /dev/null +++ b/activemodel/test/models/user.rb @@ -0,0 +1,8 @@ +class User + include ActiveModel::Validations + include ActiveModel::SecurePassword + + has_secure_password + + attr_accessor :password_digest, :password_salt +end diff --git a/activemodel/test/validators/namespace/email_validator.rb b/activemodel/test/validators/namespace/email_validator.rb new file mode 100644 index 0000000000..57e2793ce2 --- /dev/null +++ b/activemodel/test/validators/namespace/email_validator.rb @@ -0,0 +1,6 @@ +require 'validators/email_validator' + +module Namespace + class EmailValidator < ::EmailValidator + end +end |