mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Add a required
option to singular associations
In addition to defining the association, a `required` association will also have its presence validated. Before: ```ruby belongs_to :account validates_presence_of :account ``` After: ```ruby belongs_to :account, required: true ``` This helps to draw a distinction between types of validations, since validations on associations are generally for data integrity purposes, and aren't usually set through form inputs.
This commit is contained in:
parent
a6cc7b0ebd
commit
00f5551650
4 changed files with 105 additions and 1 deletions
|
@ -1309,6 +1309,10 @@ module ActiveRecord
|
|||
# that is the inverse of this <tt>has_one</tt> association. Does not work in combination
|
||||
# with <tt>:through</tt> or <tt>:as</tt> options.
|
||||
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
|
||||
# [:required]
|
||||
# When set to +true+, the association will also have its presence validated.
|
||||
# This will validate the association itself, not the id. You can use
|
||||
# +:inverse_of+ to avoid an extra query during validation.
|
||||
#
|
||||
# Option examples:
|
||||
# has_one :credit_card, dependent: :destroy # destroys the associated credit card
|
||||
|
@ -1320,6 +1324,7 @@ module ActiveRecord
|
|||
# has_one :boss, readonly: :true
|
||||
# has_one :club, through: :membership
|
||||
# has_one :primary_address, -> { where primary: true }, through: :addressables, source: :addressable
|
||||
# has_one :credit_card, required: true
|
||||
def has_one(name, scope = nil, options = {})
|
||||
reflection = Builder::HasOne.build(self, name, scope, options)
|
||||
Reflection.add_reflection self, name, reflection
|
||||
|
@ -1421,6 +1426,10 @@ module ActiveRecord
|
|||
# object that is the inverse of this <tt>belongs_to</tt> association. Does not work in
|
||||
# combination with the <tt>:polymorphic</tt> options.
|
||||
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
|
||||
# [:required]
|
||||
# When set to +true+, the association will also have its presence validated.
|
||||
# This will validate the association itself, not the id. You can use
|
||||
# +:inverse_of+ to avoid an extra query during validation.
|
||||
#
|
||||
# Option examples:
|
||||
# belongs_to :firm, foreign_key: "client_of"
|
||||
|
@ -1433,6 +1442,7 @@ module ActiveRecord
|
|||
# belongs_to :post, counter_cache: true
|
||||
# belongs_to :company, touch: true
|
||||
# belongs_to :company, touch: :employees_last_updated_at
|
||||
# belongs_to :company, required: true
|
||||
def belongs_to(name, scope = nil, options = {})
|
||||
reflection = Builder::BelongsTo.build(self, name, scope, options)
|
||||
Reflection.add_reflection self, name, reflection
|
||||
|
|
|
@ -36,6 +36,7 @@ module ActiveRecord::Associations::Builder
|
|||
reflection = builder.build(model)
|
||||
define_accessors model, reflection
|
||||
define_callbacks model, reflection
|
||||
define_validations model, reflection
|
||||
builder.define_extensions model
|
||||
reflection
|
||||
end
|
||||
|
@ -124,6 +125,10 @@ module ActiveRecord::Associations::Builder
|
|||
CODE
|
||||
end
|
||||
|
||||
def self.define_validations(model, reflection)
|
||||
# noop
|
||||
end
|
||||
|
||||
def self.valid_dependent_options
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
module ActiveRecord::Associations::Builder
|
||||
class SingularAssociation < Association #:nodoc:
|
||||
def valid_options
|
||||
super + [:remote, :dependent, :primary_key, :inverse_of]
|
||||
super + [:remote, :dependent, :primary_key, :inverse_of, :required]
|
||||
end
|
||||
|
||||
def self.define_accessors(model, reflection)
|
||||
|
@ -27,5 +27,12 @@ module ActiveRecord::Associations::Builder
|
|||
end
|
||||
CODE
|
||||
end
|
||||
|
||||
def self.define_validations(model, reflection)
|
||||
super
|
||||
if reflection.options[:required]
|
||||
model.validates_presence_of reflection.name
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
82
activerecord/test/cases/associations/required_test.rb
Normal file
82
activerecord/test/cases/associations/required_test.rb
Normal file
|
@ -0,0 +1,82 @@
|
|||
require "cases/helper"
|
||||
|
||||
class RequiredAssociationsTest < ActiveRecord::TestCase
|
||||
self.use_transactional_fixtures = false
|
||||
|
||||
class Parent < ActiveRecord::Base
|
||||
end
|
||||
|
||||
class Child < ActiveRecord::Base
|
||||
end
|
||||
|
||||
setup do
|
||||
@connection = ActiveRecord::Base.connection
|
||||
@connection.create_table :parents, force: true
|
||||
@connection.create_table :children, force: true do |t|
|
||||
t.belongs_to :parent
|
||||
end
|
||||
end
|
||||
|
||||
teardown do
|
||||
@connection.execute("DROP TABLE IF EXISTS parents")
|
||||
@connection.execute("DROP TABLE IF EXISTS children")
|
||||
end
|
||||
|
||||
test "belongs_to associations are not required by default" do
|
||||
model = subclass_of(Child) do
|
||||
belongs_to :parent, inverse_of: false,
|
||||
class_name: "RequiredAssociationsTest::Parent"
|
||||
end
|
||||
|
||||
assert model.new.save
|
||||
assert model.new(parent: Parent.new).save
|
||||
end
|
||||
|
||||
test "required belongs_to associations have presence validated" do
|
||||
model = subclass_of(Child) do
|
||||
belongs_to :parent, required: true, inverse_of: false,
|
||||
class_name: "RequiredAssociationsTest::Parent"
|
||||
end
|
||||
|
||||
record = model.new
|
||||
assert_not record.save
|
||||
assert_equal ["Parent can't be blank"], record.errors.full_messages
|
||||
|
||||
record.parent = Parent.new
|
||||
assert record.save
|
||||
end
|
||||
|
||||
test "has_one associations are not required by default" do
|
||||
model = subclass_of(Parent) do
|
||||
has_one :child, inverse_of: false,
|
||||
class_name: "RequiredAssociationsTest::Child"
|
||||
end
|
||||
|
||||
assert model.new.save
|
||||
assert model.new(child: Child.new).save
|
||||
end
|
||||
|
||||
test "required has_one associations have presence validated" do
|
||||
model = subclass_of(Parent) do
|
||||
has_one :child, required: true, inverse_of: false,
|
||||
class_name: "RequiredAssociationsTest::Child"
|
||||
end
|
||||
|
||||
record = model.new
|
||||
assert_not record.save
|
||||
assert_equal ["Child can't be blank"], record.errors.full_messages
|
||||
|
||||
record.child = Child.new
|
||||
assert record.save
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def subclass_of(klass, &block)
|
||||
subclass = Class.new(klass, &block)
|
||||
def subclass.name
|
||||
superclass.name
|
||||
end
|
||||
subclass
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue