Setup and Testing to prevent duplication in ActiveRecord many to many relationship - activerecord

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

Related

ActiveRecord association seems to read differently depending on reading forward or backward

I've read a ton of ActiveRecord SO Questions and haven't come across this yet. I know that the following code snippet is a little long winded but I'm not using Rails so I wanted to be clear about how my database was created/structured.
A household has the head (or heads) of household (like mom and dad), and it has children. Mom, Dad and the kids are all members of the household. I tried to implement that as follows. This is the full code snippet so you can just copy and paste and run it if you have active_record and sqlite3.
I wrote the question in-line in the code, but here it is in case you don't want to skim the code: when I do household.heads I get the members which I assigned as heads of household. But when I run member.household.heads (on the same household!) I don't get the heads, I get the kids! My only thought is that I shouldn't be using two 'has_many's with the same foreign_key, but everything else I've tried doesn't work.
require 'active_record'
ActiveRecord::Base.logger = Logger.new(File.open('database.log', 'w'))
ActiveRecord::Base.establish_connection(
:adapter => 'sqlite3',
:database => 'test.db'
)
ActiveRecord::Schema.define do
unless ActiveRecord::Base.connection.tables.include? 'members'
create_table :members do |table|
table.integer :household_id
table.integer :head_id
table.integer :child_id
table.string :name
end
end
unless ActiveRecord::Base.connection.tables.include? 'households'
create_table :households do |table|
table.string :address
end
end
end
class Member < ActiveRecord::Base
belongs_to :household
end
class Household < ActiveRecord::Base
has_many :heads, class_name: "Member", foreign_key: :household_id
has_many :children, class_name: "Member", foreign_key: :household_id
end
#Create some members
m1 = Member.create(name: "Foo")
m2 = Member.create(name: "Bar") #Foo's wife
m3 = Member.create(name: "foo-foo") #Foo and Bar's little girl
#Create a household
h1 = Household.create(address: "123 Fake St.")
#Assign members to households
h1.heads = [m1, m2]
h1.children = [m3]
#first let's check, h1 is m1's household. The two are the same.
p h1.id == m1.household.id
#So why doesn't this work?
h1.heads.each{|head| p head.name} #returns Foo and Bar
m1.household.heads.each{|head| p head.name} #<= Doesn't return Foo and Bar ?!?
h1.children.each{|child| p child.name} #returns foo-foo
m1.household.children.each{|child| p child.name} #Also returns foo-foo as expected
As it turns out, ActiveRecord is not OK with two Models of the same class being referred to by the same foreign key (household_id). You're just overwriting the assignment of h1's members when you assign children (or something like that, this still isn't clear to me).
You either need different classes: Head and Child or you need to be ok with the child calling the household differently.
I ended up changing the foreign keys to: household_id and house_id since I need heads.household more than I need child.household
(I still think it is annoying to use children.house but oh well!)
To be clear:
class Household < ActiveRecord::Base
has_many :heads, class_name: "Member", foreign_key: :household_id
has_many :children, class_name: "Member", foreign_key: :house_id
end

Rails 3. Decide on save if the object should be saved or not

iam just asking myself, whats the best solution for my problem.
Here are my models:
class Product < ActiveRecord::Base
has_many :prices, :class_name => "ProductPrice"
accepts_nested_attributes_for :prices
end
class ProductPrice < ActiveRecord::Base
belongs_to :product
end
The controller
def create
#product = Product.new(params[:product])
#product.save
...
end
What i want to do is to prevent all ProductPrices from being saved when product_price.value == nil or product_price.value == 0.0
before_save hook in ProductPrice. return false will rollback the whole transaction, thats not what i want to do. i just want to "kick" all prices with value == 0 or value == nil
first kick all price_params from params[...] and than call Product.new(params[:product]) seems not to be the rails way eighter...
after Product.new(params[:product]) iterate over all prices and delete them from the array. but the logic should be in my models right? i just dont want to repeat myself on every controller that creates new prices...
can someone tell me the best solution for that? whats the rails way?
thanks!
What you want it called a validation hook, something like this:
class ProductPrice < ActiveRecord::Base
belongs_to :product
validates :value, :numericality => {:greater_than => 0.0 }
end
See http://guides.rubyonrails.org/active_record_validations_callbacks.html for other ways you may want to do this with finer control.
To avoid adding these invalid prices in the first place, you can remove them from the nested attributes hash like this:
class Product < ActiveRecord::Base
def self.clean_attributes!(product_params)
product_prices = product_params['prices'] || []
product_prices.reject!{|price| price['value'].to_f == 0 rescue true }
end
end
Product.clean_attributes!(params[:product])
Product.new(params[:product])

Rails nested form on many-to-many: how to prevent duplicates?

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.

Many-to-Many Uniqueness Constraint Test Not Working

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 :)

Rails3: Nested model - child validates_with method results in "NameError - uninitialized constant [parent]::[child]"

Consider the following parent/child relationship where Parent is 1..n with Kids (only the relevant stuff here)...
class Parent < ActiveRecord::Base
# !EDIT! - was missing this require originally -- was the root cause!
require "Kid"
has_many :kids, :dependent => :destroy, :validate => true
accepts_nested_attributes_for :kids
validates_associated :kids
end
class Kid < ActiveRecord::Base
belongs_to :parent
# for simplicity, assume a single field: #item
validates_presence_of :item, :message => "is expected"
end
The validates_presence_of methods on the Kid model works as expected on validation failure, generating a final string of Item is expected per the custom message attribute supplied.
But if try validates_with, instead...
class Kid < ActiveRecord::Base
belongs_to :parent
validates_with TrivialValidator
end
class TrivialValidator
def validate
if record.item != "good"
record.errors[:base] << "Bad item!"
end
end
end
...Rails returns a NameError - uninitialized constant Parent::Kid error following not only an attempt to create (initial persist) user data, but also when even attempting to build the initial form. Relevant bits from the controller:
def new
#parent = Parent.new
#parent.kids.new # NameError, validates_* methods called within
end
def create
#parent = Parent.new(params[:parent])
#parent.save # NameError, validates_* methods called within
end
The error suggests that somewhere during model name (and perhaps field name?) resolution for error message construction, something has run afoul. But why would it happen for some validates_* methods and not others?
Anybody else hit a wall with this? Is there some ceremony needed here that I've left out in order to make this work, particularly regarding model names?
After a few hours away, and returning fresh -- Was missing require "Kid" in Parent class. Will edit.

Resources