Validate presence of polymorphic parent - ruby-on-rails

I am developing a Rails 3.2 application with the following models:
class User < ActiveRecord::Base
# Associations
belongs_to :authenticatable, polymorphic: true
# Validations
validates :authenticatable, presence: true # this is the critical line
end
class Physician < ActiveRecord::Base
attr_accessible :user_attributes
# Associations
has_one :user, as: :authenticatable
accepts_nested_attributes_for :user
end
What I am trying to do is validate whether a user always has an authenticatable parent. This works fine in itself, but in my form the user model complains that the authenticatable is not present.
I am using the following controller to show a form for a new physician which accepts nested attributes for the user:
def new
#physician = Physician.new
#physician.build_user
respond_to do |format|
format.html # new.html.erb
format.json { render json: #physician }
end
end
And this is my create method:
def create
#physician = Physician.new(params[:physician])
respond_to do |format|
if #physician.save
format.html { redirect_to #physician, notice: 'Physician was successfully created.' }
format.json { render json: #physician, status: :created, location: #physician }
else
format.html { render action: "new" }
format.json { render json: #physician.errors, status: :unprocessable_entity }
end
end
end
On submitting the form, it says that the user's authenticatable must not be empty. However, the authenticatable_id and authenticatable_type should be assigned as soon as #physician is saved. It works fine if I use the same form to edit a physician and its user, since then the id and type are assigned.
What am I doing wrong here?

I believe this is expected:
https://github.com/rails/rails/issues/1629#issuecomment-11033182 ( last two comments).
Also check this out from rails api:
One-to-one associations
Assigning an object to a has_one association automatically saves that
object and the object being replaced (if there is one), in order to
update their foreign keys - except if the parent object is unsaved
(new_record? == true).
If either of these saves fail (due to one of the objects being
invalid), an ActiveRecord::RecordNotSaved exception is raised and the
assignment is cancelled.
If you wish to assign an object to a has_one association without
saving it, use the build_association method (documented below). The
object being replaced will still be saved to update its foreign key.
Assigning an object to a belongs_to association does not save the
object, since the foreign key field belongs on the parent. It does not
save the parent either.
and this
build_association(attributes = {}) Returns a new object of the
associated type that has been instantiated with attributes and linked
to this object through a foreign key, but has not yet been saved.
You have to create a Parent first. Then assign it's id to polymorphic object.
From what I can see, you create an object Physician.new which builds User but at this point it's not saved yet, so it doesn't have an id, so there is nothing to assign to polymorphic object. So validation will always fail since it's called before save.
In other words: In your case when you call build_user, it returns User.new NOT User.create . Therefore authenticatable doesn't have a authenticatable_id assigned.
You have several options:
Save associated user first.
OR
Move validation in to after_save callback ( Possible but very annoying and bad)
OR
Change your app structure - maybe avoid polymorphic association and switch to has_many through? Hard for me to judge since I don't know internals and business requirements. But it seems to me this is not a good candidate for polymorphic association. Will you have more models than just User that will be authenticatable?
IMHO the best candidates for polymorphic associations are things like Phones, Addresses, etc. Address can belong to User, Customer, Company, Organization, Area51 etc, be Home, Shipping or Billing category i.e. It can MORPH to accommodate multiple uses, so it's a good object to extract. But Authenticatable seems to me a bit contrived and adds complexity where there is no need for it. I don't see any other object needing to be authenticable.
If you could present your Authenticatable model and your reasoning and maybe migrations (?) I could advise you more. Right now I'm just pulling this out of thin air :-) But it seems like a good candidate for refactoring.

You can just move validation to before_save callback and it will work fine:
class User < ActiveRecord::Base
# Associations
belongs_to :authenticatable, polymorphic: true
# Validations
before_save :check_authenticatable
def check_authenticatable
unless authenticatable
errors[:customizable] << "can't be blank"
false
end
end
end

In the create action, I had to assign it manually:
#physician = Physician.new(params[:physician])
#physician.user.authenticatable = #physician
My problem is a little different (has_many and with different validation), but I think this should work.

I was able to get this to work by overriding the nested attribute setter.
class Physician
has_one :user, as: :authenticatable
accepts_nested_attributes_for :user
def user_attributes=(attribute_set)
super(attribute_set.merge(authenticatable: self))
end
end
To DRY it up, I moved the polymorphic code to a concern:
module Authenticatable
extend ActiveSupport::Concern
included do
has_one :user, as: :authenticatable
accepts_nested_attributes_for :user
def user_attributes=(attribute_set)
super(attribute_set.merge(authenticatable: self))
end
end
end
class Physician
include Authenticatable
...
end
For has_many associations, the same can be accomplished with a map:
class Physician
has_many :users, as: :authenticatable
accepts_nested_attributes_for :users
def users_attributes=(attribute_sets)
super(
attribute_sets.map do |attribute_set|
attribute_set.merge(authenticatable: self)
end
)
end
end
class User
belongs_to :authenticatable, polymorphic: true
validates :authenticatable, presence: true
end
All that said, I think konung's last comment is correct - your example does not look like a good candidate for polymorphism.

I'm not sure if this solves your problem, but I use something like this when validating that a polymorphic parent exists.
Here is some code that I used in a video model with the parent as the polymorphic association. This went in video.rb.
validates_presence_of :parent_id, :unless => Proc.new { |p|
# if it's a new record and parent is nil and addressable_type is set
# then try to find the parent object in the ObjectSpace
# if the parent object exists, then we're valid;
# if not, let validates_presence_of do it's thing
# Based on http://www.rebeccamiller-webster.com/2011/09/validate-polymorphic/
if (new_record? && !parent && parent_type)
parent = nil
ObjectSpace.each_object(parent_type.constantize) do |o|
parent = o if o.videos.include?(p) unless parent
end
end
parent
}

Related

Rails validation messages for has_many through

I'm having trouble accessing validation messages for a related model when saving. The setup is that a "Record" can link to many other records via a "RecordRelation" which has a label stating what that relation is, e.g. that a record "refers_to" or "replaces" another:
class Record < ApplicationRecord
has_many :record_associations
has_many :linked_records, through: :record_associations
has_many :references, foreign_key: :linked_record_id, class_name: 'Record'
has_many :linking_records, through: :references, source: :record
...
end
class RecordAssociation < ApplicationRecord
belongs_to :record
belongs_to :linked_record, :class_name => 'Record'
validates :label, presence: true
...
end
Creating the record in the controller looks like this:
def create
# Record associations must be added separately due to the through model, and so are extracted first for separate
# processing once the record has been created.
associations = record_params.extract! :record_associations
#record = Record.new(record_params.except :record_associations)
#record.add_associations(associations)
if #record.save
render json: #record, status: :created
else
render json: #record.errors, status: :unprocessable_entity
end
end
And in the model:
def add_associations(associations)
return if associations.empty? or associations.nil?
associations[:record_associations].each do |assoc|
new_association = RecordAssociation.new(
record: self,
linked_record: Record.find(assoc[:linked_record_id]),
label: assoc[:label],
)
record_associations << new_association
end
end
The only problem with this is if the created association is somehow incorrect. Rather than seeing the actual reason, the error I get back is a validation for the Record, i.e.
{"record_associations":["is invalid"]}
Can anyone suggest a means that I might get record_association's validation back? This would be useful information for a user.
For your example, I would rather go with nested_attributes. Then you should easily get access to associated record errors. An additional benefit of using it is removing custom logic you have written for such behavior.
For more information check documentation - https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html

Saving association I.D upon creation?

If I have a user that has_many user_logins and a user_logins that belongs to user - When a user_login is created I'm using UserLogin.create(userlogin_params) and then my strong params permits the user_id column - but this alone is not saving the current users I.D to the column as it is coming out as nil.
How do I make it save the I.D?
User model:
has_many :user_logins
UserLogin model
belongs_to :user
accepts_nested_attributes_for :user
UserLoginController:
...
def create
#user_login = UserLogin.new(user_login_params)
...
end
...
def user_login_params
param.require(:user_login).permit(
:user_login_attribute1,
:user_login_attribute2,
user_attributes: [
:id,
:user_attribute1,
:user_attribute2
]
)
end
Tell me if it helps.
There are 2 issues here at hand.
First: How do you create an association with the parent record automagically there?
Second: How do you do this so your controller action isn't a giant hole waiting for a hacker to stick their nose in.
You need to start from the parent, then build the child, not start with the child and build the parent.
Consider the following:
class User < ActiveRecord::Base
has_many :logins, class_name: "UserLogin"
end
class UserLogin < ActiveRecord::Base
belongs_to :user
end
class UserLoginsController < ApplicationController
def create
if new_user_login(user_login_params).save
redirect_to :wherever
else
render :new
end
end
private
def new_user_login(attrs={})
current_user.logins.create(attrs)
end
def user_login_params
param.require(:user_login).permit(:attr_1, :attr_1)
end
Do not pass IDs into any secure params hash unless that ID is selectable by the user. If you allow an ID into secure params, a hacker can start moving records around to other objects and destroy your database integrity.
If you would like pairing help on this problem live and in person, you can check out my codementor profile at https://codementor.io/rubycasts/#reviews

In Rails, how can I create group of users as another association, such as "members"?

I am trying to create a special relationship between two existing models, User and Dwelling. A Dwelling has only one owner (Dwelling belongs_to :user, User has_one :dwelling) at the time of creation. But other Users can be added to this Dwelling as Roomies (there is no model created for this now, Roomie is a conceptual relationship).
I don't think I need a separate model but rather a special relationship with the existing models, but I could be wrong. I think the reference needs to be made with user_id from the Users table. I'm not really sure where to start this. Thank you for any and all help!
For example:
Dwelling1
user_id: 1
roomies: [1, 2, 3, 4]
Where 1, 2, 3, 4 are user_ids.
Updated Models
Dwelling Model
# dwelling.rb
class Dwelling < ActiveRecord::Base
attr_accessible :street_address, :city, :state, :zip, :nickname
belongs_to :owner, :class_name => "User", :foreign_key => "owner_id"
has_many :roomies, :class_name => "User"
validates :street_address, presence: true
validates :city, presence: true
validates :state, presence: true
validates :zip, presence: true
end
User Model
# user.rb
class User < ActiveRecord::Base
attr_accessible :email, :first_name, :last_name, :password, :password_confirmation, :zip
has_secure_password
before_save { |user| user.email = email.downcase }
before_save :create_remember_token
belongs_to :dwelling
has_many :properties, :class_name => "Dwelling", :foreign_key => "owner_id"
validates :first_name, presence: true, length: { maximum: 50 }
...
Updated Dwelling Create Action
#dwellings_controller.rb
...
def create
#dwelling = current_user.properties.build(params[:dwelling])
if #dwelling.save
current_user.dwelling = #dwelling
if current_user.save
flash[:success] = "Woohoo! Your dwelling has been created. Welcome home!"
redirect_to current_user
else
render 'new'
end
end
end
...
My answer assumes you only want a user to be a roomie at one dwelling. If you want a user to be a roomie at more than one dwelling, I think #ari's answer is good, although I might opt for has_and_belongs_to_many instead of has_many :through.
Now for my answer:
I would set it up so that a dwelling belongs_to an owner and has_many roomies (including possibly the owner, but not necessarily).
You can use the User model both for owners and roomies. You don't need any additional tables or models, you just need to setup the proper relationships by using the :class_name and :foreign_key options.
In your Dwelling model:
# dwelling.rb
belongs_to :owner, :class_name => "User", :foreign_key => "owner_id"
has_many :roomies, :class_name => "User"
In your User model:
# user.rb
belongs_to :dwelling # This is where the user lives
has_many :properties, :class_name => "Dwelling", :foreign_key => "owner_id" # This is the dwellings the user owns
In your dwellings table you need an owner_id column to store the user_id of the owner
In your users table you need a dwelling_id to store the dwelling_id of the dwelling where the user lives.
To answer your question in the comments regarding the controller:
If you want to setup current_user as the owner of the new dwelling, do this:
#dwelling = current_user.properties.build(params[:dwelling])
....
If you want to setup the current_user as the owner AND a roomie of the new dwelling, do this:
#dwelling = current_user.properties.build(params[:dwelling]
if #dwelling.save
current_user.dwelling = #dwelling
if current_user.save
# flash and redirect go here
else
# It's not clear why this wouldn't save, but you'll to determine
# What to do in such a case.
end
else
...
end
The trickiest part of above is handling the case that the dwelling is valid and saves, but for some unrelated reason the current_user can't be saved. Depending on your application, you may want the dwelling to save anyway, even if you can't assign the current_user as a roomie. Or, you might want the dwelling not to be saved --- if so, you'd need to use a model transaction, which is bit beyond the scope of this question.
Your controller code didn't work because saving the Dwelling doesn't actually update the current_user record to store the dwelling_id. Your code would be equivalent to the following:
#dwelling = Dwelling.new(params[:dwelling])
current_user.dwelling = #dwelling
if #dwelling.save
...
Note that current_user is never saved, so the current_user.dwelling = #dwelling line is useless.
This might seem counter-intuitive, but the bottom line is that build_dwelling isn't actually setting up things in memory as you might expect. You'd achieve more intuitive results if you saved the model you're building from rather than the model you're building:
#dwelling = current_user.build_dwelling(params[:dwelling])
if current_user.save # This will save the dwelling (if it is valid)
However, this (by default) won't save the dwelling if it has validation errors unless you turn :autosave on for the association, which is also a bit beyond the scope of this question. I really wouldn't recommend this approach.
Update:
Here is a more detailed code snippet:**
# dwellings_controller.rb
def create
#dwelling = current_user.properties.build(params[:dwelling])
if #dwelling.save
# The current user is now the owner, but we also want to try to assign
# his as a roomie:
current_user.dwelling = #dwelling
if current_user.save
flash[:notice] = "You have successfully created a dwelling"
else
# For some reason, current_user couldn't be assigned as a roomie at the
# dwelling. This could be for several reasons such as validations on the
# user model that prevent the current_user from being saved.
flash[:notice] = "You have successfully created a dwelling, but we could not assign you to it as a roomie"
end
redirect_to current_user
else
# Dwelling could not be saved, so re-display the creation form:
render :new
end
end
When a dwelling saves successfully, the current user will be the owner (owner_id in the database). However, if the current_user doesn't save, you'll need to decide how your application should respond to that. In the example above, I allow the dwelling to be saved (i.e. I don't rollback its creation), but I inform the user that he couldn't be assigned as a roomie. When this happens, it's most likely other code in your application causing the problem. You could examine the errors of current_user to see why. Or, you could use current_user.save! instead of current_user.save temporarily to troubleshoot.
Another way to do all of this is with an after_create callback in the Dwelling model. In many ways that would be a cleaner and simpler way to do it. However, catching the case when the current_user can't be saved could be even uglier than the method above, depending on how you want to handle it.
I believe the bottom line is that the current_user.save code is causing some problems. You'll need to diagnose why, and then determine what your application should do in that case. There are several ways to handle this, including at least the following
Put everything in a transaction block, and use current_use.save! instead of current_user.save so that an exception is raised and neither the user or dwelling is saved.
Save the dwelling, but inform the user that he isn't a roomie (As above)
Instead of saving the current_user, use update_column (which avoids callbacks, validations, etc.).
I believe the current problems you're experiencing are essentially unrelated to the original question. If you need further assistance, it might be best to break it off as a separate question.
You could do this by storing Roomie ids as a column in Dwelling
Make a migration:
class AddRoomiesToDwelling < ActiveRecord::Migration
def self.up
add_column :dwelling, :roomies, :text
end
def self.down
remove_column :dwelling, :roomies
end
end
In your Dwelling model:
class Dwelling < ActiveRecord::Base
serialize :roomies
end
You can then set the roomie ids with:
roomie_ids = [1, 2, 3, 4]
#dwelling.roomies = {:ids => roomie_ids}
#dwelling.save!
Taken from the Saving arrays, hashes, and other non-mappable objects in text columns section of this
You have two possible options.
Depending on your plan, it might be clearer for the dwelling to have_one owner instead of the owner having one dwelling. Then the dwelling would also be able to have users. You can add a column to User called dwelling_id and then you could do dwelling has_many users.
Another option would be to use the "has_many through" association. This means you would need to create a new model that would keep track of this association, say "Relationship.rb", which would belong to both User and Dwelling (and have columns for both for them). Then you would be able to write code like this:
//in Dwelling.rb
has_many :roomies, through: :relationships, source: :user
//in User.rb
has_many :dwellings, through: :relationships
This would let users also join more than one dwelling.

Saving belongs_to data when creating new record

class Party < ActiveRecord::Base
belongs_to :hostess, class_name: 'Person', foreign_key: 'hostess_id'
validates_presence_of :hostess
end
class Person < ActiveRecord::Base
has_many :parties, foreign_key: :hostess_id
end
When creating a new Party, the view lets the user select an existing Hostess, or enter a new one. (This is done with jQuery autocomplete to look up existing records.) If an existing record is chosen, params[:party][:hostess_id] will have the correct value. Otherwise, params[:party][:hostess_id] is 0 and params[:party][:hostess] has the data to create a new Hostess (e.g., params[:party][:hostess][:first_name], etc.)
In the Parties controller:
def create
if params[:party][:hostess_id] == 0
# create new hostess record
if #hostess = Person.create!(params[:party][:hostess])
params[:party][:hostess_id] = #hostess.id
end
end
#party = Party.new(params[:party])
if #party.save
redirect_to #party, :notice => "Successfully created party."
else
#hostess = #party.build_hostess(params[:party][:hostess])
render :action => 'new'
end
end
This is working fine when I pass in an existing Hostess, but it's not working when trying to create the new Hostess (fails to create the new Hostess/Person and thus fails on creating the new Party). Any suggestions?
Given the models you provided, you can have this setup in a cleaner way using a few rails tools like inverse_of, accepts_nested_attributes_for, attr_accessor, and callbacks.
# Model
class Party < ActiveRecord::Base
belongs_to :hostess, class_name: 'Person', foreign_key: 'hostess_id', inverse_of: :parties
validates_presence_of :hostess
# Use f.fields_for :hostess in your form
accepts_nested_attributes_for :hostess
attr_accessor :hostess_id
before_validation :set_selected_hostess
private
def set_selected_hostess
if hostess_id && hostess_id != '0'
self.hostess = Hostess.find(hostess_id)
end
end
end
# Controller
def create
#party = Party.new(params[:party])
if #party.save
redirect_to #party, :notice => "Successfully created party."
else
render :action => 'new'
end
end
We're doing quite a few things here.
First of all, we're using inverse_of in the belongs_to association, which allows you to validate presence of the parent model.
Second, we're using accepts_nested_attributes_for which allows you to pass params[:party][:hostess] into the party model and let it build the hostess for you.
Third, we're setting up an attr_accessor for :hostess_id, which cleans up controller logic quite a bit, allowing the model to decide what to do whether it receives hostess object or the hostess_id value.
Fourth, we're making sure to override hostess with an existing hostess in case we got a proper hostess_id value. We do this by assigning hostess in the before_validation callback.
I didn't actually check if this code works, but hopefully it reveals enough information to solve your problem and exposes more helpful tools lurking in rails.

Using accepts_nested_attributes_for + mass assignment protection in Rails

Say you have this structure:
class House < ActiveRecord::Base
has_many :rooms
accepts_nested_attributes_for :rooms
attr_accessible :rooms_attributes
end
class Room < ActiveRecord::Base
has_one :tv
accepts_nested_attributes_for :tv
attr_accessible :tv_attributes
end
class Tv
belongs_to :user
attr_accessible :manufacturer
validates_presence_of :user
end
Notice that Tv's user is not accessible on purpose. So you have a tripple-nested form that allows you to enter house, rooms, and tvs on one page.
Here's the controller's create method:
def create
#house = House.new(params[:house])
if #house.save
# ... standard stuff
else
# ... standard stuff
end
end
Question: How in the world would you populate user_id for each tv (it should come from current_user.id)? What's the good practice?
Here's the catch22 I see in this.
Populate user_ids directly into params hash (they're pretty deeply nested)
Save will fail because user_ids are not mass-assignable
Populate user for every tv after #save is finished
Save will fail because user_id must be present
Even if we bypass the above, tvs will be without ids for a moment of time - sucks
Any decent way to do this?
Anything wrong with this?
def create
#house = House.new(params[:house])
#house.rooms.map {|room| room.tv }.each {|tv| tv.user = current_user }
if #house.save
# ... standard stuff
else
# ... standard stuff
end
end
I haven't tried this out, but it seems like the objects should be built and accessible at this point, even if not saved.

Resources