How do I refactor this controller method? - ruby-on-rails

I have a Project model which has many groups. A Group has many users through a Membership model. A project must have at least one group. When creating a new project, I must create its default group, and make the current user a member.
I don't want to use ActiveRecord callbacks, but I would be interested in seeing how a solution using them would work. I'm using accepts_nested_attributes_for to create the project and the default group.
def create
#project = Project.new(project_params)
if #project.save
#project.groups.first.members << current_user
redirect_to #project
else
render 'new'
end
end
There are three problems with this action:
No transaction. The user may not get added to the project's default group.
This line #project.groups.first.members << current_user should not be in a controller.
It uses accepts_nested_attributes_for. I prefer not to use it, but I will continue to if I have to.

project.rb
def add_new_user(user_id)
self.groups.first.members << User.find_by_id(user_id)
self.groups.first.members.count == 1
end
controller.rb
def create
#project = Project.new(project_params)
if #project.save && #project.add_new_user(current_user.id)
redirect_to #project
else
render 'new'
end
end

Related

Rails Relationship (has_many/belongs_to) Completed

So it's been quite a bit of time since I played with relationships and I want to make sure I've done it right.
In my model for Client I have:
class Client < ApplicationRecord
has_many :projects, dependent: :destroy
end
In my model for Projects I have:
class Project < ApplicationRecord
belongs_to :client
end
So I know that's set. Then to grab projects I put in my projects controller:
def create
#client = Client.find(params[:client_id])
#project = #client.project.new(project_params)
flash[:notice] = "Project created successfully" if #client.project << #project
respond with #project, location: admin_project_path
end
Would I need to put something in my show that does the same?
Anything else I'm missing for relationships?
I would think this:
def create
#client = Client.find(params[:client_id])
#project = #client.project.new(project_params)
flash[:notice] = "Project created successfully" if #client.project << #project
respond with #project, location: admin_project_path
end
Would look more like:
def create
#client = Client.find(params[:client_id])
#project = #client.projects.new(project_params)
if #project.save
# do success stuff
else
# do failure stuff
end
end
Note that
#project = #client.project.new(project_params)
should be:
#project = #client.projects.new(project_params)
As Yechiel K says, no need to do:
#client.project << #project
Since:
#project = #client.projects.new(project_params)
will automatically set client_id on the new #project. BTW, if you want to add a project to the client manually, then it's:
#client.projects << #project
(Note projects vs. project.)
In the off chance that there is not a client with params[:client_id], then #client = Client.find(params[:client_id]) will throw an error. You should probably include a rescue block. Alternatively, I prefer:
def create
if #client = Client.find_by(id: params[:client_id])
#project = #client.projects.new(project_params)
if #project.save
# do success stuff
else
# do failure stuff
end
else
# do something when client not found
end
end
Also, respond with isn't a thing. respond_with is a thing. (I believe it's been moved to a separate gem, responders.) It's unclear from your code if you're needing different responses, say, for html and js. If not, then I think it would be more like:
def create
if #client = Client.find_by(id: params[:client_id])
#project = #client.projects.new(project_params)
if #project.save
flash[:notice] = "Project created successfully"
redirect_to [#client, #project]
else
# do failure stuff
end
else
# do something when client not found
end
end
This assumes that your routes look something like:
Rails.application.routes.draw do
resources :clients do
resources :projects
end
end
In which case rails will resolve [#client, #project] to the correct route/path.
As DaveMongoose mentions, you could move #client = Client.find_by(id: params[:client_id]) into a before_action. This is quite common. Here's one discussion of why not to do that. Personally, I used to use before_action like this, but don't any more. As an alternative, you could do:
class ProjectsController < ApplicationController
...
def create
if client
#project = client.projects.new(project_params)
if #project.save
flash[:notice] = "Project created successfully"
redirect_to [client, #project]
else
# do failure stuff
end
else
# do something when client not found
end
end
private
def client
#client ||= Client.find_by(id: params[:client_id])
end
end
Taking this a bit further, you could do:
class ProjectsController < ApplicationController
...
def create
if client
if new_project.save
flash[:notice] = "Project created successfully"
redirect_to [client, new_project]
else
# do failure stuff
end
else
# do something when client not found
end
end
private
def client
#client ||= Client.find_by(id: params[:client_id])
end
def new_project
#new_project ||= client.projects.new(project_params)
end
end
I would replace this line:
flash[:notice] = "Project created successfully" if #client.project << #project
with:
flash[:notice] = "Project created successfully" if #project.save
No need to manually add #project to #client.projects, it gets added automatically when you create it using #client.projects.new, the only thing you missed was that creating something using .new doesn't persist it in the DB, that gets accomplished by calling #project.save.
For your show action, I'm not sure if you mean the client's show page or the project's, but in either case, you would retrieve it using params[:id] (unless you were using some nested routing).

Save both or neither model in controller

I want to save two models in one controller action, or save neither, and return with the validation errors.
Is there a better way than this?
def update
#job = Job.find(params[:id])
#location = #job.location
#job.assign_attributes(job_params)
#location.assign_attributes(location_params)
#job.save unless #job.valid? # gets validation errors
#location.save unless #location.valid? # gets validation errors
if #job.valid? && #location.valid?
#job.save
#location.save
flash[:success] = "Changes saved."
redirect_to edit_job_path(#job)
else
render 'edit'
end
end
New version:
def update
#job = Job.find(params[:id])
#location = #job.location
begin
Job.transaction do
#job.assign_attributes(job_params)
#job.save!(job_params)
#location.assign_attributes(location_params)
#location.save!(location_params)
end
flash[:success] = "Changes saved."
redirect_to edit_job_path(#job)
rescue ActiveRecord::RecordInvalid => invalid
render 'edit'
end
end
Have a look at Active Record Nested Attributes.
Using Nested attributes, you can save associated record attributes through parent.If parent record fails, associated records won't be saved.!
the first thing you'd want to do is delete these two lines
#job.save unless #job.valid? # gets validation errors
#location.save unless #location.valid? # gets validation errors
and only keep the #save in the if statement. because if one of them is valid, but the other isn't, you'll still save the valid one to the db.
To answer your second question, is there a better way to do this? At first blush, it looks like a job for #accepts_nested_attributes_for. However, accepts_nested_attributes_for is somewhat notorious for being difficult to get working (really it just takes a fare amount of tinkering) and what you're currently doing should get you where you're trying to go, so it's up to you.
You can use validates_associated rails helper:
class Model < ActiveRecord::Base
has_one :location
validates_associated :location
end
Then:
if #job.save
#blah
else
#blah
end
Is enough without having to mess with ActiveRecord#Nested_attributes. It's fastest, but less cleaner. Your choice.
Reference:
http://apidock.com/rails/ActiveRecord/Validations/ClassMethods/validates_associated

Rails Redirect_path after save based on view

I currently have two views (new.html.erb and retirement_accounts_new.html.erb) in the Accounts both using the same create and update methods.
Here's how they're defined in the controller:
# GET /accounts/new
def new
#account = current_user.accounts.build
end
# GET /retirement/accounts/new
def retirement_accounts_new
#account = current_user.accounts.build
end
And here's the same create method they share:
def create
#account = current_user.accounts.build(account_params)
if #account.save
redirect_to accounts_path, notice: 'Account was successfully created.'
else
render action: 'new'
end
end
Is there a way to make that redirect_to accounts_path conditional based on which view is rendering the form?
I would like retirement_accounts_new on save/update to redirect_to retirement_accounts
It sounds like this might be a design issue. Are Accounts and RetirementAccounts significantly different? Will they share much of the same logic, but not all? If so, I think I would avoid using conditional logic in the controller and solve it using inheritance.
The idea here is that retirement_accounts would be considered a new resource in your routes file:
resources :retirement_accounts
Then you manually create a new controller for it (skip the rails generate... command). Save this file as app/controllers/retirement_accounts_controller.rb:
class RetirementAccountsController < AccountsController
end
Notice how it inherits from AccountsController instead of ApplicationController. Even in this empty state, RetirementAccountsController shares all of the logic of AccountsController, including the new and create methods, plus all of the view files to which they refer. To make the necessary modifications for the retirement accounts, you simply need to override the appropriate actions and views.
You can delete your retirement_accounts_new action, since it is identical to the new action. Move the view for retirement_accounts_new to app/views/retirement_accounts/new.html.erb, so that template will be rendered when new is called on the RetirementAccountsController.
As for the conditional create method, you can make a private method on both controllers that will determine where the post-create redirect should point:
class AccountsController < ApplicationController
# ...
def create
#account = current_user.accounts.build(account_params)
if #account.save
redirect_to post_create_redirect_path, notice: 'Account was successfully created.'
else
render action: 'new'
end
end
private
def post_create_redirect_path
accounts_path
end
end
class RetirementAccountsController < AccountsController
private
def post_create_redirect_path
retirement_accounts_path
end
end
If RetirementAccount < Account as a single table inheritance model then the thing you are asking would happen by default,
plan B would be to use explicit url_for in the redirect such as:
redirect_to url_for(controller: params[:controller], action: :show, id: #account.id), notice: 'Account was successfully created.'
Looking at the api doc this should work too:
redirect_to :action => "show", :id => #account.id,notice: 'Account was successfully created.'
Check out http://apidock.com/rails/ActionController/Base/redirect_to - there's probably an answer for you there somewhere :)
PS I have assumed that the the retirement account and account actions are in different controllers. If they're not in different controllers and not different model classes then you can put a hidden tag in the new form - but this is bad&ugly
Best solution is probably STI model and 2 separate resources for the 2 classes and everything will work out of the box. If this isn't an option, at least separate the controllers and make things clean that way, it's much better to reuse views then to reuse controllers

Build an object after user registers in rails

So i'm having this issue trying to figure out how to use the build method in rails to create an object once a user completely registers and still have that object connected to the users id. I'm using devise for authentication and the model that needs to be created is called "app".
This is the create method for "app".
def create
#app = App.new(app_params)
#app.id = current_user.id
respond_to do |format|
if #app.save
format.html { redirect_to #app, notice: 'Application successfully created.'}
else
format.html { render action: 'new' }
end
end
end
Im getting this error:
Couldn't find App with id=1
from my multi step form controller:
def show
#user = User.find(current_user)
case step
when :school, :grades, :extra_activity, :paragraph, :submit
#app = App.find(current_user)
end
render_wizard
end
You need an after_create callback in the User model. It makes no sense to mess with the AppController because no forms have been filled up for the app and you have no app_params.
class User < ActiveRecord::Base
after_create :build_initial_app
protected
def build_initial_app
self.create_app
end
end
You can read more about this at the Rails Guides page for ActiveRecord Callbacks.
The problem line in your code is here:
#app.id = current_user.id
Setting an ActiveRecord object's id is a no-no. Think of the id attribute like you would a pointer in C. The system creates it for you, and you can use it to refer to a unique model object.
What you probably want is something along the lines of:
#app.user_id = current_user.id
Or, even better:
#app.user = current_user
To do that, you need to set up an association between your App model and your User model. There's a good tutorial on that here.

ROR: Updating two models from one form

I have two models profiles and users on my form. After a user is created he can then move to editing his profile. The views work well. But when I click Save to update the editted profile. It doesn't update, but the flash notice displays that profile has been updated. What might be wrong? I'm not sure what went wrong. Below is the code.
class ProfilesController < ApplicationController
def new
##user.profile = Profile.new
#user = User.find(current_user.id)
#identity = #user.profile || #user.build_profile()
#identity.save
end
def update
#user = current_user
#identity = #user.profile
if #identity.update_attributes(params[:identity])
flash[:notice] = 'Profile was successfully updated.'
redirect_to(new_profile_path())
else
render :action => "new"
end
end
end
class UsersController < ApplicationController
def show
#user = current_user
#user = User.find(params[:id])
#identity = #user.profile || #user.build_profile()
#identity.save
end
......
end
Thanks for your assistance.
There are potentially a few things wrong here. But the best solution to this problem would be to simplify and use the built in rails features for editing associations.
What I suggest doing is using nested attributes, Ryan Daigle has a great article on them.
I'm not sure why you're calling save in a new action and not in a create, that doesn't feel right. Also check that the name of the model in the form you're submitting is identity and not user or profile.
Can a user exist without a profile?

Resources