diff options
Diffstat (limited to 'activemodel')
29 files changed, 392 insertions, 160 deletions
diff --git a/activemodel/CHANGELOG b/activemodel/CHANGELOG index a5e7f300d9..8374853231 100644 --- a/activemodel/CHANGELOG +++ b/activemodel/CHANGELOG @@ -1,3 +1,8 @@ +*Rails 3.0.0 [release candidate] (July 26th, 2010)* + +* Added ActiveModel::MassAssignmentSecurity [Eric Chapweske, Josh Kalderimis] + + *Rails 3.0.0 [beta 4] (June 8th, 2010)* * JSON supports a custom root option: to_json(:root => 'custom') #4515 [Jatinder Singh] diff --git a/activemodel/README b/activemodel/README.rdoc index 6f162ef408..89cacbcab4 100644 --- a/activemodel/README +++ b/activemodel/README.rdoc @@ -1,21 +1,21 @@ -= Active Model - defined interfaces for Rails - -Prior to Rails 3.0, if a plugin or gem developer wanted to be able to have -an object interact with Action Pack helpers, it was required to either -copy chunks of code from Rails, or monkey patch entire helpers to make them -handle objects that did not look like Active Record. This generated code -duplication and fragile applications that broke on upgrades. - -Active Model is a solution for this problem. - -Active Model provides a known set of interfaces that your objects can implement -to then present a common interface to the Action Pack helpers. You can include -functionality from the following modules: - -* Adding attribute magic to your objects - - Add prefixes and suffixes to defined attribute methods... - += Active Model -- model interfaces for Rails + +Active Model provides a known set of interfaces for usage in model classes. +They allow for Action Pack helpers to interact with non-ActiveRecord models, +for example. Active Model also helps building custom ORMs for use outside of +the Rails framework. + +Prior to Rails 3.0, if a plugin or gem developer wanted to have an object +interact with Action Pack helpers, it was required to either copy chunks of +code from Rails, or monkey patch entire helpers to make them handle objects +that did not exacly conform to the Active Record interface. This would result +in code duplication and fragile applications that broke on upgrades. + +Active Model solves this. You can include functionality from the following +modules: + +* Add attribute magic to objects + class Person include ActiveModel::AttributeMethods @@ -23,17 +23,18 @@ functionality from the following modules: define_attribute_methods [:name, :age] attr_accessor :name, :age - + def clear_attribute(attr) send("#{attr}=", nil) end end - ...gives you clear_name, clear_age. + person.clear_name + person.clear_age {Learn more}[link:classes/ActiveModel/AttributeMethods.html] -* Adding callbacks to your objects +* Callbacks for certain operations class Person extend ActiveModel::Callbacks @@ -45,26 +46,16 @@ functionality from the following modules: end end end - - ...gives you before_create, around_create and after_create class methods that - wrap your create method. - + + This generates +before_create+, +around_create+ and +after_create+ + class methods that wrap your create method. + {Learn more}[link:classes/ActiveModel/CallBacks.html] -* For classes that already look like an Active Record object +* Tracking value changes - class Person - include ActiveModel::Conversion - end - - ...returns the class itself when sent :to_model - - {Learn more}[link:classes/ActiveModel/Conversion.html] + The ActiveModel::Dirty module allows for tracking attribute changes: -* Tracking changes in your object - - Provides all the value tracking features implemented by ActiveRecord... - person = Person.new person.name # => nil person.changed? # => false @@ -75,14 +66,14 @@ functionality from the following modules: person.name = 'robert' person.save person.previous_changes # => {'name' => ['bob, 'robert']} - + {Learn more}[link:classes/ActiveModel/Dirty.html] -* Adding +errors+ support to your object +* Adding +errors+ interface to objects - Provides the error messages to allow your object to interact with Action Pack - helpers seamlessly... - + Exposing error messages allows objects to interact with Action Pack + helpers seamlessly. + class Person def initialize @@ -102,51 +93,38 @@ functionality from the following modules: end - ... gives you... - person.errors.full_messages # => ["Name Can not be nil"] + person.errors.full_messages # => ["Name Can not be nil"] {Learn more}[link:classes/ActiveModel/Errors.html] -* Testing the compliance of your object +* Model name introspection - Use ActiveModel::Lint to test the compliance of your object to the - basic ActiveModel API... - - {Learn more}[link:classes/ActiveModel/Lint/Tests.html] - -* Providing a human face to your object - - ActiveModel::Naming provides your model with the model_name convention - and a human_name attribute... - class NamedPerson extend ActiveModel::Naming end - ...gives you... - NamedPerson.model_name #=> "NamedPerson" NamedPerson.model_name.human #=> "Named person" {Learn more}[link:classes/ActiveModel/Naming.html] -* Adding observer support to your objects +* Observer support - ActiveModel::Observers allows your object to implement the Observer - pattern in a Rails App and take advantage of all the standard observer - functions. + ActiveModel::Observers allows your object to implement the Observer + pattern in a Rails App and take advantage of all the standard observer + functions. {Learn more}[link:classes/ActiveModel/Observer.html] -* Making your object serializable +* Making objects serializable - ActiveModel::Serialization provides a standard interface for your object - to provide to_json or to_xml serialization... - + ActiveModel::Serialization provides a standard interface for your object + to provide +to_json+ or +to_xml+ serialization. + s = SerialPerson.new s.serializable_hash # => {"name"=>nil} s.to_json # => "{\"name\":null}" @@ -154,36 +132,36 @@ functionality from the following modules: {Learn more}[link:classes/ActiveModel/Serialization.html] -* Integrating with Rail's internationalization (i18n) handling through - ActiveModel::Translations... +* Internationalization (i18n) support class Person extend ActiveModel::Translation end + + Person.human_attribute_name('my_attribute') + #=> "My attribute" {Learn more}[link:classes/ActiveModel/Translation.html] -* Providing a full Validation stack for your objects... +* Validation support class Person include ActiveModel::Validations attr_accessor :first_name, :last_name - validates_each :first_name, :last_name do |record, attr, value| record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z end end - person = Person.new person.first_name = 'zoolander' - person.valid? #=> false + person.valid? #=> false {Learn more}[link:classes/ActiveModel/Validations.html] -* Make custom validators +* Custom validators class Person include ActiveModel::Validations @@ -196,7 +174,7 @@ functionality from the following modules: record.errors[:name] = "must exist" if record.name.blank? end end - + p = ValidatorPerson.new p.valid? #=> false p.errors.full_messages #=> ["Name must exist"] diff --git a/activemodel/Rakefile b/activemodel/Rakefile index 1dba664539..3fffc0d021 100644 --- a/activemodel/Rakefile +++ b/activemodel/Rakefile @@ -1,6 +1,6 @@ dir = File.dirname(__FILE__) -gem 'rdoc', '= 2.2' +gem 'rdoc', '>= 2.5.9' require 'rdoc' require 'rake/testtask' @@ -23,16 +23,16 @@ namespace :test do end -require 'rake/rdoctask' +require 'rdoc/task' # Generate the RDoc documentation -Rake::RDocTask.new do |rdoc| +RDoc::Task.new do |rdoc| rdoc.rdoc_dir = "doc" rdoc.title = "Active Model" - rdoc.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object' + rdoc.options << '-f' << 'horo' rdoc.options << '--charset' << 'utf-8' - rdoc.template = ENV['template'] ? "#{ENV['template']}.rb" : '../doc/template/horo' - rdoc.rdoc_files.include("README", "CHANGELOG") + rdoc.options << '--main' << 'README.rdoc' + rdoc.rdoc_files.include("README.rdoc", "CHANGELOG") rdoc.rdoc_files.include("lib/**/*.rb") end diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec index 6bd6fe49ff..c483ecbc3c 100644 --- a/activemodel/activemodel.gemspec +++ b/activemodel/activemodel.gemspec @@ -14,7 +14,7 @@ Gem::Specification.new do |s| s.homepage = 'http://www.rubyonrails.org' s.rubyforge_project = 'activemodel' - s.files = Dir['CHANGELOG', 'MIT-LICENSE', 'README', 'lib/**/*'] + s.files = Dir['CHANGELOG', 'MIT-LICENSE', 'README.rdoc', 'lib/**/*'] s.require_path = 'lib' s.has_rdoc = true diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index 817640b178..a43436e008 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -283,7 +283,7 @@ module ActiveModel @attribute_methods_generated = true end - # Removes all the preiously dynamically defined methods from the class + # Removes all the previously dynamically defined methods from the class def undefine_attribute_methods generated_attribute_methods.module_eval do instance_methods.each { |m| undef_method(m) } diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index e7aad17021..8c10c54b54 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -19,7 +19,7 @@ module ActiveModel # # define_model_callbacks :create, :update # - # This will provide all three standard callbacks (before, around and after) around + # This will provide all three standard callbacks (before, around and after) for # both the :create and :update methods. To implement, you need to wrap the methods # you want callbacks on in a block so that the callbacks get a chance to fire: # diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb index 2a1650faa9..d2bd160dc7 100644 --- a/activemodel/lib/active_model/conversion.rb +++ b/activemodel/lib/active_model/conversion.rb @@ -17,9 +17,9 @@ module ActiveModel # end # # cm = ContactMessage.new - # cm.to_model == self #=> true - # cm.to_key #=> nil - # cm.to_param #=> nil + # cm.to_model == self # => true + # cm.to_key # => nil + # cm.to_param # => nil # module Conversion # If your object is already designed to implement all of the Active Model diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index 4c80863e3a..2516377afd 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -37,12 +37,13 @@ module ActiveModel # end # # def name=(val) - # name_will_change! + # name_will_change! unless val == @name # @name = val # end # # def save # @previously_changed = changes + # @changed_attributes.clear # end # # end @@ -77,13 +78,10 @@ module ActiveModel # 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' + # If an attribute is modified in-place then make use of <tt>[attribute_name]_will_change!</tt> + # to mark that the attribute is changing. Otherwise ActiveModel can't track changes to + # in-place attributes. # - # Before modifying an attribute in-place: # person.name_will_change! # person.name << 'y' # person.name_change # => ['Bill', 'Billy'] diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 482b3dac47..272ddb1554 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -37,11 +37,11 @@ module ActiveModel # send(attr) # end # - # def ErrorsPerson.human_attribute_name(attr, options = {}) + # def Person.human_attribute_name(attr, options = {}) # attr # end # - # def ErrorsPerson.lookup_ancestors + # def Person.lookup_ancestors # [self] # end # @@ -83,20 +83,16 @@ module ActiveModel # When passed a symbol or a name of a method, returns an array of errors # for the method. # - # p.errors[:name] #=> ["can not be nil"] - # p.errors['name'] #=> ["can not be nil"] + # p.errors[:name] # => ["can not be nil"] + # p.errors['name'] # => ["can not be nil"] def [](attribute) - if errors = get(attribute.to_sym) - errors - else - set(attribute.to_sym, []) - end + get(attribute.to_sym) || set(attribute.to_sym, []) end # Adds to the supplied attribute the supplied error message. # # p.errors[:name] = "must be set" - # p.errors[:name] #=> ['must be set'] + # p.errors[:name] # => ['must be set'] def []=(attribute, error) self[attribute.to_sym] << error end @@ -124,9 +120,9 @@ module ActiveModel # Returns the number of error messages. # # p.errors.add(:name, "can't be blank") - # p.errors.size #=> 1 + # p.errors.size # => 1 # p.errors.add(:name, "must be specified") - # p.errors.size #=> 2 + # p.errors.size # => 2 def size values.flatten.size end @@ -135,16 +131,16 @@ module ActiveModel # # p.errors.add(:name, "can't be blank") # p.errors.add(:name, "must be specified") - # p.errors.to_a #=> ["name can't be blank", "name must be specified"] + # p.errors.to_a # => ["name can't be blank", "name must be specified"] def to_a full_messages end # Returns the number of error messages. # p.errors.add(:name, "can't be blank") - # p.errors.count #=> 1 + # p.errors.count # => 1 # p.errors.add(:name, "must be specified") - # p.errors.count #=> 2 + # p.errors.count # => 2 def count to_a.size end @@ -158,8 +154,8 @@ module ActiveModel # # p.errors.add(:name, "can't be blank") # p.errors.add(:name, "must be specified") - # p.errors.to_xml #=> Produces: - # + # p.errors.to_xml + # # => # # <?xml version=\"1.0\" encoding=\"UTF-8\"?> # # <errors> # # <error>name can't be blank</error> @@ -169,9 +165,9 @@ module ActiveModel to_a.to_xml options.reverse_merge(:root => "errors", :skip_types => true) end - # Returns an array as JSON representation for this object. + # Returns an ActiveSupport::OrderedHash that can be used as the JSON representation for this object. def as_json(options=nil) - to_a + self end # Adds +message+ to the error messages on +attribute+, which will be returned on a call to diff --git a/activemodel/lib/active_model/mass_assignment_security/permission_set.rb b/activemodel/lib/active_model/mass_assignment_security/permission_set.rb index 7c48472799..9fcb94d48a 100644 --- a/activemodel/lib/active_model/mass_assignment_security/permission_set.rb +++ b/activemodel/lib/active_model/mass_assignment_security/permission_set.rb @@ -1,3 +1,4 @@ +require 'set' require 'active_model/mass_assignment_security/sanitizer' module ActiveModel @@ -36,4 +37,4 @@ module ActiveModel end end end -end
\ No newline at end of file +end diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index ca1e9f0ee8..b74d669f0a 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -17,7 +17,10 @@ module ActiveModel end # Transform the model name into a more humane format, using I18n. By default, - # it will underscore then humanize the class name (BlogPost.model_name.human #=> "Blog post"). + # it will underscore then humanize the class name + # + # BlogPost.model_name.human # => "Blog post" + # # Specify +options+ with additional translating options. def human(options={}) return @human unless @klass.respond_to?(:lookup_ancestors) && @@ -45,8 +48,8 @@ module ActiveModel # extend ActiveModel::Naming # end # - # BookCover.model_name #=> "BookCover" - # BookCover.model_name.human #=> "Book cover" + # BookCover.model_name # => "BookCover" + # BookCover.model_name.human # => "Book cover" # # Providing the functionality that ActiveModel::Naming provides in your object # is required to pass the Active Model Lint test. So either extending the provided @@ -57,6 +60,35 @@ module ActiveModel def model_name @_model_name ||= ActiveModel::Name.new(self) end + + # Returns the plural class name of a record or class. Examples: + # + # ActiveModel::Naming.plural(post) # => "posts" + # ActiveModel::Naming.plural(Highrise::Person) # => "highrise_people" + def self.plural(record_or_class) + model_name_from_record_or_class(record_or_class).plural + end + + # Returns the singular class name of a record or class. Examples: + # + # ActiveModel::Naming.singular(post) # => "post" + # ActiveModel::Naming.singular(Highrise::Person) # => "highrise_person" + def self.singular(record_or_class) + model_name_from_record_or_class(record_or_class).singular + end + + # Identifies whether the class name of a record or class is uncountable. Examples: + # + # ActiveModel::Naming.uncountable?(Sheep) # => true + # ActiveModel::Naming.uncountable?(Post) => false + def self.uncountable?(record_or_class) + plural(record_or_class) == singular(record_or_class) + end + + private + def self.model_name_from_record_or_class(record_or_class) + (record_or_class.is_a?(Class) ? record_or_class : record_or_class.class).model_name + end end end diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb index 5670ec74cb..e675937f4d 100644 --- a/activemodel/lib/active_model/serialization.rb +++ b/activemodel/lib/active_model/serialization.rb @@ -61,6 +61,8 @@ module ActiveModel # person.serializable_hash # => {"name"=>"Bob"} # person.to_json # => "{\"name\":\"Bob\"}" # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person... + # + # Valid options are <tt>:only</tt>, <tt>:except</tt> and <tt>:methods</tt> . module Serialization def serializable_hash(options = nil) options ||= {} diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb index 918cd0ab76..e1dbc522de 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -19,8 +19,8 @@ module ActiveModel # passed through +options+. # # The option <tt>ActiveModel::Base.include_root_in_json</tt> controls the - # top-level behavior of to_json. It is true by default. When it is <tt>true</tt>, - # to_json will emit a single root node named after the object's type. For example: + # top-level behavior of +to_json+. If true (the default) +to_json+ will + # emit a single root node named after the object's type. For example: # # konata = User.find(1) # konata.to_json @@ -32,11 +32,11 @@ module ActiveModel # # => {"id": 1, "name": "Konata Izumi", "age": 16, # "created_at": "2006/08/01", "awesome": true} # - # The remainder of the examples in this section assume include_root_in_json is set to - # <tt>false</tt>. + # The remainder of the examples in this section assume +include_root_in_json+ + # is false. # - # Without any +options+, the returned JSON string will include all - # the model's attributes. For example: + # Without any +options+, the returned JSON string will include all the model's + # attributes. For example: # # konata = User.find(1) # konata.to_json @@ -52,14 +52,14 @@ module ActiveModel # konata.to_json(:except => [ :id, :created_at, :age ]) # # => {"name": "Konata Izumi", "awesome": true} # - # To include any methods on the model, use <tt>:methods</tt>. + # To include the result of some method calls on the model use <tt>:methods</tt>: # # konata.to_json(:methods => :permalink) # # => {"id": 1, "name": "Konata Izumi", "age": 16, # "created_at": "2006/08/01", "awesome": true, # "permalink": "1-konata-izumi"} # - # To include associations, use <tt>:include</tt>. + # To include associations use <tt>:include</tt>: # # konata.to_json(:include => :posts) # # => {"id": 1, "name": "Konata Izumi", "age": 16, @@ -67,7 +67,7 @@ module ActiveModel # "posts": [{"id": 1, "author_id": 1, "title": "Welcome to the weblog"}, # {"id": 2, author_id: 1, "title": "So I was thinking"}]} # - # 2nd level and higher order associations work as well: + # Second level and higher order associations work as well: # # konata.to_json(:include => { :posts => { # :include => { :comments => { diff --git a/activemodel/lib/active_model/translation.rb b/activemodel/lib/active_model/translation.rb index 0554677296..0facbd6ce1 100644 --- a/activemodel/lib/active_model/translation.rb +++ b/activemodel/lib/active_model/translation.rb @@ -14,7 +14,7 @@ module ActiveModel # end # # TranslatedPerson.human_attribute_name('my_attribute') - # #=> "My attribute" + # # => "My attribute" # # This also provides the required class methods for hooking into the # Rails internationalization API, including being able to define a diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 5779ba3b29..3407c59e7a 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -24,20 +24,16 @@ module ActiveModel # end # # Which provides you with the full standard validation stack that you - # know from ActiveRecord. + # know from Active Record: # # person = Person.new - # person.valid? - # #=> true - # person.invalid? - # #=> false + # person.valid? # => true + # person.invalid? # => false + # # person.first_name = 'zoolander' - # person.valid? - # #=> false - # person.invalid? - # #=> true - # person.errors - # #=> #<OrderedHash {:first_name=>["starts with z."]}> + # person.valid? # => false + # person.invalid? # => true + # person.errors # => #<OrderedHash {:first_name=>["starts with z."]}> # # Note that ActiveModel::Validations automatically adds an +errors+ method # to your instances initialized with a new ActiveModel::Errors object, so @@ -122,11 +118,13 @@ module ActiveModel # end # def validate(*args, &block) - options = args.last - if options.is_a?(Hash) && options.key?(:on) + options = args.extract_options! + if options.key?(:on) + options = options.dup options[:if] = Array.wrap(options[:if]) options[:if] << "validation_context == :#{options[:on]}" end + args << options set_callback(:validate, *args, &block) end diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index c8a77ad666..a7af4f2b4d 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -40,8 +40,6 @@ module ActiveModel CHECKS.each do |key, validity_check| next unless check_value = options[key] - default_message = options[MESSAGES[key]] - options[:message] ||= default_message if default_message valid_value = if key == :maximum value.nil? || value.size.send(validity_check, check_value) @@ -51,8 +49,13 @@ module ActiveModel next if valid_value - record.errors.add(attribute, MESSAGES[key], - options.except(*RESERVED_OPTIONS).merge!(:count => check_value)) + errors_options = options.except(*RESERVED_OPTIONS) + errors_options[:count] = check_value + + default_message = options[MESSAGES[key]] + errors_options[:message] ||= default_message if default_message + + record.errors.add(attribute, MESSAGES[key], errors_options) end end end diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index 0674640925..3260e6bc5a 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -40,7 +40,7 @@ module ActiveModel # validates :email, :presence => true, :email => true # end # - # Validator classes my also exist within the class being validated + # Validator classes may also exist within the class being validated # allowing custom modules of validators to be included as needed e.g. # # class Film diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index 689c617177..163124d531 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -102,8 +102,8 @@ module ActiveModel #:nodoc: # # == Examples # - # PresenceValidator.kind #=> :presence - # UniquenessValidator.kind #=> :uniqueness + # PresenceValidator.kind # => :presence + # UniquenessValidator.kind # => :uniqueness # def self.kind @kind ||= name.split('::').last.underscore.sub(/_validator$/, '').to_sym unless anonymous? @@ -111,7 +111,7 @@ module ActiveModel #:nodoc: # Accepts options that will be made available through the +options+ reader. def initialize(options) - @options = options + @options = options.freeze end # Return the kind for this validator. diff --git a/activemodel/lib/active_model/version.rb b/activemodel/lib/active_model/version.rb index c36fc8b1f7..f2f4b15520 100644 --- a/activemodel/lib/active_model/version.rb +++ b/activemodel/lib/active_model/version.rb @@ -3,7 +3,7 @@ module ActiveModel MAJOR = 3 MINOR = 0 TINY = 0 - BUILD = "beta4" + BUILD = "rc" STRING = [MAJOR, MINOR, TINY, BUILD].join('.') end diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb index e1a35be384..858ae9cb69 100644 --- a/activemodel/test/cases/dirty_test.rb +++ b/activemodel/test/cases/dirty_test.rb @@ -3,10 +3,11 @@ require "cases/helper" class DirtyTest < ActiveModel::TestCase class DirtyModel include ActiveModel::Dirty - define_attribute_methods [:name] + define_attribute_methods [:name, :color] def initialize @name = nil + @color = nil end def name @@ -17,13 +18,92 @@ class DirtyTest < ActiveModel::TestCase name_will_change! @name = val end + + def color + @color + end + + def color=(val) + color_will_change! unless val == @color + @color = val + end + + def save + @previously_changed = changes + @changed_attributes.clear + end + end + + setup do + @model = DirtyModel.new + end + + test "setting attribute will result in change" do + assert !@model.changed? + assert !@model.name_changed? + @model.name = "Ringo" + assert @model.changed? + assert @model.name_changed? + end + + test "list of changed attributes" do + assert_equal [], @model.changed + @model.name = "Paul" + assert_equal ['name'], @model.changed + end + + test "changes to attribute values" do + assert !@model.changes['name'] + @model.name = "John" + assert_equal [nil, "John"], @model.changes['name'] end test "changes accessible through both strings and symbols" do - model = DirtyModel.new - model.name = "David" - assert_not_nil model.changes[:name] - assert_not_nil model.changes['name'] + @model.name = "David" + assert_not_nil @model.changes[:name] + assert_not_nil @model.changes['name'] + end + + test "attribute mutation" do + @model.instance_variable_set("@name", "Yam") + assert !@model.name_changed? + @model.name.replace("Hadad") + assert !@model.name_changed? + @model.name_will_change! + @model.name.replace("Baal") + assert @model.name_changed? + end + + test "resetting attribute" do + @model.name = "Bob" + @model.reset_name! + assert_nil @model.name + #assert !@model.name_changed #Doesn't work yet + end + + test "setting color to same value should not result in change being recorded" do + @model.color = "red" + assert @model.color_changed? + @model.save + assert !@model.color_changed? + assert !@model.changed? + @model.color = "red" + assert !@model.color_changed? + assert !@model.changed? + end + + test "saving should reset model's changed status" do + @model.name = "Alf" + assert @model.changed? + @model.save + assert !@model.changed? + assert !@model.name_changed? + end + + test "saving should preserve previous changes" do + @model.name = "Jericho Cane" + @model.save + assert_equal [nil, "Jericho Cane"], @model.previous_changes['name'] end end diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb new file mode 100644 index 0000000000..79b45bb298 --- /dev/null +++ b/activemodel/test/cases/errors_test.rb @@ -0,0 +1,65 @@ +require "cases/helper" + +class ErrorsTest < ActiveModel::TestCase + class Person + extend ActiveModel::Naming + def initialize + @errors = ActiveModel::Errors.new(self) + end + + attr_accessor :name + attr_reader :errors + + def validate! + errors.add(:name, "can not be nil") if name == nil + end + + def read_attribute_for_validation(attr) + send(attr) + end + + def self.human_attribute_name(attr, options = {}) + attr + end + + def self.lookup_ancestors + [self] + end + + end + + test "method validate! should work" do + person = Person.new + person.validate! + assert_equal ["name can not be nil"], person.errors.full_messages + assert_equal ["can not be nil"], person.errors[:name] + + end + + test 'should be able to assign error' do + person = Person.new + person.errors[:name] = 'should not be nil' + assert_equal ["should not be nil"], person.errors[:name] + end + + test 'should be able to add an error on an attribute' do + person = Person.new + person.errors.add(:name, "can not be blank") + assert_equal ["can not be blank"], person.errors[:name] + end + + test 'should respond to size' do + person = Person.new + person.errors.add(:name, "can not be blank") + assert_equal 1, person.errors.size + end + + test 'to_a should return an array' do + person = Person.new + person.errors.add(:name, "can not be blank") + person.errors.add(:name, "can not be nil") + assert_equal ["name can not be blank", "name can not be nil"], person.errors.to_a + + end + +end diff --git a/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb b/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb index 367207aab3..015153ec7c 100644 --- a/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb +++ b/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb @@ -31,7 +31,7 @@ class SanitizerTest < ActiveModel::TestCase log = StringIO.new @sanitizer.logger = Logger.new(log) @sanitizer.sanitize(original_attributes) - assert (log.string =~ /admin/), "Should log removed attributes: #{log.string}" + assert_match(/admin/, log.string, "Should log removed attributes: #{log.string}") end end diff --git a/activemodel/test/cases/mass_assignment_security_test.rb b/activemodel/test/cases/mass_assignment_security_test.rb index 0f7a38b0bc..c25b0fdf00 100644 --- a/activemodel/test/cases/mass_assignment_security_test.rb +++ b/activemodel/test/cases/mass_assignment_security_test.rb @@ -35,10 +35,10 @@ class MassAssignmentSecurityTest < ActiveModel::TestCase assert_equal Set.new([ 'credit_rating', 'administrator', 'phone_number', 'name']), LooseDescendantSecond.protected_attributes, 'Running attr_protected twice in one class should merge the protections' - assert (TightPerson.protected_attributes - TightPerson.attributes_protected_by_default).blank? + assert_blank TightPerson.protected_attributes - TightPerson.attributes_protected_by_default assert_equal Set.new([ 'name', 'address' ]), TightPerson.accessible_attributes - assert (TightDescendant.protected_attributes - TightDescendant.attributes_protected_by_default).blank? + assert_blank TightDescendant.protected_attributes - TightDescendant.attributes_protected_by_default assert_equal Set.new([ 'name', 'address', 'phone_number' ]), TightDescendant.accessible_attributes end @@ -49,4 +49,4 @@ class MassAssignmentSecurityTest < ActiveModel::TestCase assert_equal sanitized, { } end -end
\ No newline at end of file +end diff --git a/activemodel/test/cases/naming_test.rb b/activemodel/test/cases/naming_test.rb index dc39b84ed8..5a8bff378a 100644 --- a/activemodel/test/cases/naming_test.rb +++ b/activemodel/test/cases/naming_test.rb @@ -1,4 +1,6 @@ require 'cases/helper' +require 'models/contact' +require 'models/sheep' require 'models/track_back' class NamingTest < ActiveModel::TestCase @@ -26,3 +28,40 @@ class NamingTest < ActiveModel::TestCase assert_equal 'post/track_backs/track_back', @model_name.partial_path end end + +class NamingHelpersTest < Test::Unit::TestCase + def setup + @klass = Contact + @record = @klass.new + @singular = 'contact' + @plural = 'contacts' + @uncountable = Sheep + end + + def test_singular + assert_equal @singular, singular(@record) + end + + def test_singular_for_class + assert_equal @singular, singular(@klass) + end + + def test_plural + assert_equal @plural, plural(@record) + end + + def test_plural_for_class + assert_equal @plural, plural(@klass) + end + + def test_uncountable + assert uncountable?(@uncountable), "Expected 'sheep' to be uncoutable" + assert !uncountable?(@klass), "Expected 'contact' to be countable" + end + + private + def method_missing(method, *args) + ActiveModel::Naming.send(method, *args) + end +end + diff --git a/activemodel/test/cases/serializeration/json_serialization_test.rb b/activemodel/test/cases/serializeration/json_serialization_test.rb index 04b50e5bb8..1ac991a8f1 100644 --- a/activemodel/test/cases/serializeration/json_serialization_test.rb +++ b/activemodel/test/cases/serializeration/json_serialization_test.rb @@ -89,7 +89,7 @@ class JsonSerializationTest < ActiveModel::TestCase assert_match %r{"preferences":\{"shows":"anime"\}}, json end - test "methds are called on object" do + test "methods are called on object" do # Define methods on fixture. def @contact.label; "Has cheezburger"; end def @contact.favorite_quote; "Constraints are liberating"; end @@ -102,4 +102,18 @@ class JsonSerializationTest < ActiveModel::TestCase assert_match %r{"label":"Has cheezburger"}, methods_json assert_match %r{"favorite_quote":"Constraints are liberating"}, methods_json end + + test "should return OrderedHash for errors" do + car = Automobile.new + + # run the validation + car.valid? + + 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 + end + + end diff --git a/activemodel/test/cases/validations/length_validation_test.rb b/activemodel/test/cases/validations/length_validation_test.rb index 012c5a2f37..1e6180a938 100644 --- a/activemodel/test/cases/validations/length_validation_test.rb +++ b/activemodel/test/cases/validations/length_validation_test.rb @@ -229,6 +229,20 @@ class LengthValidationTest < ActiveModel::TestCase assert_equal ["hoo 5"], t.errors["title"] end + def test_validates_length_of_custom_errors_for_both_too_short_and_too_long + Topic.validates_length_of :title, :minimum => 3, :maximum => 5, :too_short => 'too short', :too_long => 'too long' + + t = Topic.new(:title => 'a') + assert t.invalid? + assert t.errors[:title].any? + assert_equal ['too short'], t.errors['title'] + + t = Topic.new(:title => 'aaaaaa') + assert t.invalid? + assert t.errors[:title].any? + assert_equal ['too long'], t.errors['title'] + end + def test_validates_length_of_custom_errors_for_is_with_message Topic.validates_length_of( :title, :is=>5, :message=>"boo %{count}" ) t = Topic.new("title" => "uhohuhoh", "content" => "whatever") diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb index e94d8ce88c..8d6bdeb6a5 100644 --- a/activemodel/test/cases/validations_test.rb +++ b/activemodel/test/cases/validations_test.rb @@ -170,9 +170,11 @@ class ValidationsTest < ActiveModel::TestCase assert_match %r{<errors>}, xml assert_match %r{<error>Title can't be blank</error>}, xml assert_match %r{<error>Content can't be blank</error>}, xml - - json = t.errors.to_json - assert_equal t.errors.to_a.to_json, json + + hash = ActiveSupport::OrderedHash.new + hash[:title] = "can't be blank" + hash[:content] = "can't be blank" + assert_equal t.errors.to_json, hash.to_json end def test_validation_order diff --git a/activemodel/test/models/contact.rb b/activemodel/test/models/contact.rb index 605e435f39..f4f3078473 100644 --- a/activemodel/test/models/contact.rb +++ b/activemodel/test/models/contact.rb @@ -1,4 +1,5 @@ class Contact + extend ActiveModel::Naming include ActiveModel::Conversion attr_accessor :id, :name, :age, :created_at, :awesome, :preferences diff --git a/activemodel/test/models/sheep.rb b/activemodel/test/models/sheep.rb new file mode 100644 index 0000000000..175dbe6477 --- /dev/null +++ b/activemodel/test/models/sheep.rb @@ -0,0 +1,4 @@ +class Sheep + extend ActiveModel::Naming +end +
\ No newline at end of file |