Uniqueness error in has_many nested attributes - ruby-on-rails

I have a class student with has_many tests. The test class has a student_id, marks, name. Here the test name should be unique. The test is a nested attribute for student. So the parameters are this way:
:student => {:first_name => "abc",
:email => "dfsdf#sfdsdsd.bbb",
:tests_attributes => { "0" => {:name => "bgc", :marks => "470"}}}
I have a problem with update. If I update_attributes with the tests_attributes, it throws a validation error saying the name for test is not unique. I am actually addressing the same record here. How do I overcome this?

Without seeing your models (& validations), it's going to be quite difficult to diagnose your error directly.
--
Nested Attributes
We've done something like this, and found that your nested data is passed to the child model as if it were receiving a new object (without being nested). This means if you've got validates uniqueness for that model, it should be okay:
#app/models/test.rb
Class Test < ActiveRecord::Base
belongs_to :student
validates :name, uniqueness: true
end
Reason I write this is because there's a method called inverse_of, which basically allows you to access the parent model data in your child model
--
Update
I think the problem will likely lie with your use of update_attributes. Problem being you're trying to update both the student and the test attributes at one time.
I'm not sure exactly why this would be a problem, but I'd test this:
#app/controllers/students_controller.rb
class StudentsController < ApplicationController
def update
#student = Student.find params[:id]
#student.test.update(name: params[:test_name], marks: params[:marks])
end
end
I think if you can explain your methodology a little more, it will be much more helpful. I.E are you trying to update student or test? If you're updating student & adding a new test, how are you updating the studet?

Thanks for the reply guys. I ended up finding the answer myself. I did have a uniqueness validation for name.
I had a situation where initially I wouldn't know the student but have only his details. So I would have to create this hash and pass it to update. The trick to not trying to create a new record for the same name in test is to pass the actual record's ID along with it. This solved the problem

Nested Attributes
I think the problem with nested_attributes. For update need to pass nested_attributes with ID.
Ex.
:student => {:first_name => "abc",
:email => "dfsdf#sfdsdsd.bbb",
:tests_attributes => { "0" => {id: 1, :name => "bgc", :marks => "470"}}}
I have tried below-given example it is worked for me:
Update
#app/controllers/students_controller.rb
class StudentsController < ApplicationController
def update
#student = Student.find params[:id]
#student.update_attributes(student_params)
end
private
def student_params
params.require(:student).permit(:first_name, :email,
tests_attributes: [:id, :name, :marks])
end
end

Related

Validating uniqueness of an attribute in two tables/models

I have two different kind of user (devise) models in my Rails application. Doctor and Patient. It was my fault I haven't defined an upper class User and inherit attributes from it. They have a mutual attribute - a personal identification number and I want two check uniqueness of this attribute in both of tables. I've searched a bit and saw this answer.
I've applied things as it written there but it had no effect.
#patient.rb
class Patient < ActiveRecord::Base
...
validates_uniqueness_of :pin
validate :pin_not_on_doctors
private
def pin_not_on_doctors
Doctor.where(:pin => self.pin).first.nil?
end
end
#doctor.rb
class Doctor < ActiveRecord::Base
...
validates_uniqueness_of :pin
validate :pin_not_on_patients
private
def pin_not_on_patients
Patient.where(:pin => self.pin).first.nil?
end
end
First I created a patient instance, then a doctor instance with the same pin I used in my first (patient) case. Rails unexpectedly didn't spit out an error message and created doctor instance, and more interestingly devise also turned a blind eye for duplicate email.
How can I overcome this problem?
You should add the error on the validation function:
http://api.rubyonrails.org/classes/ActiveModel/Errors.html
def pin_not_on_doctors
errors.add :pin, "already exists" if Doctor.exists?(:pin => self.pin)
end
In addition to adding the error, try adding a line to return true/false,
Something like,
def pin_not_on_doctors
errors.add :pin, "already exits" if Doctor.exists?(:pin => self.pin)
Doctor.exists?(:pin => self.pin)
end
I don't know the details of your app, but in depending on how you're actually creating the object in this case, it might need that.
EDIT: Misread something in the original, looks like your current version is to just return true/false, so this probably doesn't help. Sorry.

Validate Associated Object Presence Before Create

