Trouble creating new nested resource with has_one relationship - ruby-on-rails

I am trying to create a nested resource with a has_one relationship. When the form is submitted all the parameters look right, and it routes correctly to the create action. However, something is making it error out inside the create action.
Error: undefined method 'new' for nil:NilClass
Relevant resources:
#models/employer.rb
class Employer < ActiveRecord::Base
has_one :employment_training_opportunity, dependent: :destroy
end
#models/employment_training_opportunity.rb
class EmploymentTrainingOpportunity < ActiveRecord::Base
belongs_to :employer
end
And the controller:
#controllers/employment_training_opportunities_controller.rb
class EmploymentTrainingOpportunitiesController < ApplicationController
def create
#employer = Employer.find(params[:employer_id])
# error highlights line below
#employment_training_opportunity = #employer.employment_training_opportunity.new(employment_training_opportunity_params)
if #employment_training_opportunity.save
redirect_to #employer, flash: {success: "Employment Opportunities was successfully created"}
else
render :new
end
end
end
Some more clues I got:
When I put in a binding.pry I noticed that it is properly finding the employer resource, so #employer is returning an active record object.
When I call #employer.employment_training_opportunity it returns nil. This makes sense because this employer resource does not yet have an associated employment_training_opportunity resource.
But when I call #employer.employment_training_opportunity.new, that is when it errors out with NoMethodError: undefined method 'new' for nil:NilClass. I just don't get it because this works just fine with has_many associations for nested resources.

#employer.employment_training_opportunity is nil, so you can't call new on it. You could do this:
#employer.employment_training_opportunity = EmploymentTrainingOpportunity.new(employment_training_opportunity_params)
Which will create the new record and set up the relationship. But this is cleaner, IMO:
#employer.build_employment_training_opportunity(employment_training_opportunity_params)

Related

Creating model and nested model (1:n) at once with ActiveRecord

