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:
parent
1881a7715d
commit
21e448b5a5
8 changed files with 95 additions and 4 deletions
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
4
activerecord/test/models/guitar.rb
Normal file
4
activerecord/test/models/guitar.rb
Normal file
|
@ -0,0 +1,4 @@
|
|||
class Guitar < ActiveRecord::Base
|
||||
has_many :tuning_pegs, index_errors: true
|
||||
accepts_nested_attributes_for :tuning_pegs
|
||||
end
|
4
activerecord/test/models/tuning_peg.rb
Normal file
4
activerecord/test/models/tuning_peg.rb
Normal file
|
@ -0,0 +1,4 @@
|
|||
class TuningPeg < ActiveRecord::Base
|
||||
belongs_to :guitar
|
||||
validates_numericality_of :pitch
|
||||
end
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue