Rails build_association with child class in has_one best practice advice - ruby-on-rails-3

I have a User class defined like this
class User
end
And I have sublassed this to create an Owner class and created a has_one relationship with another Company class
class Owner < User
has_one :company
end
class Company
belongs_to :owner
end
In my Users controller when creating a new User I want to accomplish the following:
Create a new User
Create a new Company
Associate the User with the Company (as the Owner i.e. company.owner_id)
I can accomplish this with the following code (simplified for brevity)
def create
#user = User.new(params[:user])
#company = Company.new(params[:company])
if #user.save
#company.owner_id = #user.id
#company.save
...
Now, this just feels ugly to me, but I can't seem to get the whole build_asociation process to work as expected (yes, fields are there in both dev and test).
What should I be doing here?

If you need to create Owner and Company at the same time, I suggest you use accepts_nested_attributes_for in Owner. Here's the code:
class Owner < User
has_one :company
accepts_nested_attributes_for :company
end
Then in your controller you can do something like this:
def create
#user = User.new(params[:user]) # should it be User or Owner?
#user.company_attributes = params[:company] # assume two separate forms for User and Company
# if you use fields_for, however, company attributes are nested under params[:user] automatically.
if #user.save
# do your job here
end
end
For complete reference, please take a look at Active Record Nested Attributes and the view helper fields_for

Related

What is better way to create Client record after User is registered (corresponding client record)

Basically i have user registering himself to the app , by using devise gem.
Instead of having standard sign up form like (email, password) i have an extra 2 fields (name, contact_nr) in total used (name, contact_nr, email, password, password_confirm) fields, :name and :contact_nr attributes exists in 'clients' table only.
Table name: clients
id :integer not null, primary key,
name :string(255)
surname :string(255)
contact_nr :string(255)
user_id :integer
class Client < ActiveRecord::Base
belongs_to :user
end
class User < ActiveRecord::Base
has_one :client, dependent: :destroy
after_create :update_user_client
def name
return unless client
client.name
end
def contact_nr
return unless client
client.contact_nr
end
def update_user_client
Client.last.update_attributes(user: self)
end
end
In my RegistrationsController I have only one method
class RegistrationsController < Devise::RegistrationsController
before_action :create_client
private
def create_client
return if params[:user].blank?
Client
.new(name: params[:user][:name],
contact_nr: params[:user][:contact_nr])
.save(validate: false)
end
end
What bothers me is that kind of writing code, it feels like code smell.
How would you implement it?
Thanks guys looking forward to your answers..
First advice I can give is do not separate client and user into two tables if you don't have valid reasons and/or requirements for now. That would make things much easier.
If you have valid reasons, here are my advices on how to improve your existing state of this code piece:
Rails and all mature gems around it rely on 'convention over configuration', so you should check if there are conventional ways to achieve same results.
In your RegistrationsController instead of doing params[:user].blank? check, you should use Devise's way of doing this, provided with inherited methods as devise_parameter_sanitizer.permit within a before_action callback.
Instead of creating client in your controller, move that to model logic, and in your user model put accepts_nested_attributes_for :client.
Since both of your models(client and user) share same name, put a before_save callback, so that you can pass user's name attribute to client itself.
after_create callback is very risky, since it is not an atomic save (no guarantee that client will be updated after user record is updated.). So don't use it. accepts_nested_attributes_for will handle both create and update calls.
If name attribute for user would be fetched through client only, there is no need to keep name within user.
If you want to access client's contact_nr and name attributes directly from user model, then use delegate method inside it.
Putting all together, I would refactor that code piece as this:
class User < ActiveRecord::Base
has_one :client, dependent: :destroy
accept_nested_attributes_for :client
delegate :name, to: :client
delegate :contact_nr, to: :client
# optional. if you want to keep name attr in both models.
before_save :sync_names
private
def sync_names
self.client.name = name if client.present?
end
end
class RegistrationsController < Devise::RegistrationsController
before_action :configure_permitted_parameters
protected
def configure_permitted_parameters
added_attrs = [:name, :email, :password, :password_confirmation, client_attributes: [:contact_nr]]
devise_parameter_sanitizer.permit :sign_up, keys: added_attrs
devise_parameter_sanitizer.permit :account_update, keys: added_attrs
end
end
Don't forget to update your signup and account update forms to accept nested attributes for client resource.
As far as you are validating the data with JS and filtering with params.require(:client).permit, the code looks fine. Try to create many differente scenarios in your Rspec. The test usually reveals unexpected flaws.

Rails - Show action for nested resource is deleting the record?

I am building a simple web app with a typical User model & Profile model. The user model has_one profile & the profile model belongs_to user. All is going pretty well as I am basically following Michael Hartl's tutorial (except he uses has_many for microposts).
So here's the deal. I create a user, then create a profile. I know both records exist b/c I verify their presence in my sqlite3 db browser. However, as soon as I try to render the show view by visiting -> localhost/3000/profiles/1, I get the error shown below. But the even weirder part is that now when I check my db, the profile record is gone. Please help!
Note: I have a feeling it has something to do with dependent destroy (b/c removing it will eliminate this issue), but I have no idea why. Furthermore, I think I want dependent destroy anyway. I don't want any stray profile records if there are no corresponding users, right?
Routes
resources :users
resources :profiles
Models
class User < ActiveRecord::Base
has_one :profile, dependent: :destroy
class Profile < ActiveRecord::Base
belongs_to :user
ProfilesController
def new
#profile = current_user.build_profile
end
def create
#profile = current_user.build_profile(params[:profile])
if #profile.save
flash[:success] = "Profile created!"
redirect_to root_path
else
render 'new'
end
end
def show
#profile = Profile.find(params[:id])
end
views/profiles/show.html.erb
<p>Display Name: <%= #profile.display_name %></p>
This is the error message I get when I try to visit -> localhost/3000/profiles/1
ActiveRecord::RecordNotFound in ProfilesController#show
Couldn't find Profile without an ID

has_many :through and build

