Create Deeply Nested Rails Associations in One Form - ruby-on-rails

I have a user form on my welcome page. As the user form gets submitted, I want to create a website record that belongs to the user and also a page record that belongs to that website.
I'm trying to use fields_for but I'm not sure what I'm doing correctly or incorrectly.
# welcome.html.erb
<%= form_for #user do |f| %>
<div>
<%= f.text_field :name' %>
</div>
<div>
<%= f.email_field :email %>
</div>
<div>
<%= f.password_field :password %>
</div>
<div>
<%= f.fields_for :website do |website_fields| %>
<%= website_fields.text_field :name, value: 'Untitled website' %>
<% end %>
</div>
<div>
<%= f.fields_for :page do |page_fields| %>
<%= page_fields.text_field :name, value: 'Untitled page' %>
<%= page_fields.text_field :content, class: 'js-PageContentHiddenField', value: 'Page content' %>
<% end %>
</div>
<div>
<%= f.submit 'Create account' %>
</div>
<% end %>
The pages controller looks like this...
# pages_controller.rb
def welcome
#user = User.new
#website = #user.websites.new
#page = #user.websites.pages.new
end
My routes are as follows...
# routes.rb
resources :users do
resources :websites do
resources :pages
end
end
The models look like this...
# User model
has_many :websites, :dependent => :destroy
has_many :pages, through: :websites
accepts_nested_attributes_for :websites
accepts_nested_attributes_for :pages
# Website model
belongs_to :user
has_many :pages, :dependent => :destroy
# Page model
belongs_to :website
And finally my users controller...
# User controller
def create
#user = User.new(shared_params)
#website = #user.websites.new(website_params)
#page = #website.pages.new(page_params)
if #user.save
auto_login(#user)
#redirect_to user_website_page_path
else
# redirect to wherever
end
end
private
def shared_params
params.require(:user).permit(:email, :password, :name)
end
def website_params
params.require(:user).permit(:name, :user_id)
end
def page_params
params.require(:user).permit(:name, :website_id)
end
The problem I'm having now is that the user name is being saved as the page and website name etc. It's some problem with my params I think. Also, I'm not sure how to set the redirect to redirect to the page after it's been created.
I've been playing around with various configurations for weeks now and I can't crack this. I can't stress how little I know what I'm doing here, would really love some help!

Callbacks are methods that get called at certain moments of an object's life cycle. With callbacks it is possible to write code that will run whenever an Active Record object is created, saved, updated, deleted, validated, or loaded from the database.
So as you need to create a website and a page when the user is being created the you can use a after_create or before_create callback. after_create gets executed after the object is created and the before_create callback gets executed before the object is being created.
So in your User model what you can do is:
after_create :create_website_and_page
def create_website_and_page
website = websites.build(name: 'Untitled Website')
page = website.pages.build(name: 'Untitled Page', content: 'Page Content')
website.save # This will automatically save the associated page too
end
So as soon as your user is created the website and page will also be created.
To get the content for the page from the form you can do:
In User model:
attr_accessor :page_content
This will create a virtual attribute on the user object which will not exist in database. Now in your form:
<%= f.hidden_field :page_content, class: 'js-PageContentHiddenField' %>
So append the content from javascript in this field. Now in your controller permit this attribute too:
params.require(:user).permit(:email, :password, :name, :page_content)
And then finally in the method we wrote above create_website_and_page change the line to this:
page = website.pages.build(name: 'Untitled Page', content: page_content)
This should do the required.
If you still wish to use the nested_form for any reason then the mistake you are doing is the params. Just place a debugger on the top of your create action and check the params. So to permit the parameters of website and page you have to do:
def user_params
params.require(:user).permit(:email, :password, :name, website_attributes: [:name], page_attributes: [:name, :website_id, :content])
end
but you will never get the website_id in params as you are creating that also from the same form. And also the user_id will not be required to be permitted as it will automatically get associated in case of the nested forms.

Related

Create parent object in child create method

There are many related question on SO. I went through all of them but still struggle with my situation.
I have two models User and Booking
#model/user.rb
has_many :bookings
#modle/booking.rb
belongs_to :user
I want to create a booking and a user at the same time. If the user already exist, just add the new booking to the existing user.
My form for creating booking:
<%= simple_form_for :booking, url: bookings_path, :method => :post, do |f| %>
<%= f.fields_for :user, do |ff| %>
<%= ff.input_field :first_name %>
<%= ff.input_field :last_name %>
<%= ff.input_field :email %>
<%= ff.input_field :phone %>
<% end %>
...
<% end %>
And in the booking controller
#control/bookings_controller.rb
def create
#user = User.find_by(:email => booking_params[:user][:email])
if #user == nil
#user = User.new(booking_params[:user])
#user.username = User.autousername(#user)
#user.password = Devise.firendly_token(8)
else
#user.update(booking_params[:user])
end
#user.save
b_params = booking_params
b_params.delete("user")
#booking = Booking.new(b_params)
#booking.user_id = #user.id
#booking.save
...
end
def booking_params
params.require(:booking).permit(:status, :house_id, :check_in, :check_out, :adult_guest, :children_guest, :temp_profit,
:note, :check_in_note, :user => [:first_name, :last_name, :email, :phone])
end
I didn't use nested_attributes because I want to generate username and password myself rather than collect them using form.
What I got is ActiveRecord::AssociationTypeMismatch in BookingsController#create
User(#70121505047640) expected, got ActionController::Parameters(#70121556154400)
Most SO related problems want to create the child object under parent controller. In my case, it's reversed.
UPDATE
After trying every possible way, I found out that the CanCanCan load_and_authorize_resource seems to be the trouble maker. If I comment out it in my bookings_controller, everything works fine. Could someone tell me why?
Solution
Finally, I found out the reason. I'm using CanCanCan load_and_authorize_resource in my booking_controller. In order to create nested user instance under it, I have to load user resources. Just add load_and_authorize_resource :user will solve the problem!
Thanks for all the answers. Hope this will help people with the same problem.
This is probably happening when you try to do the #booking.save. Because you have a user param, it will try to use the user= to set it. In this case, the user= will apply a params object.
Try to delete the user param from the booking_params when you create the booking.
b_params.delete(:user)
or
b_params[:user] = #user

How to validate multiple models in a single transaction?

In my rails (4.1.6) app, I have a contact model that has_one :address, :email
I construct a contact and related address and email in a single form using fields_for:
views/contacts/new.html.erb
<%= form_for #contact, ... %>
...
<%= fields_for :address do |address_fields| %>
<%= address_fields.text_field :street, ... %>
<%= address_fields.text_field :city, ... %>
...
<% end %>
<%= fields_for :email do |email_fields| %>
<%= email_fields.text_field :display_name, ... %>
<%= email_fields.text_field :mail_id, ... %>
<% end %>
...
<% end %>
I want email to be required, while address is optional. In other words, if email is not provided, none of the 3 models should be created, but if only email is provided, the email and contact must be created.
One way that does work is to validate the params manually in the contacts_controller#create before constructing anything, and flash[:error] and return without saving if email is not specified, or save it if all is well:
contacts_controller.rb
def create
#contact = Contact.new
if(params_email_valid? params)
#contact.save!
#email = Email.create(...)
#email.save!
...
else
flash[:error] = 'Email must be specified to save a contact'
redirect_to :root
end
end
private:
def params_email_valid? params
!(params[:email][:display_name].blank? || params[:email][:mail_id].blank?)
end
Another way that may work is to drop down to SQL and validate everything through direct SQL calls in a transaction.
However, both of these are not 'the rails way', since validations belong in the models. So, I am trying to use some combination of validates_presence_of, validates_associated and custom validators to validate this scenario. The problem here is that model level validation of associated models requires either self to be already saved in the database, or the associated model to be already saved in the database. Is there a way to validate all these models in a single transaction?
Considering you have appropriate validations in the models:
class Contact <
has_many :addresses
has_many :emails
#add
accepts_nested_attributes_for :addresses, :emails #you can add some validations here to like reject_all if blank? see the docs
end
class Address <
belongs_to :contact
end
class Email <
belongs_to :contact
end
In your CompaniesController
def new
#contact = Contact.new
#contact.addresses.new
#contact.emails.new
end
def create
#contact = Contact.new(contact_params)
if #contact.save
#redirect add flash
else
#add flash
#render action: new
end
protected
def contact_params
#permit(#contact_fields, address_attributes: [#address_fields], email_attributes: [#email_fields])
end
And you would like to modify your form like this
<%= form_for #contact, ... do|f| %>
...
<%= f.fields_for :address do |address_fields| %>
<%= address_fields.text_field :street, ... %>
<%= address_fields.text_field :city, ... %>
...
<% end %>
<%= f.fields_for :email do |email_fields| %>
<%= email_fields.text_field :display_name, ... %>
<%= email_fields.text_field :mail_id, ... %>
<% end %>
...
<% end %>
So accepts_nested_attributes helps you validate the child as well as the parent and adds [child]_attributes getters and setters, So normally in your form what was contact[email][display_name] will become contact[email_attributes][display_name]

How to get Rails build and fields_for to create only a new record and not include existing?

I am using build, fields_for, and accepts_nested_attributes_for to create a new registration note on the same form as a new registration (has many registration notes). Great.
Problem: On the edit form for the existing registration, I want another new registration note to be created, but I don't want to see a field for each of the existing registration notes.
I have this
class Registration < ActiveRecord::Base
attr_accessible :foo, :bar, :registration_notes_attributes
has_many :registration_notes
accepts_nested_attributes_for :registration_notes
end
and this
class RegistrationsController < ApplicationController
def edit
#registration = Registration.find(params[:id])
#registration.registration_notes.build
end
end
and in the view I am doing this:
<%= form_for #registration do |r| %>
<%= r.text_field :foo %>
<%= r.text_field :bar %>
<%= r.fields_for :registration_notes do |n| %>
<%= n.text_area :content %>
<% end %>
<% end %>
and it is creating a blank text area for a new registration note (good) and each existing registration note for that registration (no thank you).
Is there a way to only create a new note for that registration and leave the existing ones alone?
EDIT: My previous answer (see below) was bugging me because it's not very nice (it still loops through all the other registration_notes needlessly). After reading the API a bit more, the best way to get the behaviour the OP wanted is to replace:
<%= r.fields_for :registration_notes do |n| %>
with:
<%= r.fields_for :registration_notes, #registration.registration_notes.build do |n| %>
fields_for optionally takes a second parameter which is the specific object to pass to the builder (see the API), which is built inline. It's probably actually better to create and pass the new note in the controller instead of in the form though (just to move the logic out of the view).
Original answer (I was so close):
Just to clarify, you want your edit form to include a new nested registration note (and ignore any other existing ones)? I haven't tested this, but you should be able to do so by replacing:
<%= r.fields_for :registration_notes do |n| %>
with:
<%= r.fields_for #registration.registration_notes.build do |n| %>
EDIT: Okay, from a quick test of my own that doesn't work, but instead you can do:
<%= r.fields_for :registration_notes do |n| %>
<%= n.text_area :content if n.object.id.nil? %>
<% end %>
This will only add the text area if the id of the registration note is nil (ie. it hasn't been saved yet).
Also, I actually tested this first and it does work ;)
If you want to create a new registration form on your edit action, you can just instantiate a new registration_note object. Right now, your form is for the existing registration object.
I believe this is what you want:
class RegistrationsController < ApplicationController
def edit
#new_registration_note = RegistrationNote.new
#registration = Registration.find(params[:id])
#registration.registration_notes.build
end
end
In your view, you should pass a hidden param that references the registration record id:
<%= form_for #new_registration_note do |r| %>
<%= r.hidden_field :registration_id, :value => #registration.id %>
<%= r.text_area :content %>
<% end %>
Now, you can create your new registration note that belongs to #registration. Make sure you have a column in your registration_notes table to point to the registration. You can read more about associations here: http://guides.rubyonrails.org/association_basics.html
Thank you so much for your help as I said in my post the only problem with the approach from "Zaid Crouch"(I don't know how to make a reference to a user hehe) is that if the form has error fields the form will be clear and boom after the page reloading you'll have nothing filled in your form and can you imagine if you form is like 20 or 30 fields that would be a terrible user experience of course
Here is my solution that works with validation models:
class Registration < ActiveRecord::Base
attr_accessible :foo, :bar, :registration_notes_attributes
has_many :registration_notes
has_one :new_registration, class_name: 'RegistrationNote'
accepts_nested_attributes_for :new_registration
end
class RegistrationsController < ApplicationController
def edit
#registration = Registration.find(params[:id])
#registration.build_new_registration
end
end
<%= form_for #registration do |r| %>
<%= r.text_field :foo %>
<%= r.text_field :bar %>
<%= r.fields_for :new_registration do |n| %>
<%= n.text_area :content %>
<% end %>
<% end %>
I'm using simple_form in my example if you want to see the same working with validations and transaction take a look at the complete post here:
http://elh.mx/ruby/using-simple_form-for-nested-attributes-models-in-a-has_many-relation-for-only-new-records/
As Heriberto Perez correctly pointed out the solution in the most upvoted answer will simply discard everything if there's a validation error on one of the fields.
My approach is similar to Heriberto's but nevertheless a bit different:
Model:
class Registration < ActiveRecord::Base
has_many :registration_notes
accepts_nested_attributes_for :registration_notes
# Because 0 is never 1 this association will never return any records.
# Above all this association don't return any existing persisted records.
has_many :new_registration_notes, -> { where('0 = 1') }
, class_name: 'RegistrationNote'
accepts_nested_attributes_for :new_registration_notes
end
Controller:
class RegistrationsController < ApplicationController
before_action :set_registration
def edit
#registration.new_registration_notes.build
end
private
def set_registration
#registration = Registration.find(params[:id])
end
def new_registration_params
params.require(:registration).permit(new_registrations_attributes: [:content])
end
end
View:
<%= form_for #registration do |r| %>
<%= r.text_field :foo %>
<%= r.text_field :bar %>
<%= r.fields_for :new_registration_notes do |n| %>
<%= n.text_area :content %>
<% end %>
<% end %>

Multi-model nested form, can't add users to current account

I've searched everywhere for a solution but haven't come up with any.
The part that works: My app allows customers to create an account using a nested form. The data collected creates records in four models - accounts, users, accounts_users (because a user can be associated with many accounts), and profile (to store the user's fname, lname, phone, etc).
That part that doesn't work: Once logged in, I want the users to be able to add more users to their account using the form below. I don't receive any errors upon submit but I am brought back to the same form with no additional records created. Any help would be awesome!
Here is the nested form...
<%= form_for #user, :validate => true do |f| %>
<fieldset>
<%= f.fields_for :profile do |p| %>
<div class="field">
<%= p.label :first_name %>
<%= p.text_field :first_name %>
</div>
<div class="field">
<%= p.label :last_name %>
<%= p.text_field :last_name %>
</div>
<div class="field">
<%= p.label :phone %>
<%= p.text_field :phone %>
</div>
<% end %>
<div class="field">
<%= f.label :email %>
<%= f.text_field :email %>
</div>
<div class="actions">
<%= f.submit 'Create New User', :class => "btn btn-large btn-success" %>
<%= cancel %>
</div>
</fieldset>
The ApplicationController scopes everything to the current_account like so:
def current_account
#current_account ||= Account.find_by_subdomain(request.subdomain) if request.subdomain
end
The UsersController
def new
#user = User.new
#user.build_profile()
#current_account.accounts_users.build() #Edit2: This line was removed
respond_to do |format|
format.html # new.html.erb
format.json { render json: #user }
end
def create
#user = User.new(params[:user])
#user.accounts_users.build(:account_id => current_account.id) #Edit2: This line was added
if #user.save
# Send Email and show 'success' message
flash[:success] = 'An email has been sent to the user'
else
# Render form again
render 'new'
end
end
Models look like this:
class Account < ActiveRecord::Base
attr_accessible :name, :subdomain, :users_attributes
has_many :accounts_users
has_many :users, :through => :accounts_users
accepts_nested_attributes_for :users
end
class User < ActiveRecord::Base
attr_accessible :email, :password, :password_confirmation, :profile_attributes
has_many :accounts_users
has_many :accounts, :through => :accounts_users
has_one :profile
accepts_nested_attributes_for :profile
end
class AccountsUser < ActiveRecord::Base
belongs_to :account
belongs_to :user
end
class Profile < ActiveRecord::Base
belongs_to :user
attr_accessible :first_name, :last_name, :phone
end
Edit2: It turns out that I had required a password + password_comfirmation validation in the User model which prevented me from adding another user without these fields. I commented out these validations plus removed the line: current_account.accounts_users.build() in the 'new' action and added the line: #user.accounts_users.build(:account_id => current_account.id) in the 'create' action.
"I want the users to be able to add more users to their account using the form below." I assume you mean profiles (since your nested form is on profiles)?
If that's the case, I think your UsersController's create action isn't associating the profiles with users by using new.
Try this...
def new
#user = User.build
#profile = #user.profiles.build #build adds the profile to user's associated collection of profiles, but new doesn't
...
end
def create
#user = User.build(params[:user])
if #user.save
....
end
end
If you want the user to be associated with account, then you need to put the new and create actions in the AccountsController and do something similar to nest association of the users and profiles records.
Btw, the reason that it went back to new is because you render new at the end of the create, in case that's also part of the question. Hope that helps!

Rails 3.2 - Why is simple_form is not generating any markup for my nested attributes?

I've spent about 5 straight hours at this and keep ending up back at square one...time to ask for time help!
I am using Rails 3.2, devise and simple_form, I am trying to build a form that will allow a user to register (email, password) & allow them to create a simple listing object - all on the one page. However none of my nested attributes for the user are appearing on the markup when the /listings/new page loads & I cannot figure out why.
Here is what I have:
Listing controller:
def new
#listing = Listing.new
respond_to do |format|
format.html # new.html.erb
format.json { render json: #listing }
end
end
Listing model:
class Listing < ActiveRecord::Base
has_one :address
belongs_to :category
belongs_to :user
accepts_nested_attributes_for :address
accepts_nested_attributes_for :user
end
User model:
class User < ActiveRecord::Base
has_many :listings
devise :database_authenticatable, :registerable, :validatable
attr_accessible :email, :password, :password_confirmation, :remember_me
end
New Listings Form:
<%= simple_form_for(#listing) do |f| %>
<%= f.label :listing_type %>
<%= f.collection_select :listing_type, [["Creative","Creative"]], :first, :last%>
<%= f.simple_fields_for :user do |u| %>
<%= u.label :email %>
<%= u.input_field :email %>
<%= u.label_for :password %>
<%= u.input_field :password %>
<%= u.label_for :password_confirmation %>
<%= u.input_field :password_confirmation %>
<% end %>
<% end %>
My head is melted looking at this, any help is much appreciated!
Railscasts' Nested Model Form would be a good tutorial for you.
Also, what it sounds like you'd want to do is call Users#new, not Listings#new. Usually you make a form for the thing (User) which has_many of something else (Listings). So you want to make a form for a new User, not a new listing. If you take this route, then in Users#new in your controller, do something like
#user = User.new
#user.listings.build
....
If you want to keep it how it is, you might be able to do
#listing.user.build
But I'm not sure if that'll work since you're doing it in the opposite direction as I described above.
You need a new User object.
Change
<%= f.simple_fields_for :user do |u| %>
to
<%= f.simple_fields_for :user, User.new do |u| %>
It should be work.

Resources