has_many with at least two entries - ruby-on-rails

I need a has_many association that has at least two entries, how do I write the validation and how could this be tested using RSpec + factory-girl? This is what I got till now, but it fails with ActiveRecord::RecordInvalid: Validation failed: Bars can't be blank and I'm completely stuck on the RSpec test.
/example_app/app/models/foo.rb
class Foo < ActiveRecord::Base
has_many :bars
validates :bars, :presence => true, :length => { :minimum => 2}
end
/example_app/app/models/bar.rb
class Bar < ActiveRecord::Base
belongs_to :foo
validates :bar, :presence => true
end
/example-app/spec/factories/foo.rb
FactoryGirl.define do
factory :foo do
after(:create) do |foo|
FactoryGirl.create_list(:bar, 2, foo: foo)
end
end
end
/example-app/spec/factories/bar.rb
FactoryGirl.define do
factory :bar do
foo
end
end

class Foo < ActiveRecord::Base
validate :must_have_two_bars
private
def must_have_two_bars
# if you allow bars to be destroyed through the association you may need to do extra validation here of the count
errors.add(:bars, :too_short, :count => 2) if bars.size < 2
end
end
it "should validate the presence of bars" do
FactoryGirl.build(:foo, :bars => []).should have_at_least(1).error_on(:bars)
end
it "should validate that there are at least two bars" do
foo = FactoryGirl.build(:foo)
foo.bars.push FactoryGirl.build(:bar, :foo => nil)
foo.should have_at_least(1).error_on(:bar)
end

You want to use a custom validator
class Foo < ActiveRecord::Base
has_many :bars
validate :validates_number_of_bars
private
def validates_number_of_bars
if bars.size < 2
errors[:base] << "Need at least 2 bars"
end
end
end

Related

Joint query across 2 models (has_many)

Hi I need help and all insight appreciated. I have two models Auctions and Bids and I want to retrieve the All auctions current_user won, the ones s/he has been outbid on and the ones s/he's winning
Here are the two models:
class Auction < ActiveRecord::Base
extend FriendlyId
friendly_id :guid, use: :slugged
before_save :populate_guid
mount_uploaders :images, ImageUploader
belongs_to :client
has_many :bids, dependent: :destroy
has_one :order, dependent: :destroy
validates_presence_of :title, :lien_price,
:end_time, :collateral_value,
:redemption_date, :current_interest_rate,
:additional_tax, :collateral_details,
:location, :client_id, :starting_bid
validate :end_time_in_the_future, :on => :update
validates_uniqueness_of :guid, case_sensitive: false
def end_time_in_the_future
errors.add(:end_time, "can't be in the past") if self.end_time && self.end_time < Time.now
end
def self.get_active_auctions
where("end_time > ?", Time.now)
end
def self.closed_auctions
where("end_time < ?", Time.now)
end
def highest_bid
self.bids.maximum("amount")
end
def highest_bid_object
self.bids.order(:amount => :desc).limit(1).first
end
def highest_bidder
self.highest_bid_object.user if highest_bid_object
end
def closed?
self.end_time < Time.now
end
private
def populate_guid
if new_record?
while !valid? || self.guid.nil?
self.guid = SecureRandom.random_number(1_000_000_000).to_s(36)
end
end
end
end
and
class Bid < ActiveRecord::Base
extend FriendlyId
friendly_id :guid, use: :slugged
belongs_to :auction
belongs_to :user
before_save :populate_guid
validates_presence_of :amount, :user_id,
:auction_id
#validate :higher_than_current?
validates :amount, :numericality => true
validates_uniqueness_of :guid, case_sensitive: false
def higher_than_current?
if !Bid.where("amount > ? AND auction_id = ?", amount, self.auction.id).empty?
errors.add(:amount, "is too low! It can't be lower than the current bid, sorry.")
end
end
private
def populate_guid
if new_record?
while !valid? || self.guid.nil?
self.guid = SecureRandom.random_number(1_000_000_000).to_s(36)
end
end
end
end
I thought
#auctions = Auction.closed_auctions.where(highest_bidder: current_user)
or
#auctions = Auction.closed_auctions.joins(:bids).where(highest_bidder: current_user)
would work but they both raise an error.
Edit this works
#auctions = Auction.closed_auctions.references(highest_bidder: current_user)
But there's probably a better way.
You probably can't access current_user from controller (devise?). So you need to pass the user as a parameter to the class or instance method. What you should look into are scopes and especially scopes that accept parameters. Scopes could really help you refactor your Auction model (you really don't need any methods that only return a where()), but also solve the inaccessible current_user.
Use it like this in your Auction model:
scope: :highest_bidder -> (current_user) { where(highest_bidder: current_user) }
And call it like this from your controller:
#auctions = Auction.closed_auctions.highest_bidder(current_user)

How to show errors for relations on the main Model

