Rails: how can I optimize this action - ruby-on-rails

The action bellow creates a new comment.
A user has many statuses
A status has many comments
How can optimize this action so that head 401 and return is not repeated many times.
def create
#user = User.where(id: params[:user_id]).first
if #user
if current_user.friend_with?(#user) or current_user == #user
#status = #user.statuses.where(id: params[:status_id]).first
if #status
#comment = #status.comments.build(params[:comment])
#comment.owner = current_user
if #comment.valid?
#comment.save
current_user.create_activity(:comment_status, #comment, #user)
else
head 401 and return
end
else
head 401 and return
end
else
head 401 and return
end
else
head 401 and return
end
end
Thank you.

When do you want to return 401?
when a user has not been found
when a user is not a current user or is not a friend of that user
when a status has not been found
when new comment has not been successfully created
Instead of using so many conditionals, you can use methods that raise exceptions. When you do so, you can rescue from that exceptions with the desired behavior (rendering 401).
So my suggestions for listed conditions are:
use find! instead of where and then first.
raise something, preferably custom exception (NotAFriendError)
same as 1., use find!
use create!, it's an equivalent to new and then save! which will raise ActiveRecord::RecordInvalid exception if it fails on validation.
Here's the result:
def create
begin
#user = User.find!(params[:user_id])
raise unless current_user.friend_with?(#user) || current_user == #user
#status = #user.statuses.find!(params[:status_id])
#comment = #status.comments.
create!(params[:comment].merge(:owner => current_user))
rescue ActiveRecord::RecordNotFound, ActiveRecord::RecordInvalid
head 401
end
# everything went well, no exceptions were raised
current_user.create_activity(:comment_status, #comment, #user)
end

You have a lot of excessive checking and branching in your code, so it can be simplified to this:
def create
success = false
#user = User.find(params[:user_id])
current_user_is_friend = current_user.friend_with?(#user) || current_user == #user
if #user && current_user_is_friend && #status = #user.statuses.find(params[:status_id])
#comment = #status.comments.build(params[:comment])
#comment.owner = current_user
if #comment.save
current_user.create_activity(:comment_status, #comment, #user)
success = true
end
end
render(status: 401, content: '') unless success
end
A few things I did:
Combine a lot of the if conditions, since there was no need for them to be separate.
Change where(id: ...).first to find(...) since they're the same. Note that, if the find fails, it will give a 404. This may make more sense, though (I think it does)
Don't call #comment.valid? right before #comment.save, since save returns false if the object wasn't valid.
Use || instead of or for boolean logic (they're not the same).
Use render(status: ..., content: '') instead of head ... and return.
Use a boolean variable to track the success of the method.
I would advise that you try and pull some of this logic out into models. For example, User#friend_with should probably just return true if it's passed the same User.

def create
#user = User.where(id: params[:user_id]).first
if #user
if current_user.friend_with?(#user) or current_user == #user
#status = #user.statuses.where(id: params[:status_id]).first
if #status
#comment = #status.comments.build(params[:comment])
#comment.owner = current_user
if #comment.valid?
#comment.save
current_user.create_activity(:comment_status, #comment, #user)
everythingOK = true
end
end
end
end
head 401 and return unless everythingOK
end

Related

Returning a 404 if a user doesn't exist

Why would this not generate a 404 response?
def index
if find_user
#documents = #client.documents
respond_to do |format|
format.html
format.atom { render layout: false }
end
else
flash[:error] = "#{params[:client_code]} is not a client."
render 'error', status: '404'
end
end
def find_user
#client = User.find_by_client_code(params[:client_code]) if valid_user?
end
def valid_user?
User.all.each.map(&:client_code).include?(params[:client_code])
end
Like, if the code is incorrect it should return a 404, right? And not an exception? Can't quite get it to work.
EDIT: sorry, here's the error:
An ActionView::MissingTemplate occurred in share#index:
* Parameters : {"controller"=>"share", "action"=>"index", "client_code"=>"ampNDHEDD", "format"=>"atom"}
If you don't use the valid_user? or the find_user methods elsewhere, they can be removed and you could do the following
def index
#client = User.find_by_client_code(params[:client_code]) # returns nil or a record
if #client
#documents = #client.documents
respond_to do |format|
format.html
format.atom { render layout: false }
end
else
flash[:error] = "#{params[:client_code]} is not a client."
render status: 404
end
end
However, your previous comment states you're getting a template error which indicates that you may not have an index.atom template available to render.
do this
def index
if find_user
#documents = #client.documents
respond_to do |format|
format.html
format.atom { render layout: false }
end
else
flash[:error] = "#{params[:client_code]} is not a client."
raise ActionController::RoutingError.new('Not Found')
end
end
def find_user
#client = User.find_by_client_code(params[:client_code]) if valid_user?
end
def valid_user?
User.where(client_code: params[:client_code]).present?
// User.all.each.map(&:client_code).include?(params[:client_code]) // this is terrible (Read my comment )
end
First, your valid_user? method is a really bad idea - it loads the entire user database just to see if the code is present... which is the same result as what User.find_by_client_code does, but without loading every record! I'd just nuc the method and the if clause. If there is no matching record, it should return nil, which should take the else path and render the error.
As for why it's not rendering the error... I'm not sure if the atom format has anything to do with it, but when code doesn't branch the way I expect, I always put a Rails.logger.debug ... before the branch I have an issue with, and/or put a bad method in the branch it's supposed to take. That helps narrow it down. :D

Render failing to render correct template in rescue_from ActiveRecord::Rollback method

I'm building the checkout page for an e-commerce site, and I have a fairly long transaction that creates a new User model and a new Order model. I wrapped the creation of these models in a transaction so that if validation for one fails, the other isn't hanging around in the database. Here's the trimmed-down code in my OrdersController:
rescue_from ActiveRecord::Rollback, with: :render_new
def render_new
render action: 'new'
end
ActiveRecord::Base.transaction do
#user = User.new params[:user]
unless #user.save
raise ActiveRecord::Rollback
end
//More stuff
...
#order = Order.new params[:order]
...
unless #order.save
raise ActiveRecord::Rollback
end
end
The error I'm seeing is this:
Missing template orders/create, application/create with {:locale=>[:en], :formats=>[:html], :handlers=>[:erb, :builder, :coffee]}
I'm confused as to why its trying to render the templates orders/create and application/create instead of rendering orders/new.
Is there a better way to force the transaction to fail so that the rollback will occur?
I think the intention is a bit clearer when wrapping the transaction in a begin/rescue block.
def create
begin
ActiveRecord::Base.transaction do
#user = User.new params[:user]
unless #user.save
raise ActiveRecord::Rollback
end
//More stuff
...
#order = Order.new params[:order]
...
unless #order.save
raise ActiveRecord::Rollback
end
end
rescue ActiveRecord::Rollback
render action: "new" and return
end
end
You need to return in the create method, otherwise it's execution will continue to the end of the method and Rails default render will occur (in this case it means attempting to find a create.___ template).
If you don't like the begin/rescue block you can just add an and return to the raise lines
raise ActiveRecord::Rollback and return
Above answer is correct but some modification is required for rendering action.
Do it like this:-
def create
is_project_saved = false
is_proposal_saved = false
ActiveRecord::Base.transaction do
is_project_saved = #project.save
is_proposal_saved = #proposal.save
if is_project_saved && is_proposal_saved
# Do nothing
else
raise ActiveRecord::Rollback
end
end
if is_project_saved && is_proposal_saved
# You can add more nested conditions as per you need.
flash[:notice] = "Proposal Created Successfully."
redirect_to project_show_path(:job_id => #project.job_id)
else
render :new
end
end
ActiveRecord::Rollback will not be caught by resque. So it needs to be done outside transaction block.
You can also use save_point using :requires_new => true in nested ActiveRecord::Base.transaction.
You need to raise the ActiveRecord::Rollback and manage the render/redirect as you desire. As #WasimKhan said, the ActiveRecord::Rollback will not be caught by rescue.
def create
#user = User.new params[:user]
ActiveRecord::Base.transaction do
if #user.save
#order = Order.new params[:order]
if #order.save
redirect_to :index
else
raise ActiveRecord::Rollback
end
else
render :new
end
end
render :new if #user.id.nil?
end

How to show errors for two objects and don't save unless both are valid

I have a form for two object User Board
Here is my controller:
def create
#board = Board.new(params[:board])
#user = User.new(params[:user])
respond_to do |format|
if (#user.save and #board.save)
format.js {redirect_to some_path}
else
format.js {render :action => "new" }
end
end
end
I don't want to save either one unless both are valid. And I want to show the error messages for both at one time on the form.
I have tried all types of combinations of '&&' '&' 'and' but they don't give me the result I want. They show the errors of one object while saving the other.
How can I do this properly?
&& doesn't work like && in Linux.
You have two alternatives. You can check whether the records are valid?, then perform the save.
def create
#board = Board.new(params[:board])
#user = User.new(params[:user])
respond_to do |format|
if #user.valid? && #board.valid?
#user.save!
#board.save!
format.js { redirect_to some_path }
else
# do something with #errors.
# You check the existence of errors with
# #user.errors.any?
# #board.errors.any?
# and you access the errors with
# #user.errors
# #board.errors
format.js { render :action => "new" }
end
end
end
or if your database supports transactions, use transactions.
def create
#board = Board.new(params[:board])
#user = User.new(params[:user])
respond_to do |format|
begin
transaction { #user.save! && #board.save! }
format.js { redirect_to some_path }
rescue ActiveRecord::RecordInvalid
format.js { redirect_to some_path }
end
end
end
Personally, I would check for valid?.
If you look in the source code of the save method of ActiveRecord you see:
def save(options={})
perform_validations(options) ? super : false
end
What you want to do, is running the perform_validations manually before calling save. For this, you can use the valid? method from ActiveResource. You can find the documentation here:
http://api.rubyonrails.org/classes/ActiveRecord/Validations.html#method-i-valid-3F

Getting user information using either user.login or user.id

So I have a url like the following
localhost/users/:id/posts
which gives the posts of that particular user. Now this id can be either his login (which is a string) or the id (user.id) which is technically an Integer but params[:id] is always a string. So how do I implement this an action.
#user = params[:id].is_a?(String) ? User.find_by_login(params[:id]) : User.find(params[:id])
The above code miserably fails since params[:id] is always a string. Any thoughts? Thanks.
When I've done this, I've actually had two separate controller actions-- show and show_by_login. I feel like it's less unpredictable that way, and I have more control.
Be sure to enforce uniqueness of your logins, index them, and if show_by_login can't find the record you have to raise ActiveRecord::RecordNotFound yourself.
def show
#user = User.find(params[:id])
respond_to do |format|
format.html
format.xml { render :xml => #user.to_xml }
end
end
def show_by_login
#user = User.find_by_login(params[:login])
unless #user
raise ActiveRecord::RecordNotFound
end
render :action => 'show'
end
You could use a regular expression:
#user = params[:id] =~ /^\d+$/ ? User.find(params[:id]) : User.find_by_login(params[:id])
So long as you don't allow any logins to consist purely of digits, you could write your own finder/named_scope.
class User < ActiveRecord::Base
named_scope :find_by_id_or_login, lambda {|id_or_login|
{ :conditions => ["id = ? OR login = ?", id_or_login, id_or_login] }
}
end
#user = User.find_by_id_or_login(params[:id])

Rails: Keeping user spoofing checks DRY

In a fit of unoriginality, I'm writing a blog application using Ruby on Rails. My PostsController contains some code that ensures that the logged in user can only edit or delete their own posts.
I tried factoring this code out into a private method with a single argument for the flash message to display, but when I did this and tested it by editing another author's post, I got an ActionController::DoubleRenderError - "Can only render or redirect once per action".
How can I keep these checks DRY? The obvious approach is to use a before filter but the destroy method needs to display a different flash.
Here's the relevant controller code:
before_filter :find_post_by_slug!, :only => [:edit, :show]
def edit
# FIXME Refactor this into a separate method
if #post.user != current_user
flash[:notice] = "You cannot edit another author’s posts."
redirect_to root_path and return
end
...
end
def update
#post = Post.find(params[:id])
# FIXME Refactor this into a separate method
if #post.user != current_user
flash[:notice] = "You cannot edit another author’s posts."
redirect_to root_path and return
end
...
end
def destroy
#post = Post.find_by_slug(params[:slug])
# FIXME Refactor this into a separate method
if #post.user != current_user
flash[:notice] = "You cannot delete another author’s posts."
redirect_to root_path and return
end
...
end
private
def find_post_by_slug!
slug = params[:slug]
#post = Post.find_by_slug(slug) if slug
raise ActiveRecord::RecordNotFound if #post.nil?
end
The before filter approach is still an ok option. You can gain access to which action was requested using the controller's action_name method.
before_filter :check_authorization
...
protected
def check_authorization
#post = Post.find_by_slug(params[:slug])
if #post.user != current_user
flash[:notice] = (action_name == "destroy") ?
"You cannot delete another author’s posts." :
"You cannot edit another author’s posts."
redirect_to root_path and return false
end
end
Sorry for that ternary operator in the middle there. :) Naturally you can do whatever logic you like.
You can also use a method if you like, and avoid the double render by explicitly returning if it fails. The key here is to return so that you don't double render.
def destroy
#post = Post.find_by_slug(params[:slug])
return unless authorized_to('delete')
...
end
protected
def authorized_to(mess_with)
if #post.user != current_user
flash[:notice] = "You cannot #{mess_with} another author’s posts."
redirect_to root_path and return false
end
return true
end
You could simplify it more (in my opinion) by splitting out the different parts of behavior (authorization, handling bad authorization) like this:
def destroy
#post = Post.find_by_slug(params[:slug])
punt("You cannot mess with another author's post") and return unless author_of(#post)
...
end
protected
def author_of(post)
post.user == current_user
end
def punt(message)
flash[:notice] = message
redirect_to root_path
end
Personally, I prefer to offload all of this routine work to a plugin. My personal favorite authorization plugin is Authorization. I've used it with great success for the last several years.
That would refactor your controller to use variations on:
permit "author of :post"
The simple answer is to change the message to something that fits both: "You cannot mess with another author's posts."
If you don't like the ugly* return in that last solution, you can use an around filter and conditionally yield only if the user is authorized.
around_filter :check_authorization, :only => [:destroy, :update]
private
def check_authorization
#post = Post.find_by_slug(params[:slug])
if #post.user == current_user
yield
else
flash[:notice] = case action_name
when "destroy"
"You cannot delete another author's posts."
when "update"
"You cannot edit another author's posts."
end
redirect_to root_path
end
end
*-- that's my preference, though code-wise it's perfectly valid. I just find that style-wise, it tends to not fit.
I also should add I haven't tested this and am not 100% certain it would work, though it should be easy enough to try.

Resources