I've been following the Getting Started rails tutorial and am now trying some custom functionality.
I have 2 models, Person and Hangout. A Person can have many Hangouts. When creating a Hangout, a Person has to be selected and associated with the new Hangout. I'm running into issues however when I call my create action. This fires before the validate_presence_of for person.
Am I going about this the wrong way? Seems like I shouldn't have to create a custom before_create validation to make sure that a Hangout was created with a Person.
#hangout_controller
def create
#person = Person.find(params[:hangout][:person_id])
#hangout = #person.hangouts.create(hangout_params)
#hangout.save
redirect_to hangouts_path(#hangout)
end
#hangout.rb
class Hangout < ActiveRecord::Base
belongs_to :person
validates_presence_of :person
end
#person.rb
class Person < ActiveRecord::Base
has_many :hangouts
validates :first_name, presence: true
validates :met_location, presence: true
validates :last_contacted, presence: true
def full_name
"#{first_name} #{last_name}"
end
end
Create action fires before the validate_presence_of for person
I think you are confused about rails MVC. Your form contains a url and when you submit your form your form params are send to your controller action according to the routes you have defined in routes.rb Your controller action, in this case create action, interacts with model this is very it checks for your validations and if all the validations are passed your object is saved in databse so even though in your app the control is first passed to your controller but your object is saved only once if all the validations are passed.
Now lets comeback to your code. There are couple of things you are doing wrong
a. You don't need to associate your person separately:
In your create action you have this line:
#person = Person.find(params[:hangout][:person_id])
You don't need to do this because your person_id is already coming from your form and it'll automatically associate your hangout with person.
b. You are calling create method instead of build:
When you call .association.create method it does two things for you it first initialize your object, in your case your hangout and if all the validations are passed it saves it. If all the validations are not passed it simply rollback your query.
If you'll use .association.build it'll only initialize your object with the params coming from your form
c. Validation errors won't show:
As explained above, since you are calling create method instead of build your validation error won't show up.
Fix
Your create method should look like this:
def create
#hangout = Hangout.new(hangout_params) # since your person_id is coming from form it'll automatically associate your new hangout with person
if #hangout.save
redirect_to hangouts_path(#hangout)
else
render "new" # this will show up validation errors in your form if your hangout is not saved in database
end
end
private
def hangout_params
params.require(:hangout).permit(:person_id, :other_attributes)
end
You are confused with the controller and model responsibilities.
Let me try to explain what I think is confusing you:
First try this in your rails console:
Hangout.create
It shouldn't let you because you are not passing a Person object to the create method. So, we confirm that the validation is working fine. That validation means that before creating a Hangout, make sure that there is a person attribute. All this is at the model level, nothing about controllers yet!
Let's go to the controllers part. When the create action of the controller 'is fired', that controller doesn't know what you are trying to do at all. It doesn't run any validations. It is just an action, that if you want, can call the Hangout model to create one of those.
I believe that when you say 'it fires' you are saying that the create action of the HangoutController is called first than the create method on the Hangout model. And that is completely fine. The validations run at the model level.
Nested Attributes
I think you'll be better using accepts_nested_attributes_for - we've achieved functionality you're seeking before by using validation on the nested model (although you'll be able to get away with using reject_if: :all_blank):
#app/models/person.rb
Class Person < ActiveRecord::Base
has_many :hangouts
accepts_nested_attributes_for :hangouts, reject_if: :all_blank
end
#app/models/hangout.rb
Class Hangout < ActiveRecord::Base
belongs_to :person
end
This will give you the ability to call the reject_if: :all_blank method -
Passing :all_blank instead of a Proc will create a proc that will
reject a record where all the attributes are blank excluding any value
for _destroy.
--
This means you'll be able to create the following:
#config/routes.rb
resources :people do
resources :hangouts # -> domain.com/people/:people_id/hangouts/new
end
#app/controllers/hangouts_controller.rb
Class HangoutsController < ApplicationController
def new
#person = Person.find params[:people_id]
#hangout = #person.hangouts.build
end
def create
#person = Person.find params[:people_id]
#person.update(hangout_attributes)
end
private
def hangout_attributes
params.require(:person).permit(hangouts_attributes: [:hangout, :attributes])
end
end
Although I've not tested the above, I believe this is the way you should handle it. This will basically save the Hangout associated object for a particular Person - allowing you to reject if the Hangout associated object is blank
The views would be as follows:
#app/views/hangouts/new.html.erb
<%= form_for [#person, #hangout] do |f| %>
<%= f.fields_for :hangouts do |h| %>
<%= h.text_field :name %>
<% end %>
<%= f.submit %>
<% end %>

Ruby On Rails Model has no methods

I have a model with a belongs to relationship.
class Product < ActiveRecord::Base
attr_accessible :name, :price, :request_id, :url
# Relationships
belongs_to :request
end
class Request < ActiveRecord::Base
attr_accessible :category, :keyword
# Relationships
has_many :products
end
This is the code in my controller function
product = Product.where({ :asin => asin }).first
# See if the product exists
begin
#This throws a method not found error for where
product = Product.where({ :name => name }).first
rescue
Product.new
# This throws a method not found error for request_id
product.request_id = request.id
product.save
end
I'm trying to create a new product object like so
product = Product.first(:conditions => { :name => name })
When I call that I get an error saying undefined method 'first' for Product:Class
I tried doing Product.new and I can't access any attributes. I get this for every one undefined method 'request_id=' for #<Product:0x007ffce89aa7f8>
I've been able to save request objects. What am I doing wrong with products?
EDIT:
So as it turns out there was an old Product data type that was being imported that wasn't an ActiveRecord class. It was using that instead of my Product::ActiveRecord. I deleted that import and it's good to go. Sorry to have wasted everybody's time.
Not sure what the proper protocol is here for what to do with this question.
Is your Product class an ActiveRecord::Base class? You can find out by running:
Product.ancestors.include?(ActiveRecord::Base)
If this returns false, it's getting the class loaded from somewhere else.
First check to see that your Product class is set up correctly by typing in:
rails c
# after console has loaded
Product
If this looks correct then we will try to instantiate a product by calling:
# Create a new product
product = Product.new(name: "first product", price: 100, url: "http://www.example.com")
# Persist this object to the database
product.save
If you are missing any attributes run another migration to add them to the Product table.
If none of those suggestions work, check to make sure that there isn't an existing class with the same name in your project. This would cause all kinds of errors and would explain certain methods not being found.

Automatically mapping associations in Mongoid using params

I'm using Mongoid and when I .update_attributes on a model that has a references_one using params[:model_name] I get the error...
#model.update_attributes(params[:model_name])
undefined method `associations' for "...":String
I understand why this is happening. Mongoid is trying to map that .association_name to the string value in the params hash when what it wants is a reference to another Mongoid::Document. That I get.
What I'd like to know is if there is a global way to fix this. For the moment I've gotten around this issue by doing something like the following...
model_params = params[:model_name]
if model_params.has_key? :relationship
model_params[:relationship] = RelatedModel.first(:conditions => { :_id => model_params[:relationship] })
end
This works but I'd rather have a fix that fixes it every time so that I'm not manually mapping the related model every time I do an update. That would defiantly be a violation of DRY.
Here's sample module that you could include in all your models
module MyAppBase
def my_update_attributes(model_params,related_model)
if model_params.has_key? :relationship
model_params[:relationship] = related_model.first(:conditions => { :_id => model_params[:relationship] })
end
self.update_attributes(model_params)
end
end
#include it in your model classes
class MyModel < ActiveRecord::Base
include MyAppBase
#normal model code
end

I feel like this needs to be refactored - any help? Ruby modeling

So let's say you have
line_items
and line_items belong to a make and a model
a make has many models and line items
a model belongs to a make
For the bare example idea LineItem.new(:make => "Apple", :model => "Mac Book Pro")
When creating a LinteItem you want a text_field box for a make and a model. Makes and models shouldn't exist more than once.
So I used the following implementation:
before_save :find_or_create_make, :if => Proc.new {|line_item| line_item.make_title.present? }
before_save :find_or_create_model
def find_or_create_make
make = Make.find_or_create_by_title(self.make_title)
self.make = make
end
def find_or_create_model
model = Model.find_or_create_by_title(self.model_title) {|u| u.make = self.make}
self.model = model
end
However using this method means I have to run custom validations instead of a #validates_presence_of :make due to the associations happening off a virtual attribute
validate :require_make_or_make_title, :require_model_or_model_title
def require_make_or_make_title
errors.add_to_base("Must enter a make") unless (self.make || self.make_title)
end
def require_model_or_model_title
errors.add_to_base("Must enter a model") unless (self.model || self.model_title)
end
Meh, this is starting to suck. Now where it really sucks is editing with forms. Considering my form fields are a partial, my edit is rendering the same form as new. This means that :make_title and :model_title are blank on the form.
I'm not really sure what the best way to rectify the immediately above problem is, which was the final turning point on me thinking this needs to be refactored entirely.
If anyone can provide any feedback that would be great.
Thanks!
I don't think line_items should belong to a make, they should only belong to a model. And a model should have many line items. A make could have many line items through a model. You are missing a couple of methods to have your fields appear.
class LineItem
belongs_to :model
after_save :connect_model_and_make
def model_title
self.model.title
end
def model_title=(value)
self.model = Model.find_or_create_by_title(value)
end
def make_title
self.model.make.title
end
def make_title=(value)
#make = Make.find_or_create_by_title(value)
end
def connect_model_and_make
self.model.make = #make
end
end
class Model
has_many :line_items
belongs_to :make
end
class Make
has_many :models
has_many :line_items, :through => :models
end
It's really not that bad, there's just not super easy way to do it. I hope you put an autocomplete on those text fields at some point.

Resources