I have a many-to-many relationship with a join table in my Rails application. I'm using the has_many :through idiom in my models. To keep things simple, lets call my first class Student, my second class Course, and the join table class Enrollment (which contains fields student_id and course_id). I want to make sure that a given Student is associated with a given Course at most once (i.e. the {student_id, course_id} tuple should be unique in the enrollment table).
So I have a migration a that enforces this uniqueness.
def change
add_index :enrollments, [:student_id, :course_id], :unique => true
end
In addition my model classes are defined as such:
class Student < ActiveRecord::Base
has_many :enrollments
has_many :courses, :through => :enrollment
end
class Course < ActiveRecord::Base
has_many :enrollments
has_many :students, :through => :enrollment
end
class Enrollment < ActiveRecord::Base
belongs_to :student
belongs_to :course
validates :student, :presence => true
validates :course, :presence => true
validates :student_id, :uniqueness => {:scope => :course_id}
end
In a rails console, I can do the following:
student = Student.first
course = Course.first
student.courses << course
#... succeeds
student.courses << course
#... appropriately fails and raises an ActiveRecord::RecordInvalid exception
In my RSpec test, I do the exact same thing and I get no exception with the following code:
#student.courses << #course
expect { #student.courses << #course }.to raise_error(ActiveRecord::RecordInvalid)
And so my test fails and reports:
expected ActiveRecord::RecordInvalid but nothing was raised
What's going on here? What could I be doing wrong? How do I fix it?
Rails uses model level validation, if you want strict checking for uniquiness you need to use database level - foreign keys for example. But in this case you need to catch exceptions from database connector.
This is strange because in my code (very similar to your) validation for unique raises exception.
There's a couple of things here that could be happening:
#courses has changed between uses.
#student has changed between uses.
By using let you'll protect these values from changing between expectations.
let(:course) { Course.first }
let(:student) { Student.first }
subject{ student.courses << course << course }
it { should raise_error(ActiveRecord::RecordInvalid) }
Or, there could just be something wrong with your code :)
Related
I apologize if my English is not perfect but it is not my native language.
I have to test a MySql database with Rails 4.2, Rspec 3.3 and FactoryGirl 4.5.
The tests of base models are green. The problems comes when I have to test a model that contains foreign keys that can't be duplicated.
At first I have two models (dimension.rb through feature.rb and technical.rb) each having a foreign key that comes from the same model (current.rb):
#models/dimension.rb
class Dimension < ActiveRecord::Base
belongs_to :current
has_many :features
...
end
#models/feature.rb
class Feature < ActiveRecord::Base
belongs_to :dimension
has_many :bxes
...
end
#models/technical.rb
class Technical < ActiveRecord::Base
belongs_to :current
has_many :bxes
...
end
These two models are placed in the final model (bxe.rb)
#models/bxe.rb
class Bxe < ActiveRecord::Base
belongs_to :feature
belongs_to :technical
...
validates :technical_id, presence: true
validates :feature_id, presence: true
end
The Current model is:
#models/current.rb
class Current < ActiveRecord::Base
has_many :technicals
has_many :dimensions
validates :current, presence: true, uniqueness: true
validates :value, presence: true, uniqueness: true
end
The factories are the following:
#spec/factories/current.rb
FactoryGirl.define do
factory :current do
trait :lower do
current '800A'
value 800
end
trait :higher do
current '2000A'
value 2000
end
end
end
#spec/factories/dimension.rb
FactoryGirl.define do
factory :dimension do
...
trait :one do
current {create(:current, :lower)}
end
trait :two do
current {create(:current, :higher)}
end
end
end
#spec/factories/feature.rb
FactoryGirl.define do
factory :feature do
descr 'MyString'
dimension { create(:dimension, :one) }
...
end
end
#spec/factories/technical.rb
FactoryGirl.define do
factory :technical do
...
trait :A do
current { create(:current, :lower) }
end
trait :L do
current { create(:current, :higher) }
end
end
end
#spec/factories/bxes.rb
FactoryGirl.define do
factory :bxe do
...
technical {create(:technical, :A) }
feature
end
end
When I run the test on the models the first command (technical) runs and the factory creates a Current record with id = 1 but the second (features) fails, since the factory try again to create the record of Current with the same data, action prohibited from the model current.rb
#rspec spec/models
2.1.2 :001 > FactoryGirl.create(:bxebusbar, :one)
... Current Exists ... SELECT 1 AS one FROM `currents` WHERE `currents`.`current` = BINARY '800A' LIMIT 1
... INSERT INTO `currents` (`current`, `value`, `created_at`, `updated_at`) VALUES ('800A', 800, ..., ...)
... INSERT INTO `technicals` (..., `current_id`, ..., `created_at`, `updated_at`) VALUES (..., 1, ..., ...)
... Current Exists ... SELECT 1 AS one FROM `currents` WHERE `currents`.`current` = BINARY '800A' LIMIT 1
... ActiveRecord::RecordInvalid: Validation failed: Current has already been taken, Value has already been taken
I think that the problem can be solved by creating once only Current record and then using it in the technical and features factories, what would happen in reality, but I do not know how to do that.
Any suggestion? Thanks
You can use sequences to generate values that will not be duplicated. Another option is to use DatabaseCleaner and setup it to clean database after each test.
With first option:
FactoryGirl.define do
factory :current do
trait :lower do
sequence :current do { |n| "#{n}A" }
sequence :value do {|n| "#{n}" }
end
end
end
Or to setup DataBase Cleaner: Database cleaner
I have the following classes for a many to many relationship between "Item" and "Color".
And "Item" should not have duplicated "Colors",
for example:-
If "Item1" has "Blue" & "Red" then we cannot add another "Red" to "Item1"
Is this the correct way to set this up?
class Item < ActiveRecord::Base
has_many :item_colors
has_many :colors, :through => item_colors
end
class Color < ActiveRecord::Base
has_many :item_colors
has_many :items, :through => item_colors
end
class ItemColor < ActiveRecord::Base
belongs_to :item
belongs_to :color
validates_uniqueness_of :color, scope: :item
end
My Test for duplicated colors. Is it how to test it?
describe "item should not have duplicated colors" do
before do
#item = FactoryGirl.create(:item)
#color1 = FactoryGirl.create(:color)
#item.colors << #color1
#item.colors << #color1
#item.save
end
it { should_not be_valid }
end
When I try this in rails console, it will fail when I add duplcated color to an item
but instead of getting an error message in item.errors.message, I got an ActiveRecord exception
"ActiveRecord::RecordInvalid: Validation failed: Color has already been taken"
Please advise.
When you add the second color, it is automatically saved because the parent object #item is already saved, i.e. it is not a new_record.
Given it is a has_many :through association, it is always saved with the bang version of save!, which in turn raises the exception because your join model ItemColor fails on the validation of uniqueness.
In your case you have two options:
rescue the exception and manage the error messages manually or;
if you're using a join model just to add the validation layer, you could get rid of it, use a HABTM instead and handle the association as a set, e.g.
> item = FactoryGirl.create(:item)
> color = FactoryGirl.create(:color)
> 10.times { item.colors |= [color] } # you can add it n times...
> item.colors.count # => 1 ...still only one is saved b/c it is a union set.
How does that sound to you?
UPDATE: In case you really want to show an error message, you could, e.g.
if item.colors.include?(color)
item.errors.add(:colors, "color already selected")
else
item.colors |= [color]
end
Models:
A User has_one Ucellar
A Ucellar belongs_to User
I have confirmed from multiple sources that these are set up correctly. For posterity, here is the top portion of those two models.
class User < ActiveRecord::Base
has_many :authorizations
has_one :ucellar
validates :name, :email, :presence => true
This is actually the entire Ucellar model.
class Ucellar < ActiveRecord::Base
belongs_to :user
end
Ucellar has a column called user_id, which I know is necessary. The part of my application that creates a user uses the method create_with_oath. Below is the entire User class. Note the second line of the create method.
class User < ActiveRecord::Base
has_many :authorizations
has_one :ucellar
validates :name, :email, :presence => true
def create
#user = User.new(user_params)
#ucellar = #user.create_ucellar
end
def add_provider(auth_hash)
# Check if the provider already exists, so we don't add it twice unless authorizations.find_by_provider_and_uid(auth_hash["provider"], auth_hash["uid"])
Authorization.create :user => self, :provider => auth_hash["provider"], :uid => auth_hash["uid"]
end
end
def self.create_with_omniauth(auth)
user = User.create({:name => auth["info"]["name"], :email => auth["info"]["email"]})
end
private
def user_params
params.require(:user).permit(:name, :email)
end
end
EDIT:
Forgot to summarize the symptoms. On create, the user is in the db, with no exceptions thrown, and nothing to signify that anything went wrong. However, the related ucellar is never created. Per the documentation Here, the create method should create AND save the related ucellar.
It should create ucellar too.
Try to get the error messages after the creation by calling:
raise #user.errors.full_messages.to_sentence.inspect
I'm not sure why this wasn't working, but I ended up just moving this code out of the create action of the user controller, and putting it directly after an action that was creating a user. It solved my issue though. Thanks everyone for your help!
I've setup a nested form in my rails 3.2.3 app, it's working fine, my models are:
class Recipe < ActiveRecord::Base
attr_accessible :title, :description, :excerpt, :date, :ingredient_lines_attributes
has_and_belongs_to_many :ingredient_lines
accepts_nested_attributes_for :ingredient_lines
end
and:
class IngredientLine < ActiveRecord::Base
attr_accessible :ingredient_id, :measurement_unit_id, :quantity
has_and_belongs_to_many :recipes
belongs_to :measurement_unit
belongs_to :ingredient
end
As above, a Recipe can have multiple IngredientLines and vice versa.
What I'm trying to avoid is record duplication on IngredienLine table.
For example imagine that for recipe_1 an IngredientLine with {"measurement_unit_id" => 1, "ingredient_id" => 1, "quantity" => 3.5} is associated, if for recipe_5 the IngredientLine child form is compiled by the user with the same values, I don't want a new record on IngredientLine table, but only a new association record in the join table ingredient_lines_recipes.
Note that currently I dont't have any IngredientLine controller as saving and updating IngredientLines is handled by nested form routines. Even my Recipe controller is plain and standard:
class RecipesController < ApplicationController
respond_to :html
def new
#recipe = Recipe.new
end
def create
#recipe = Recipe.new(params[:recipe])
flash[:notice] = 'Recipe saved.' if #recipe.save
respond_with(#recipe)
end
def destroy
#recipe = Recipe.find(params[:id])
#recipe.destroy
respond_with(:recipes)
end
def edit
respond_with(#recipe = Recipe.find(params[:id]))
end
def update
#recipe = Recipe.find(params[:id])
flash[:notice] = 'Recipe updated.' if #recipe.update_attributes(params[:recipe])
respond_with(#recipe)
end
end
My guess is that should be enough to override the standard create behavior for IngredientLine with find_or_create, but I don't know how to achieve it.
But there's another important point to take care, imagine the edit of a child form where some IngredientLines are present, if I add another IngredientLine, which is already stored in IngredientLine table, rails of course should not write anything on IngredientLine table, but should also distinguish between child records already associated to the parent, and the new child record for which needs to create the relation, writing a new record on the join table.
Thanks!
in Recipe model redefine method
def ingredient_lines_attributes=(attributes)
self.ingredient_lines << IngredientLine.where(attributes).first_or_initialize
end
Old question but I had the same problem. Forgot to add :id to white list with rails 4 strong_parameters.
For example:
widgets_controller.rb
def widget_params
params.require(:widget).permit(:name, :foos_attributes => [:id, :name, :_destroy],)
end
widget.rb
class Widget < ActiveRecord::Base
has_many :foos, dependent: :destroy
accepts_nested_attributes_for :foos, allow_destroy: true
end
foo.rb
class Foo < ActiveRecord::Base
belongs_to :widget
end
I have run into a similar situation and found inspiration in this answer. In short, I don't worry about the duplication of nested models until save time.
Translated to your example, I added autosave_associated_records_for_ingredient_lines to Recipe. It iterates through ingredient_lines and performs a find_or_create as your intuition said. If ingredient_lines are complex, Yuri's first_or_initialize approach may be cleaner.
I believe this has the behavior you're looking for: nested models are never duplicated, but editing one causes a new record rather than updating a shared one. There is the strong possibility of orphaned ingredient_lines but if that's a serious concern you could choose to update if that model has only one recipe with an id that matches the current one.
I'm unclear on what this method actually does or when to use it.
Lets say I have these models:
Person < ...
# id, name
has_many :phone_numbers
end
PhoneNumber < ...
# id, number
belongs_to :person
validates_length_of :number, :in => 9..12
end
When I create phone numbers for a person like this:
#person = Person.find(1)
#person.phone_numbers.build(:number => "123456")
#person.phone_numbers.build(:number => "12346789012")
#person.save
The save fails because the first number wasn't valid. This is a good thing, to me. But what I don't understand is if its already validating the associated records what is the function validates_associated?
You can do has_many :phone_numbers, validate: false and the validation you're seeing wouldn't happen.
Why use validates_associated then? You might want to do validates_associated :phone_numbers, on: :create and skip validation on update (e.g. if there was already bad data in your db and you didn't want to hassle existing users about it).
There are other scenarios. has_one according to docs is validate: false by default. So you need validates_associated to change that.