I want to include errors from a rather deep assocation in a parent:
class Order < ActiveRecord::Base
has_many :line_items
end
class LineItem < ActiveRecord::Base
belongs_to :product, polymorphic: true
end
class Site < ActiveRecord::Base
has_one :line_item, as: :product, autosave: true
validates :domain, presence: true
end
Used as:
product = Site.new(domain: nil)
order = Order.new
order.line_items << LineItem.new(product: product)
order.valid? #=> false
product.valid? #=> false
product.errors? #=> { 'domain' => 'cannot be blank' }
Is there some rails way, or association-parameter to make the errors
bubble up so that I get:
order.errors #=> { 'domain' => 'cannot be blank' }
In other words, that the Order, the top of the association,
transparently proxies the validation errors from its children?
I am aware of using simple before_validation hooks, like so:
class Order < ActiveRecord::Base
before_validation :add_errors_from_line_items
private
def add_errors_from_line_items
self.line_items.each do |line_item|
line_item.product.errors.each do |field, message|
errors.add(field, message)
end unless line_item.product.valid?
end
end
end
end
But I am wondering if there is not some ActiveRecord feature that I am overlooking.

Using Roles for Validations in Rails

Is it possible to use the roles used for attr_accessible and attr_protected? I'm trying to setup a validation that only executes when not an admin (like this sort of http://launchware.com/articles/whats-new-in-edge-scoped-mass-assignment-in-rails-3-1). For example:
class User < ActiveRecord::Base
def validate(record)
unless # role.admin?
record.errors[:name] << 'Wrong length' if ...
end
end
end
user = User.create({ ... }, role: "admin")
After looking into this and digging through the source code, it appears that the role passed in when creating an Active Record object is exposed through a protected method mass_assignment_role. Thus, the code in question can be re-written as:
class User < ActiveRecord::Base
def validate(record)
unless mass_assignment_role.eql? :admin
record.errors[:name] << 'Wrong length' if ...
end
end
end
user = User.create({ ... }, role: "admin")
Sure can would be something like this:
class User < ActiveRecord::Base
attr_accessible :role
validates :record_validation
def record_validation
unless self.role == "admin"
errors.add(:name, "error message") if ..
end
end
You could do this
class User < ActiveRecord::Base
with_options :if => :is_admin? do |admin|
admin.validates :password, :length => { :minimum => 10 } #sample validations
admin.validates :email, :presence => true #sample validations
end
end
5.4 Grouping conditional validations

Has "def validate" been taken out in Rails 3.1?

Has "def validate" been taken out in Rails 3.1? I'm on Rails 3.1 pre and it doesn't seem to be working
class Category < ActiveRecord::Base
validates_presence_of :title
private
def validate
errors.add(:description, "is too short") if (description.size < 200)
end
end
The "title" validation works but the "description" validation doesn't.
Does something like this work for you?
class Category < ActiveRecord::Base
validates_presence_of :title
validate :description_length
def description_length
errors.add(:description, "is too short") if (description.size < 200)
end
end
class Category < ActiveRecord::Base
validates_presence_of :title
private
validate do
errors.add(:description, "is too short") if (description.size < 200)
end
end
For other types of validations, you can also add 'Validators' like the one listed here:
http://edgeguides.rubyonrails.org/3_0_release_notes.html#validations
class TitleValidator < ActiveModel::EachValidator
Titles = ['Mr.', 'Mrs.', 'Dr.']
def validate_each(record, attribute, value)
unless Titles.include?(value)
record.errors[attribute] << 'must be a valid title'
end
end
end
class Person
include ActiveModel::Validations
attr_accessor :title
validates :title, :presence => true, :title => true
end
# Or for Active Record
class Person < ActiveRecord::Base
validates :title, :presence => true, :title => true
end

validates_associated not honoring :if

I'm totally blocked on this.
See this code:
# user.rb
class User < ActiveRecord::Base
has_one :address
accepts_nested_attributes_for :address
validates_associated :address, :if => Proc.new {|u| u.addressable? }
end
# address.rb
class Address < ActiveRecord::Base
belongs_to :user
validates_presence_of :address_text
end
# user_test.rb
require File.dirname(__FILE__) + '/../test_helper'
class UserTest < ActiveSupport::TestCase
setup { }
test "address validation is not ran w update attributes and anaf" do
#user = User.create!
#user.build_address
assert_nothing_raised do
#user.update_attributes!(:addressable => false, :address_attributes => {:address => "test"})
end
end
test "address validation w update_attributes and anaf" do
#user = User.create!
#user.build_address
#user.save
assert_raise ActiveRecord::RecordInvalid do
#user.update_attributes!(:addressable => true, :address_attributes => {:address => "test"})
end
end
end
The first test will fail.
The user model validates an associated address model, but is only supposed to do it if a flag is true. In practice it does it all the time.
What is going on?
Actually I ran into further problems with my (more complicated) real world scenario that were only solved by doing the equivalent of:
def validate_associated_records_for_address
self.addressable? ? validate_single_association(User.reflect_on_association(:address)) : nil
end
This adapts anaf's compulsory validations to only run under the condition we want (addressable? is true).
The validates_associated...:if is not necessary now.

Resources