Hi i'm using a has_and_belongs_to_many in a model.
I want set the valitor of presence for kinds.
and set the max number of kinds per core to 3
class Core < ActiveRecord::Base
has_and_belongs_to_many :kinds, :foreign_key => 'core_id', :association_foreign_key => 'kind_id'
end
how can i do?
thanks
validate :require_at_least_one_kind
validate :limit_to_three_kinds
private
def require_at_least_one_kind
if kinds.count == 0
errors.add_to_base "Please select at least one kind"
end
end
def limit_to_three_kinds
if kinds.count > 3
errors.add_to_base "No more than 3 kinds, please"
end
end
You could try something like this (tested on Rails 2.3.4):
class Core < ActiveRecord::Base
has_and_belongs_to_many :kinds, :foreign_key => 'core_id', :association_foreign_key => 'kind_id'
validate :maximum_three_kinds
validate :minimum_one_kind
def minimum_one_kind
errors.add(:kinds, "must total at least one") if (kinds.length < 1)
end
def maximum_three_kinds
errors.add(:kinds, "must not total more than three") if (kinds.length > 3)
end
end
... which works in the following way:
require 'test_helper'
class CoreTest < ActiveSupport::TestCase
test "a Core may have kinds" do
core = Core.new
3.times { core.kinds << Kind.new }
assert(core.save)
end
test "a Core may have no more than 3 kinds" do
core = Core.new
4.times { core.kinds << Kind.new }
core.save
assert_equal(1, core.errors.length)
assert_not_nil(core.errors['kinds'])
end
test "a Core must have at least one kind" do
core = Core.new
core.save
assert_equal(1, core.errors.length)
assert_not_nil(core.errors['kinds'])
end
end
Obviously the above isn't particularly DRY or production-ready, but you get the idea.
Related
I work on a booking application, where each Home can have several Phone.
I would like to limit the number of Phone by Home to 3, and display a nice error in the create phone form.
How can I achieve that, in a rails way ?
code
class Phone < ApplicationRecord
belongs_to :user
validates :number, phone: true
# validates_associated :homes_phones
has_many :homes_phones, dependent: :destroy
has_many :homes, through: :homes_phones
end
class User < ApplicationRecord
has_many :phones, dependent: :destroy
end
class HomesPhone < ApplicationRecord
belongs_to :home
belongs_to :phone
validate :check_phones_limit
def check_phones_limit
errors.add(:base, "too_many_phones") if home.phones.size >= 3
end
end
specs
it 'should limit phones to 3' do
user = create(:user)
home = create(:home, :active, manager: user)
expect(home.phones.create(user: user, number: "+33611223344")).to be_valid
expect(home.phones.create(user: user, number: "+33611223345")).to be_valid
expect(home.phones.create(user: user, number: "+33611223346")).to be_valid
# unexpectedly raises a ActiveRecord::RecordInvalid
expect(home.phones.create(user: user, number: "+33611223347")).to be_invalid
end
Side notes
My understanding of the flow is:
a transaction opens
phone attributes are validated (and valid)
phone is created, a primary key is available
homes_phone is saved! and an error is raised because the validation fails
the all transaction is rolled back, and the error bubble up
I have tried:
has_many before_add in Home which also raise an error;
validating these rules in Phone does not make sense to me, as this rule is a Home concern
You can just validate it in the controller, count the phones and render a flash error, before you actually try and save the records.
Doing this in callbacks is difficult and not foolproof.
I've added some quick tests to cover different ways phones can be created:
# spec/models/homes_phone_spec.rb
require "rails_helper"
RSpec.describe HomesPhone, type: :model do
it "saves 3 new phones" do
home = Home.create(phones: 3.times.map { Phone.new })
expect(home.phones.count).to eq 3
end
it "doesn't save 4 new phones" do
home = Home.create(phones: 4.times.map { Phone.new })
expect(home.phones.count).to eq 0
expect(home.phones.size).to eq 4
end
it "can create up to 3 three phones through association" do
home = Home.create!
expect do
5.times { home.phones.create }
end.to change(home.phones, :count).by(3)
end
it "doesn't add 4th phone to existing record" do
Home.create(phones: 3.times.map { Phone.new })
home = Home.last
# NOTE: every time you call `valid?` or `validate`, it runs validations
# again and all previous errors are cleared.
# expect(home.phones.create).to be_invalid
expect(home.phones.create).to be_new_record
# or
# expect(home.phones.create).not_to be_persisted
# expect(home.phones.create.errors.any?).to be true
end
it "adds phone limit validation error to Phone" do
home = Home.create(phones: 3.times.map { Phone.new })
phone = home.phones.create
expect(phone.errors[:base]).to eq ["too many phones"]
end
end
# app/models/phone.rb
class Phone < ApplicationRecord
has_many :homes_phones, dependent: :destroy
has_many :homes, through: :homes_phones
end
# app/models/home.rb
class Home < ApplicationRecord
has_many :homes_phones, dependent: :destroy
has_many :phones, through: :homes_phones
end
# app/models/homes_phone.rb
class HomesPhone < ApplicationRecord
belongs_to :home
# NOTE: setting `inverse_of` option will stop raising errors and
# just return `phone` with errors that we'll add below
belongs_to :phone, inverse_of: :homes_phones
# because of the way through association is saved with `save!` call
# it raises validation errors. `inverse_of` allows the through association
# to be properly built and it skips `save!` call:
# https://github.com/rails/rails/blob/v7.0.4.2/activerecord/lib/active_record/associations/has_many_through_association.rb#L79-L80
validate do
# NOTE: if you use `homes_phones` association, the `size` method returns
# current count, instead of a lagging `phones.size` count.
if home.homes_phones.size > 3
errors.add(:base, "too many phones")
phone.errors.merge!(errors)
end
end
end
$ rspec spec/models/homes_phone_spec.rb
HomesPhone
saves 3 new phones
doesn't save 4 new phones
can create up to 3 three phones through association
doesn't add 4th phone to existing record
adds phone limit validation to Phone
Finished in 0.10967 seconds (files took 2.35 seconds to load)
5 examples, 0 failures
But it doesn't cover everything, like this:
it "can append up to 3 three phones" do
home = Home.create!
expect do
5.times { home.phones << Phone.new }
end.to change(home.phones, :count).by(3)
end
$ rspec spec/models/homes_phone_spec.rb:38
Run options: include {:locations=>{"./spec/models/homes_phone_spec.rb"=>[38]}}
HomesPhone
can append up to 3 three phones (FAILED - 1)
and I thought I fixed everything. You can try this instead:
after_validation do
if home.homes_phones.size > 3
errors.add(:base, "too many phones")
phone.errors.merge! errors
raise ActiveRecord::Rollback
end
end
$ rspec spec/models/homes_phone_spec.rb -f p
......
Finished in 0.12411 seconds (files took 2.25 seconds to load)
6 examples, 0 failures
This is the rails way of doing this, in your Home class
validates :phones, length: { maximum: 3, message: "too many phones" }
You could think validation is something like this: it target to the Model we want to create directly.
In your case, you want to validate when create phones, so you have to put the validation rule into the Phone model.
This way the validation will take effect, and you could get the error message correctly.
I have a LabCollection:
class LabCollection < ApplicationRecord
# Relationships
belongs_to :lab_container, polymorphic: true, optional: true
has_many :lab_collection_labs
has_many :labs, -> { published }, through: :lab_collection_labs
has_many :lab_collection_inclusions, dependent: :destroy
end
It has many LabCollectionLabs:
class LabCollectionLab < ApplicationRecord
acts_as_list scope: :lab_collection_id, add_new_at: :bottom
belongs_to :lab_collection
belongs_to :lab
end
Which has a lab ID and belongs to a lab.
I have a spec which tests how new associations are created, and it's failling at the following point:
context 'when the lab container has labs already present' do
it 'removes the present labs and adds the new ones' do
subject = RawLabAdder
expect(populated_lab_collection.labs).to eq labs
subject.new(populated_lab_collection, [lab4.id, lab5.id]).perform
expect(populated_lab_collection.labs).not_to eq labs
binding.pry
expect(populated_lab_collection.labs).to eq [lab4, lab5]
end
end
If you need to see the internal working of the code let me know, however the issue seems to be with RSpec and refreshing associations. When I hit the binding.pry point and call populated_lab_collection.lab_collection_labs
populated_lab_collection.lab_collection_labs
=>
[lab_collection_lab_4, lab_collection_lab_5]
However when I call .labs instead:
populated_lab_collection.labs
=>
[]
Inspecting the lab_collection_labs, I can see that they each have a lab_id and that a lab exists for those ID's. I believe my problem is that I'm not refreshing the records correctly, however I've tried:
# populated_lab_collection.reload
# populated_lab_collection.lab_collection_labs.reload
# populated_lab_collection.lab_collection_labs.each do |x|
# x.lab.reload
# end
# populated_lab_collection.labs.reload
Any advice on how I can get RSpec to correctly read in a records nested associations is greatly appreciated. As I say when I inspect the record, it has 2 lab_inclusion_labs, which each have a lab, however the parent record apparently has no labs.
EDIT: RawLabAdder class:
module LabCollections
class RawLabAdder
def initialize(incoming_lab_collection, lab_ids = [])
#lab_ids = lab_ids
#lab_collection = incoming_lab_collection
end
def perform
remove_associated_labs
add_labs
end
private
def add_labs
#lab_ids.each do |x|
lab = Lab.find(x)
LabCollectionInclusionAdder.new(#lab_collection, lab).perform
end
end
def remove_associated_labs
#lab_collection.lab_collection_inclusions.map(&:destroy)
#lab_collection.lab_collection_labs.map(&:destroy)
end
end
end
If you create an instance in a before hook or in a spec and then perform some database related work on it the instance you have will no longer reference the up to date information.
Try reloading the populated_lab_collection before asserting.
expect(populated_lab_collection.reload.labs).not_to eq labs
I have methods that i'm trying to test in my models, but they're not working well, it doesn't seem to return false when it should- any suggestions?
class Registration < ActiveRecord::Base
validate :check_duplicate_section
def check_duplicate_section
all_registrations = Registration.all
all_registrations.each do |reg|
puts reg.section_id
if reg.section_id == self.section_id && reg.student_id == self.student_id
errors.add(:registration, "Already exists")
return false
end
return true
end
end
Test File: (#bruce is defined earlier)
class RegistrationTest < ActiveSupport::TestCase
should "not allow invalid student registrations" do
#mike = FactoryGirl.create(:student, :first_name => "Mike")
good_reg = FactoryGirl.build(:registration, :section => #section2, :student => #mike)
bad_reg = FactoryGirl.build(:registration, :section => #section1, :student => #bruce)
bad_reg2 = FactoryGirl.build(:registration, :section => #section2, :student => #mike)
assert_equal true, good_reg.valid?
assert_equal false, bad_reg.valid?
assert_equal false, bad_reg2.valid?
from the looks of what you're trying to do with check_duplicate_section, it's better to use the built in uniqueness validation
validates :section_id, uniqueness: { scope: :user_id }
If you don't want to use this, change your method to
def check_duplicate_section
if Registration.where(section_id: self.section_id, student_id: self.student_id).exists?
errors.add :registration, "Already exists"
end
end
Also, in your tests, you are using build which doesn't save anything to the db. You should use create or better yet, use mocks to force the returned values of your db queries.
The good thing about using the built in validation approach is you don't need to test it because it should work.
Here is the relevant section from my models:
belongs_to :cart
belongs_to :product
validate :quantity, :more_than_stock, :message => "more than in stock is reserved."
def more_than_stock
errors.add(:quantity, "should be less than in stock") if self.quantity > self.product.stock
end
I keep erroring out on this line: errors.add(:quantity, "should be less than in stock") if self.quantity > self.product.stock with regards to the .stock method.
The error I keep getting is: 1) Error:
test_product_id_must_be_a_number(CartRowTest):
NoMethodError: undefined method 'stock' for nil:NilClass in my tests.
It seems to me that my test suite doesn't know about the .stock method on product.
However, here is my product factory:
factory :product do
name 'Cholecap - 100mg'
limit 3
stock 10
end
and my cart_row factory:
factory :cart_row do
product
cart
quantity 3
end
Here is the relevant portion of my unit test that throws the error:
def setup
#cart_row = FactoryGirl.create(:cart_row)
end
test "product_id must not be blank" do
#cart_row.product_id = " "
assert !#cart_row.valid?
end
test "product_id must be a number" do
#cart_row.product_id = '234'
assert !#cart_row.valid?
end
What do I need to do to let the test suite know about the .stock method?
Because you set product_id to invalid value you can't make test suite know about the #stock method. If you realy want make these test pass try this code:
belongs_to :cart
belongs_to :product
validates_associated :product
validate :quantity, :more_than_stock, message: "more than in stock is reserved." , if: "product.respond_to? :stock"
def more_than_stock
errors.add(:quantity, "should be less than in stock") if quantity > product.stock
end
Consider the following:
ScheduledSession ------> Applicant <------ ApplicantSignup
Points to note:
A ScheduledSession will exist in the system at all times; think of this as a class or course.
The intent here is to validate the ApplicantSignup model against an attribute on ScheduledSession during signups_controller#create
Associations
class ScheduledSession < ActiveRecord::Base
has_many :applicants, :dependent => :destroy
has_many :applicant_signups, :through => :applicants
#...
end
class ApplicantSignup < ActiveRecord::Base
has_many :applicants, :dependent => :destroy
has_many :scheduled_sessions, :through => :applicants
#...
end
class Applicant < ActiveRecord::Base
belongs_to :scheduled_session
belongs_to :applicant_signup
# TODO: enforce validations for presence
# and uniqueness constraints etc.
#...
end
SignupsController
Resources are RESTful, i.e. the #create action will have a path that's similar to /scheduled_sessions/:id/signups/new
def new
#session = ScheduledSession.find(params[:scheduled_session_id])
#signup = #session.signups.new
end
def create
#session = ScheduledSession.find(params[:scheduled_session_id])
#session.duration = (#session.end.to_time - #session.start.to_time).to_i
#signup = ApplicantSignup.new(params[:signup].merge(:sessions => [#session]))
if #signup.save
# ...
else
render :new
end
end
You'll notice I'm setting a virtual attribute above #session.duration to prevent Session from being considered invalid. The real 'magic' if you will happens in #signup = ApplicantSignup.new(params[:signup].merge(:sessions => [#session])) which now means that in the model I can select from self.scheduled_sessions and access the ScheduledSession this ApplicantSignup is being built against, even though at this very point in time, there is no record present in the join table.
Model validations for example look like
def ensure_session_is_upcoming
errors[:base] << "Cannot signup for an expired session" unless self.scheduled_sessions.select { |r| r.upcoming? }.size > 0
end
def ensure_published_session
errors[:base] << "Cannot signup for an unpublished session" if self.scheduled_sessions.any? { |r| r.published == false }
end
def validate_allowed_age
# raise StandardError, self.scheduled_sessions.inspect
if self.scheduled_sessions.select { |r| r.allowed_age == "adults" }.size > 0
errors.add(:dob_year) unless (dob_year.to_i >= Time.now.strftime('%Y').to_i-85 && dob_year.to_i <= Time.now.strftime('%Y').to_i-18)
# elsif ... == "children"
end
end
The above works quite well in development and the validations work as expected — but how does one test with with Factory Girl? I want unit tests to guarantee the business logic I've implemented after all — sure, this is after the fact but is still one way of going about TDD.
You'll notice I've got a commented out raise StandardError, self.scheduled_sessions.inspect in the last validation above — this returns [] for self.scheduled_sessions which indicates that my Factory setup is just not right.
One of Many Attempts =)
it "should be able to signup to a session" do
scheduled_session = Factory.build(:scheduled_session)
applicant_signup = Factory.build(:applicant_signup)
applicant = Factory.create(:applicant, :scheduled_session => scheduled_session, :applicant_signup => applicant_signup)
applicant_signup.should be_valid
end
it "should be able to signup to a session for adults if between 18 and 85 years" do
scheduled_session = Factory.build(:scheduled_session)
applicant_signup = Factory.build(:applicant_signup)
applicant_signup.dob_year = 1983 # 28-years old
applicant = Factory.create(:applicant, :scheduled_session => scheduled_session, :applicant_signup => applicant_signup)
applicant_signup.should have(0).error_on(:dob_year)
end
The first one passes, but I honestly do not believe it's properly validating the applicant_signup model; the fact that self.scheduled_sessions is returning [] simply means that the above just isn't right.
It's quite possible that I'm trying to test something outside the scope of Factory Girl, or is there a far better approach to tackling this? Appreciate all comments, advice and constructive criticism!
Updates:
Not sure what this is called but this is the approach taken at least with regards to how it's implemented at the controller level
I need to consider ignoring Factory Girl for the association aspect at least and attempt to return the scheduled_session by mocking scheduled_sessions on the applicant_signup model.
Factories
FactoryGirl.define do
factory :signup do
title "Mr."
first_name "Franklin"
middle_name "Delano"
last_name "Roosevelt"
sequence(:civil_id) {"#{'%012d' % Random.new.rand((10 ** 11)...(10 ** 12))}"}
sequence(:email) {|n| "person#{n}##{(1..100).to_a.sample}example.com" }
gender "male"
dob_year "1980"
sequence(:phone_number) { |n| "#{'%08d' % Random.new.rand((10 ** 7)...(10 ** 8))}" }
address_line1 "some road"
address_line2 "near a pile of sand"
occupation "code ninja"
work_place "Dharma Initiative"
end
factory :session do
title "Example title"
start DateTime.civil_from_format(:local,2011,12,27,16,0,0)
duration 90
language "Arabic"
slides_language "Arabic & English"
venue "Main Room"
audience "Diabetic Adults"
allowed_age "adults"
allowed_gender "both"
capacity 15
published true
after_build do |session|
# signups will be assigned manually on a per test basis
# session.signups << FactoryGirl.build(:signup, :session => session)
end
end
factory :applicant do
association :session
association :signup
end
#...
end
My earlier assumption was correct, with on small change:
I need to consider ignoring Factory Girl for the association aspect at
least and attempt to return the scheduled_session by stubbing
scheduled_sessions on the applicant_signup model.
making my tests quite simply:
it "should be able to applicant_signup to a scheduled_session" do
scheduled_session = Factory(:scheduled_session)
applicant_signup = Factory.build(:applicant_signup)
applicant_signup.stub!(:scheduled_sessions).and_return{[scheduled_session]}
applicant_signup.should be_valid
end
it "should be able to applicant_signup to a scheduled_session for adults if between 18 and 85 years" do
scheduled_session = Factory(:scheduled_session)
applicant_signup = Factory.build(:applicant_signup)
applicant_signup.dob_year = 1983 # 28-years old
applicant_signup.stub!(:scheduled_sessions).and_return{[scheduled_session]}
applicant_signup.should have(0).error_on(:dob_year)
applicant_signup.should be_valid
end
and this test in particular required a similar approach:
it "should not be able to applicant_signup if the scheduled_session capacity has been met" do
scheduled_session = Factory.build(:scheduled_session, :capacity => 3)
scheduled_session.stub_chain(:applicant_signups, :count).and_return(3)
applicant_signup = Factory.build(:applicant_signup)
applicant_signup.stub!(:scheduled_sessions).and_return{[scheduled_session]}
applicant_signup.should_not be_valid
end
...and success — ignore the testing duration as spork causes false reporting of this.
Finished in 2253.64 seconds
32 examples, 0 failures, 3 pending
Done.
As another approach you could use Rspecs stub_model.
Also, if you test ApplicantSignup, you should init it and not test the creation of the Applicant. Eg:
applicant_signup = Factory.build(:applicant_signup);
applicant_signup.should_receive(:scheduled_sessions)
.and_return{[scheduled_session]};
So there will be less DB access and you will test ApplicantSignup, not Applicant.