I have a model called Option.
class Option < ApplicationRecord
belongs_to :user
belongs_to :company
belongs_to :scheme
validate :check_for_quantity
def check_for_quantity
if self.quantity > self.scheme.remaining_options
errors.add(:quantity, "cannot be more than the remaining options #{ self.scheme.remaining_options.to_i}")
end
end
end
and a model called Scheme.
class Scheme < ApplicationRecord
belongs_to :share_class
belongs_to :equity_pool
belongs_to :company
has_many :options, dependent: :destroy
attr_accessor :percentage
def ownership
self.remaining_options * 100 / self.company.total_fdsc
end
def remaining_options
self.initial_size - self.options.sum(&:quantity)
end
end
My spec for Option Model looks like this
require 'rails_helper'
RSpec.describe Option, type: :model do
describe "Associations" do
subject { create (:option) }
it { is_expected.to belong_to(:scheme) }
it { is_expected.to belong_to(:vesting_schedule).optional }
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:company) }
end
end
When I run this spec the first example gives an error
1) Option Associations is expected to belong to scheme required: true
Failure/Error: if self.quantity > self.scheme.remaining_options
NoMethodError:
undefined method `remaining_options' for nil:NilClass
# ./app/models/option.rb:9:in `check_for_quantity'
What is the problem here?
My options factory bot
FactoryBot.define do
factory :option do
security "MyString"
board_status false
board_approval_date "2018-08-16"
grant_date "2018-08-16"
expiration_date "2018-08-16"
quantity 1
exercise_price 1.5
vesting_start_date "2018-08-16"
vesting_schedule nil
scheme
user
company
end
end
Just add a condition to the validation so that it is not fired if the association is nil.
class Option < ApplicationRecord
belongs_to :user
belongs_to :company
belongs_to :scheme
validate :check_for_quantity, unless: -> { self.scheme.nil? }
def check_for_quantity
if self.quantity > self.scheme.remaining_options
errors.add(:quantity, "cannot be more than the remaining options #{ self.scheme.remaining_options.to_i}")
end
end
end
You may also want to ensure that self.quantity is a number and not nil to avoid NoMethodError: undefined method > for nil:NilClass which you can do with a numericality validation.
class Option < ApplicationRecord
belongs_to :user
belongs_to :company
belongs_to :scheme
validates_numericality_of :quantity
validate :check_for_quantity, if: -> { self.scheme && self.quantity }
def check_for_quantity
if self.quantity > self.scheme.remaining_options
errors.add(:quantity, "cannot be more than the remaining options #{ self.scheme.remaining_options.to_i}")
end
end
end
Related
In my app I created friend list for User. Inside Friend model I have validations for unique connection between users. Everything works good, but I don't know how to write tests for this model. It's looks like that:
Friend.rb
class Friend < ApplicationRecord
belongs_to :user1, :class_name => 'User'
belongs_to :user2, :class_name => 'User'
validate :uniqueness_of_users_associations, :cant_be_friend_with_yourself
def uniqueness_of_users_associations
unless (user1.friends.where(user2: user2) + user1.is_friend.where(user1: user2)).blank?
errors.add(:friend, 'He is already your friend')
end
end
def cant_be_friend_with_yourself
errors.add(:friend, "You can't be friend with yourself") if user1 == user2
end
end
User.rb:
class User < ActiveRecord::Base
extend Devise::Models
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
include DeviseTokenAuth::Concerns::User
has_many :friends, :class_name => 'Friend', :foreign_key => 'user1', dependent: :destroy
has_many :is_friend, :class_name => 'Friend', :foreign_key => 'user2', dependent: :destroy
end
spec/factories/friends.rb :
FactoryBot.define do
factory :friend do
association :user1, factory: :user
association :user2, factory: :user
confirmed { true }
end
end
friend_spec.rb :
RSpec.describe Friend, type: :model do
describe 'relationships' do
it { is_expected.to belong_to(:user1).class_name('User') }
it { is_expected.to belong_to(:user2).class_name('User') }
end
end
When I try to run test I get error:
Failure/Error: unless (user1.friends.where(user2: user2) + user1.is_friend.where(user1: user2)).blank?
NoMethodError:
undefined method `friends' for nil:NilClass
Why I get nil inside model? I did something wrong inside factory?
What you actually want to find if Users 1 & 2 are friends is this query:
EXISTS (
SELECT 1
FROM friends
WHERE
friends.user1_id = 1 OR friends.user2_id = 1
AND
friends.user1_id = 2 OR friends.user2_id = 2
)
class Friend < ApplicationRecord
validate :uniqueness_of_users_associations, :cant_be_friend_with_yourself
def uniqueness_of_users_associations
if Friend.between(user1.id, user2.id).exists?
errors.add(:base, 'Friendship already exists')
end
end
def self.between(a, b)
user1_id, user2_id = arel_table[:user1_id], arel_table[:user2_id]
where(user1_id.eq(a).or(user2_id.eq(a)))
.where(user1_id.eq(b).or(user2_id.eq(b)))
end
# ...
end
However the naming here is super off. The model should be named Friendship as friend actually means the person that you are friends with.
In our Rails app, we have the following models:
class User < ActiveRecord::Base
has_many :administrations, dependent: :destroy
has_many :calendars, through: :administrations
end
class Administration < ActiveRecord::Base
belongs_to :user
belongs_to :calendar
end
class Calendar < ActiveRecord::Base
has_many :administrations, dependent: :destroy
has_many :users, through: :administrations
end
We tried to validate the Administration model with the following administration_test.rb test file:
require 'test_helper'
class AdministrationTest < ActiveSupport::TestCase
def setup
#user = users(:noemie)
#administration = Administration.new(user_id: #user.id, calendar_id: #calendar_id)
end
test "should be valid" do
assert #administration.valid?
end
test "user id should be present" do
#administration.user_id = nil
assert_not #administration.valid?
end
test "calendar id should be present" do
#administration.calendar_id = nil
assert_not #administration.valid?
end
end
When we run the test, we get the following results:
FAIL["test_calendar_id_should_be_present", AdministrationTest, 2015-06-30 07:24:58 -0700]
test_calendar_id_should_be_present#AdministrationTest (1435674298.26s)
Expected true to be nil or false
test/models/administration_test.rb:21:in `block in <class:AdministrationTest>'
FAIL["test_user_id_should_be_present", AdministrationTest, 2015-06-30 07:24:58 -0700]
test_user_id_should_be_present#AdministrationTest (1435674298.27s)
Expected true to be nil or false
test/models/administration_test.rb:16:in `block in <class:AdministrationTest>'
We are kind of lost: is this the right way to right the test?
If no, how should we write it?
If yes, how can we make it pass?
The problem is not your test but rather that you are expecting the wrong outcome.
belongs_toin ActiveRecord does not add a validation, the macro simply creates a relation.
To validate a relation you would use validates_associated which calls #valid? on each of the associated records and validates_presence_of to ensure that the associated record is present.
class Administration < ActiveRecord::Base
belongs_to :user
belongs_to :calendar
validates_associated :user
validates :user, presence: true
end
When testing validations it is better to write on the assertions on the errors hash, as assert_not #administration.valid? will give a false positive if the validation fails for any other reason.
Bad:
test "user id should be present" do
#administration.user_id = nil
assert_not #administration.valid?
end
Good:
test "user id should be present" do
#administration.user_id = nil
#administration.valid?
assert #administration.errors.key?(:user)
end
Ok, we found the solution.
We updated our Administration model as follows:
class Administration < ActiveRecord::Base
belongs_to :user
belongs_to :calendar
validates :user_id, presence: true
validates :calendar_id, presence: true
end
And also edited our administration_test.rb file:
require 'test_helper'
class AdministrationTest < ActiveSupport::TestCase
def setup
#user = users(:noemie)
#calendar = calendars(:one)
# This code is not idiomatically correct.
#administration = Administration.new(user_id: #user.id, calendar_id: #calendar.id)
end
test "should be valid" do
assert #administration.valid?
end
test "user id should be present" do
#administration.user_id = nil
assert_not #administration.valid?
end
test "calendar id should be present" do
#administration.calendar_id = nil
assert_not #administration.valid?
end
end
The tests are now passing just fine.
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.
In my Rails project I have a User that can have many Projects which in turn can have many Invoices. Each Invoice can have many nested Items.
class Invoice < ActiveRecord::Base
attr_accessible :number, :date, :recipient, :project_id, :items_attributes
belongs_to :project
belongs_to :user
has_many :items, :dependent => :destroy
accepts_nested_attributes_for :items, :reject_if => :all_blank, :allow_destroy => true
validates :project_id, :presence => true
def build_item(user)
items.build(:price => default_item_price(user), :tax_rate => user.preference.tax_rate)
end
def set_number(user)
self.number ||= (user.invoices.maximum(:number) || 0).succ
end
end
class InvoicesController < ApplicationController
def new
#invoice = current_user.invoices.build(:project_id => params[:project_id])
#invoice.build_item(current_user)
#invoice.set_number(current_user)
#title = "New invoice"
end
def create
#invoice = current_user.invoices.build(params[:invoice])
if #invoice.save
flash[:success] = "Invoice created"
redirect_to invoices_path
else
render :new
end
end
end
Now, I am trying to test the creation of invoices with RSpec and FactoryGirl and all tests pass except for the ones related to the POST create action, such as:
it "saves the new invoice in the database" do
expect {
post :create, invoice: attributes_for(:invoice, project_id: #project, items_attributes: [ attributes_for(:item) ])
}.to change(Invoice, :count).by(1)
end
It keeps giving me this error:
Failure/Error: expect {
count should have been changed by 1, but was changed by 0
Can anybody tell me why this happens?
This is my factories.rb which I use to fabricate objects:
FactoryGirl.define do
factory :invoice do
number { Random.new.rand(0..1000000) }
recipient { Faker::Name.name }
date { Time.now.to_date }
association :user
association :project
end
factory :item do
date { Time.now.to_date }
description { Faker::Lorem.sentences(1) }
price 50
quantity 2
tax_rate 10
end
end
Can anybody help?
Thanks...
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