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

Errors can be indexed with nested attributes

`has_many` can now take `index_errors: true` as an
option.  When this is enabled, errors for nested models will be
returned alongside an index, as opposed to just the nested model name.
This option can also be enabled (or disabled) globally through
`ActiveRecord::Base.index_nested_attribute_errors`

E.X.

```ruby
class Guitar < ActiveRecord::Base
  has_many :tuning_pegs
  accepts_nested_attributes_for :tuning_pegs
end

class TuningPeg < ActiveRecord::Base
  belongs_to :guitar
  validates_numericality_of :pitch
end
```

 - Old style
 - `guitar.errors["tuning_pegs.pitch"] = ["is not a number"]`

 - New style (if defined globally, or set in has_many_relationship)
 - `guitar.errors["tuning_pegs[1].pitch"] = ["is not a number"]`

[Michael Probber, Terence Sun]
This commit is contained in:
Michael Probber 2015-04-07 10:42:57 -04:00
parent 1881a7715d
commit 21e448b5a5
8 changed files with 95 additions and 4 deletions

View file

@ -1,3 +1,31 @@
* Add option to index errors in nested attributes
For models which have nested attributes, errors within those models will
now be indexed if :index_errors is specified when defining a
has_many relationship, or if its set in the global config.
E.X.
```ruby
class Guitar < ActiveRecord::Base
has_many :tuning_pegs
accepts_nested_attributes_for :tuning_pegs
end
class TuningPeg < ActiveRecord::Base
belongs_to :guitar
validates_numericality_of :pitch
end
```
- Old style
- `guitar.errors["tuning_pegs.pitch"] = ["is not a number"]`
- New style (if defined globally, or set in has_many_relationship)
- `guitar.errors["tuning_pegs[1].pitch"] = ["is not a number"]`
*Michael Probber and Terence Sun*
* Fixed a bug where uniqueness validations would error on out of range values,
even if an validation should have prevented it from hitting the database.

View file

@ -5,7 +5,7 @@ module ActiveRecord::Associations::Builder
end
def self.valid_options(options)
super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table, :foreign_type]
super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table, :foreign_type, :index_errors]
end
def self.valid_dependent_options

View file

@ -141,6 +141,8 @@ module ActiveRecord
included do
Associations::Builder::Association.extensions << AssociationBuilderExtension
mattr_accessor :index_nested_attribute_errors, instance_writer: false
self.index_nested_attribute_errors = false
end
module ClassMethods
@ -315,7 +317,7 @@ module ActiveRecord
def validate_collection_association(reflection)
if association = association_instance_get(reflection.name)
if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
records.each { |record| association_valid?(reflection, record) }
records.each_with_index { |record, index| association_valid?(reflection, record, index) }
end
end
end
@ -323,14 +325,18 @@ module ActiveRecord
# Returns whether or not the association is valid and applies any errors to
# the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
# enabled records if they're marked_for_destruction? or destroyed.
def association_valid?(reflection, record)
def association_valid?(reflection, record, index=nil)
return true if record.destroyed? || record.marked_for_destruction?
validation_context = self.validation_context unless [:create, :update].include?(self.validation_context)
unless valid = record.valid?(validation_context)
if reflection.options[:autosave]
record.errors.each do |attribute, message|
attribute = "#{reflection.name}.#{attribute}"
if index.nil? || (!reflection.options[:index_errors] && !ActiveRecord::Base.index_nested_attribute_errors)
attribute = "#{reflection.name}.#{attribute}"
else
attribute = "#{reflection.name}[#{index}].#{attribute}"
end
errors[attribute] << message
errors[attribute].uniq!
end

View file

@ -24,6 +24,8 @@ require 'models/molecule'
require 'models/member'
require 'models/member_detail'
require 'models/organization'
require 'models/guitar'
require 'models/tuning_peg'
class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase
def test_autosave_validation
@ -385,6 +387,40 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib
assert_not molecule.persisted?, 'Molecule should not be persisted when its electrons are invalid'
end
def test_errors_should_be_indexed_when_passed_as_array
guitar = Guitar.new
tuning_peg_valid = TuningPeg.new
tuning_peg_valid.pitch = 440.0
tuning_peg_invalid = TuningPeg.new
guitar.tuning_pegs = [tuning_peg_valid, tuning_peg_invalid]
assert_not tuning_peg_invalid.valid?
assert tuning_peg_valid.valid?
assert_not guitar.valid?
assert_equal ["is not a number"], guitar.errors["tuning_pegs[1].pitch"]
assert_not_equal ["is not a number"], guitar.errors["tuning_pegs.pitch"]
end
def test_errors_should_be_indexed_when_global_flag_is_set
old_attribute_config = ActiveRecord::Base.index_nested_attribute_errors
ActiveRecord::Base.index_nested_attribute_errors = true
molecule = Molecule.new
valid_electron = Electron.new(name: 'electron')
invalid_electron = Electron.new
molecule.electrons = [valid_electron, invalid_electron]
assert_not invalid_electron.valid?
assert valid_electron.valid?
assert_not molecule.valid?
assert_equal ["can't be blank"], molecule.errors["electrons[1].name"]
assert_not_equal ["can't be blank"], molecule.errors["electrons.name"]
ensure
ActiveRecord::Base.index_nested_attribute_errors = old_attribute_config
end
def test_valid_adding_with_nested_attributes
molecule = Molecule.new
valid_electron = Electron.new(name: 'electron')

View file

@ -0,0 +1,4 @@
class Guitar < ActiveRecord::Base
has_many :tuning_pegs, index_errors: true
accepts_nested_attributes_for :tuning_pegs
end

View file

@ -0,0 +1,4 @@
class TuningPeg < ActiveRecord::Base
belongs_to :guitar
validates_numericality_of :pitch
end

View file

@ -336,6 +336,10 @@ ActiveRecord::Schema.define do
t.column :key, :string
end
create_table :guitar, force: true do |t|
t.string :color
end
create_table :inept_wizards, force: true do |t|
t.column :name, :string, null: false
t.column :city, :string, null: false
@ -789,6 +793,11 @@ ActiveRecord::Schema.define do
t.belongs_to :ship
end
create_table :tuning_pegs, force: true do |t|
t.integer :guitar_id
t.float :pitch
end
create_table :tyres, force: true do |t|
t.integer :car_id
end

View file

@ -314,6 +314,10 @@ All these configuration options are delegated to the `I18n` library.
by a query exceeds the threshold, a warning is logged. This can be used to
identify queries which might be causing memory bloat.
* `config.active_record.index_nested_attribute_errors` allows errors for nested
has_many relationships to be displayed with an index as well as the error.
Defaults to false.
The MySQL adapter adds one additional configuration option:
* `ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans` controls whether Active Record will consider all `tinyint(1)` columns in a MySQL database to be booleans and is true by default.