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
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
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.
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.
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.