My Rails5 application has an organization model and a user model (1:n relationship). The workflow of creating an organization should include the creation of the organization's first user as well. I thought this would be able with ActiveRecord through nested models, however the create action fails with the error message "Users organization must exist".
class Organization < ApplicationRecord
has_many :users, dependent: :destroy
accepts_nested_attributes_for :users
end
class User < ApplicationRecord
belongs_to :organization
end
class OrganizationsController < ApplicationController
def new
#organization = Organization.new
#organization.users.build
end
def create
#organization = Organization.new(organization_params)
if #organization.save
redirect_to #organization
else
render 'new'
end
end
def organization_params
params.require(:organization).permit(:name, users_attributes: [:name, :email, :password, :password_confirmation])
end
end
In the view I use the <%= f.fields_for :users do |user_form| %> helper.
Is this a bug on my side, or isn't this supported by ActiveRecord at all? Couldn't find anything about it in the rails guides. After all, this should be (theoretically) possible: First do the INSERT for the organization, then the INSERT of the user (the order matters, to know the id of the organization for the foreign key of the user).
As described in https://github.com/rails/rails/issues/18233, Rails5 requires integrity checks. Because I didn't like a wishy-washy solution like disabling the integrity checks, I followed DHH's advice from the issue linked above:
I like aggregation through regular Ruby objects. For example, we have a Signup model that's just a Ruby object orchestrating the build process. So I'd give that a go!
I wrote a ruby class called Signup which encapsulates the organization and user model and offers a save/create interface like an ActiveRecord model would. Furthermore, by including ActiveModel::Model, useful stuff comes in to the class for free (attribute hash constructor etc., see http://guides.rubyonrails.org/active_model_basics.html#model).
# The Signup model encapsulates an organization and a user model.
# It's used in the signup process and helps persisting a new organization
# and a referenced user (the owner of the organization).
class Signup
include ActiveModel::Model
attr_accessor :organization_name, :user_name, :user_email, :user_password, :user_password_confirmation
# A save method that acts like ActiveRecord's save method.
def save
#organization = build_organization
return false unless #organization.save
#user = build_user
#user.save
end
# Checks validity of the model.
def valid?
#organization = build_organization
#user = build_user
#organization.valid? and #user.valid?
end
# A create method that acts like ActiveRecord's create method.
# This builds the object from an attributes hash and saves it.
def self.create(attributes = {})
signup = Signup.new(attributes)
signup.save
end
private
# Build an organization object from the attributes.
def build_organization
#organization = Organization.new(name: #organization_name)
end
# Build a user object from the attributes. For integritiy reasons,
# a organization object must already exist.
def build_user
#user = User.new(name: #user_name, email: #user_email, password: #user_password, password_confirmation: #user_password_confirmation, organization: #organization)
end
end
Special thanks to #engineersmnky for pointing me to the corresponding github issue.
You're looking for "Association Callbacks". Once you send those params to your organization model you have access to them inside that model. If everytime an organization is created there will be a new user assigned to it you can just do the following in your Organization Model:
has_many :users, dependent: :destroy, after_add: :create_orgs_first_user
attr_accessor: :username #create virtual atts for all the user params and then assign them as if they were organizational attributes in the controller. This means changing your `organization_params` method to not nest user attributes inside the array `users_attributes`
def create_orgs_first_user
User.create(name: self.username, organization_id: self.id, etc.) # You can probably do self.users.create(params here) but I didn't try it that way.
end
The "Users organization must exist" error should not occur. ActiveRecord is "smart," in that it should execute two INSERTs. First, it will save the model on the has_many side, so that it has an id, and then it will save the model on the belongs_to side, populating the foreign key value. The problem is actually caused by a bug in accepts_nested_attributes_for in Rails 5 versions prior to 5.1.1. See https://github.com/rails/rails/issues/25198 and Trouble with accepts_nested_attributes_for in Rails 5.0.0.beta3, -api option.
The solution is to use the inverse_of: option or, better yet, upgrade to Rails 5.1.1.
You can prove that this is true by removing the accepts_nested_attributes_for in your Organization model and, in the Rails console, creating a new Organization model and a new User model, associating them (eg myorg.users << myuser) and trying a save (eg myorg.save). You'll find that it will work as expected.

Passing params[:id] to create method in Rails?

I'm trying to write a create method that collects the ID of the profile the user is currently viewing, along with some other information that is irrelevant to this question. However, because the create method POSTs rather than GETs (as I understand it), the value of params[:id] doesn't exist so it's always null. My code is as follows:
class PostsController < ApplicationController
def new
#Post = Post.new
end
def create
#Post = Post.new(post_params)
#Post.user_id = current_user.id
#Post.target_id = params[:id] #this
if #Post.save
redirect_to :back, notice: "You added a post!"
end
end
private
def post_params
params.require(:post).permit(:body)
end
end
Is there a way to get the value of params[:id] from elsewhere, perhaps from my Users controller in the show method where it actually exists?
Keep in mind that I was successfully able to create a hidden field in the Posts form, but I didn't like the fact that users were able to edit the value using Developer Tools, allowing them to change what profile the post would go to.
If there is a direct relation between the Target and the Post model, you should express this in the controller and model structure: link
This expresses your intention and it provides all the rails automations like routing, url helpers, form helpers, a.s.o.
In your concrete example, my guess is the Target would have many Posts:
class Target < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :target
end
Which would lead to the following route structure:
resources :targets do
resources :posts
end
To create a new post for the current target you would post to:
targets/:target_id/posts
And the target id would be accessed via params[:target_id]

Rails - Associated model's show action & its corresponding view?

I'm building a simple app that has a typical User model & a Profile model. User has_one Profile and Profile belongs_to User. All seems to be working fairly well as I am basically following the Michael Hartl tutorial. However, when I try to render the view of something from the profiles table (show action), I get an error (no id) AND the profile record I created gets deleted!
Questions:
In my ProfilesController, am I defining my show action properly for the simple view I am trying to render?
Why does simply visiting the url localhost/3000/profiles/1 delete the profile record? I think it has something to do with dependent destroy (b/c removing that will stop this behavior), but I think I want to keep dependent destroy, correct?
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 dude!"
redirect_to root_path
else
render 'new'
end
end
def show
#profile = Profile.find(params[:user_id])
end
View (profiles/show.html.erb)
<p>Display Name: <%= #profile.display_name %></p>
Check your rake routes. You will see that for your Profile#show, you have URL structure like: /profiles/show/:id.
Thus, params, must be expecting the :id instead of :user_id.
If by /profiles/show/3, you wish to show profile 3, then:
def show
#profile = Profile.find(params[:id])
end

Devise with Associated Record validation

My background
I am/was a PHP developper. Have been for 15 years. Ruby is new to me (My new challenge)!
Current Setup
I am using Devise with a User model.
Rails: 3.2.1
Devise: 2.1.2
Use Case
When the user registers (going thru Devise controller), I want to create the User record but also a Foo record automatically. I created an after_create which handles the creation of the Foo record.
Class User < ActiveRecord::Base
after_create :make_foo
def make_foo
Foo.create(
:name => name,
:user_id => id
)
end
end
Class Foo < ActiveRecord::Base
belongs_to :user
end
Symptoms
I had a problem where when the Foo record was not being created (validation for example), then the User record was still created (I did not want that). I added a Raise Exception in after_create which rolls back the User creation.
However, I would prefer some nice error handling rather than Exception being throwed. Right now I get a 500 Error page with that Exception.
I would prefer that the form can be shown again with the reason(s) of the failure.
Class User < ActiveRecord::Base
after_create :make_foo
def make_foo
foo = Foo.create(
:name => name,
:user_id => id
)
if !foo.valid?
raise Exception.new('Foo creation failed.')
end
end
end
Plea for help
Any suggestions?
Instead of raising an exception you can redirect back to same page with setting flash message in
if !foo.valid?
block like this
flash[:error] = 'error msg'
and redirect using
session[:return_to] = request.referer
redirect_to session[:return_to]
I ended up overriding the Devise Resitrations Controller and putting a begin...rescue...end inside the create method.
# routes.rb
devise_for :users, :controllers => { :registrations => "my_devise/registrations" }
# app/controllers/my_devise/registrations_controller.rb
class MyDevise::RegistrationsController < Devise::RegistrationsController
def create
begin
super
rescue Exception
self.resource.errors[:base] << "My error message here"
clean_up_passwords resource
respond_with resource
end
end
end
You might want to look at Rails3: Devise User has_one relationship to see if better modelling can make the problem easier.
The way you are modelling user.rb now, is indeed such that a User may exist without Foo (which must belong to a User however), so it just calls :make_foo as an after_create callback with no other guarantees whatsoever.

respond_with and namespaces

Tricky issue...
Assume the following models:
class Foo::Bar < ActiveRecord::Base
class Foo::Nut < ActiveRecord::Base
The following route:
namespace :admin do
resources :bars do
resources :nuts do
In the create action for nuts at /admin/bars/100/nuts, I create the model based on post data and would like to respond with:
#respond_with(:admin, #bar, #nut) (where bar and nut had been set up in the action)
I'm presented with this lovely error:
NoMethodError (undefined method `admin_foo_bar_foo_nut_url')
I'd like rails to look for admin_bar_nut_url and not admin_foo_bar_foo_nut_url.
Any ideas if I can get around this? Clearly something up with having my models define in modules...
Would prefer to have to abandon the model namespacing but can if I must.
Thanks so much!
I do not see why you need to go away with name space rather than using respond_to instead of respond_with
respond_to do |format|
format.html { redirect_to(admin_bar_nut_url(#bar, #nut)) }
end
To remove foo from your routes, override model_name in your models like:
class Foo::Bar < ApplicationRecord
def self.model_name
ActiveModel::Name.new(self, Foo)
end
end
class Foo::Nut < ApplicationRecord
def self.model_name
ActiveModel::Name.new(self, Foo)
end
end
This will result in Foo::Nut.model_name.route_key being "nuts" instead of "foo_nuts"
And now respond_with should work as desired.

Resources