I have three models, Account, User and Contact:
class User < ActiveRecord::Base
has_one :account
has_many :contacts, :through => :account
end
class Account < ActiveRecord::Base
belongs_to :owner, :class_name => 'User'
has_many :contacts
end
class Contact < ActiveRecord::Base
belongs_to :account
end
I'm trying to scope build a new contact through the user record, like this in my contacts controller.
def create
#contact = current_user.contacts.build(params[:contact])
respond_to do |format|
if #contact.save
...
else
...
end
end
end
When I do this, I don't receive any errors, the contact record is saved to the database however the account_id column is not set on the contact, and it is not added to the collection so calling #current_user.contacts returns an empty collection.
Any suggestions?
Using build makes a new instance of Contact in memory, but you would need to manually set the account_id on the record (e.g. #contact.account_id = current_user.account.id), or perhaps set it in a hidden field in the new form used to display the contact for creation such that it is picked up in the params array passed to the build method.
You might also want to consider whether accepts_nested_attributes_for may be helpful in this case. Another option may be to use delegate, although in both cases, your use may be sort of the opposite of what these are intended for (typically defined on the "parent").
Update:
In your case, the build method is added to both the User instance and to the Account (maybe "Owner") instance, because you have both a many-to-many relationship between User and Contact, as well as a one-to-many relationship between Account and Contact. So to get the account_id I think you would need to call Account's build, like
#contact = current_user.accounts.contacts.build(params[:contact])
Does this work?

Can I set a nested attribute in the controller vs. the view

I have a simple nested form and I am setting a nested attribute with a hidden field:
<%= role_form.hidden_field :company_id, :value => session[:company_id] %>
The idea here is I am associating this nested model (a role based permissions system) to another model Company via the company_id set by the current session variable. My issue is that the user could send a request and create / update the role with an arbitrary company_id and gain access to another company's account.
Can I force the nested model attributes to be this session value or perhaps a validation?
I was thinking for create:
#user = User.new(params[:user])
#user.roles.first.company_id = session[:company_id]
and for update I could do sort of the same thing.
As for the validation I tried:
accepts_nested_attributes_for :roles, :limit => 1, :allow_destroy => true , :reject_if => proc { |attributes| attributes['company_id'] != session[:company_id] }
but it looks like you can't access the session info in the model.
Any one have an idea if I can do this either of these ways?
Rather than storing the company_id in the session, you should instead add a randomly generated token column to the company, and get the id by doing Company.find_by_token(session[:token]). If you look at how the current_user method in this Railscast on authentication, it's the same idea.
Edit:
Sorry, I misunderstood your question. You should not have a hidden company_id field at all in your view. You should be setting it manually in your create method:
#user = User.new(params[:user])
#user.company_id = session[:company_id]
And you can protect the company_id from ever being set from the user changing an input name by having company_id protected against mass assignment in the model:
attr_protected :company_id
See the rails guide on mass assignment protection for more information. Note: a more common solution is something along these lines:
class ApplicationController < ActionController::Base
protect_from_forgery
def current_company
#current_company ||= Company.find_by_auth_token!(cookies[:auth_token]) if cookies[:auth_token]
end
end
class User < ApplicationController
def create
#user = current_company.users.build(params[:user])
end
end
UPDATE 2:
So you're creating a user and a role, and want to do separate validation on them, this should do what you want.
role_params = params[:user].delete :role # Change to the appropriate symbol for your form
#user = User.new(params[:user])
role = #user.roles.build(role_params)
role.company_id = session[:company_id]
if(#user.save and role.user_id = #user.id and role.save) # Might want to just check for valid instead of trying to save
...

Simple has_many :through association

Pretty simple setup. I want to make sure my understanding of the ORM is correct.
class User < ActiveRecord::Base
has_many :memberships
has_many :groups, through => memberships
end
class Group < ActiveRecord::Base
has_many :memberships
has_many :users, through => memberships
end
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :group
end
Now when a user creates a group I want the membership record in the link table to get populated. It should be an atomic(transaction).
class GroupsController < ApplicationController
def create
#group = current_user.groups.build(params[:group])
if #group.save
flash[:notice] = "Group has been created."
redirect_to #group
else
flash[:alert] = "Group has not been created."
render :action => "new"
end
end
end
This doesn't work. The group gets saved but no membership record created in the link table. However using a create vs build works. Is that how it's supposed to work?
What's the best approach here?
This behaviour is by design. As you mentioned, you can either do #group = current_user.groups.create(params[:group]).
Or you can add an additional statement to create a record in the join model's table as :
#group = current_user.groups.build(params[:group])
if #group.save
#group.memberships.create(:user_id => current_user)
# redirect and notify
Well, the reason being simply building #group and saving it does not add an additional record in the join table.
Infact, in this case, #group = current_user.groups.build(params[:group]) is somewhat similar to #group = Group.new(params[:group]). The difference being, in the former case, current_user.groups will contain #group (you can try that in Groups#create before redirect) but doing current_user.reload followed by current_user.groups will yield [].
The best way to do this is somewhat similar to your approach. Have a simple create action as :
def create
#group = Group.new(params[:group])
# if else for save and redirect
However, for this to work the params hash submitted to Groups#create should include user_ids as :
"group"=>{"name"=>"new group", "user_ids"=>["1", "2", "3"]}, "commit"=>"Create Group"
May be that was the reason why #bruno077 was asking you to paste your view's code, so as to get an idea on user_ids params being passed.
So, if the new group form contains fields to select multiple users, then its simple create action as shown right above (because of the user_ids params). But if have a new group form with no options to select users, then you are better off using the first option (one using create).