Why there is no double-render when using before_action? - ruby-on-rails

I am wondering why there is no double-render when there is a redirect_to or render in before_action. Consider this example:
class SomeController < ApplicationController
before_action :callback
def new
callback2
render 'new'
end
def callback
render 'new'
end
def callback2
render 'new'
end
end
I see that before_action will be useless if it can't redirect but how it is made? If I comment the before_action it will throw exception.
How is before_action implemented to not cause double-render?

See the Rails Guide on controllers :
If a "before" filter renders or redirects, the action will not run. If there are additional filters scheduled to run after that filter, they are also cancelled.

Related

DoubleRenderError but only one render

I have this
class ScriptsController < ApplicationController
before_action :authenticate_user!
load_and_authorize_resource
def run
#script.update(script_params)
Delayed::Job.enqueue ScriptRunnerJob.new(#script)
render head :ok
end
end
I get a DoubleRenderError in run.
Any idea ?
render and head are both methods that perform a render; you only want to call one of them.
# This is all you need
head :ok

before_action hook in case of Ajax request

Some of my controllers have before_action hooks in order to make sure that the model instance belong to the right user, or that a user is authenticated
before_action :authenticate_user!
before_action :check_owner
private
def check_owner
Unless current_user.id == Myobject.find(params[:id]).user.id
redirect_to root_path
end
Now I am wondering if I do an Ajax request (remote: true argument to a form) would the before_action hooks pass in case no User is authenticated for example ? At least I guess the redirections would fail..
If the hooks do the job, then I am happy. But maybe there is a better way to do this or get the redirections work.
The before_action would still halt the request in the case of remote: true, however as you mentioned, the redirect as you have it above will only work for requests with HTML format. The best way to handle the redirect for a JS request is by adding your own method in ApplicationController to handle all desired request formats:
class ApplicationController < ActionController::Base
...
def all_formats_redirect_to(path)
respond_to do |format|
format.html { redirect_to path }
format.js { render js: "window.location = #{path.to_json}" }
# you can add how you want to handle redirect for other formats if you use them
end
end
...
end
And then instead of using redirect_to in your before_actions (or even in actions that handle multiple formats), you can use all_formats_redirect_to:
before_action :authenticate_user!
before_action :check_owner
private
def check_owner
unless current_user.id == Myobject.find(params[:id).user_id
all_formats_redirect_to(root_path)
false
end
end

Devise AJAX user authentication doesn't redirect

I have a link in my application.html.erb file
<%= link_to 'Sprzedaż', sell_path, remote: true %>
In the controller I authenticate user with before_action :authenticate_user!. Below is my authenticate_user! method.
protected
def authenticate_user!
if user_signed_in?
super
else
respond_to do |format|
format.js { render nothing: true, status: :unauthorized }
format.html { redirect_to root_path, :alert => 'Aby przejść do tej strony, musisz być zalogowany.' }
end
end
end
Basically it works correctly if the user isn't authorized. If the user has enabled Javascript it shows nice notification, and if the user hasn't enabled Javascript it shows alert and redirect to root_path which is good. The problem is that when the user is signed in and click the link nothing happens. It should redirect to the sell_path.
This is my ItemsController
class ItemsController < ApplicationController
before_action :authenticate_user!
def sell
#user = current_user
#items = JSON.parse(HTTParty.get("http://steamcommunity.com/profiles/#{#user.uid}/inventory/json/730/2?l=polish").body)
end
end
This is my ApplicationController
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
before_action :steam_informations
def steam_informations
#steam = session[:steam]
end
protected
def authenticate_user!
if user_signed_in?
super
else
respond_to do |format|
format.js { render nothing: true, status: :unauthorized }
format.html { redirect_to root_path, :alert => 'Aby przejść do tej strony, musisz być zalogowany.' }
end
end
end
end
You are trying to override helpers which defined in runtime. This is not how you must to do it.
In your case I recommend you to define for example authenticate! method like this:
def authenticate!
return true if user_signed_in?
respond_to do |format|
format.js { render nothing: true, status: :unauthorized }
format.html { redirect_to root_path, :alert => 'Aby przejść do tej strony, musisz być zalogowany.' }
end
end
This method will do nothing if user signed in and redirect to root page if user not signed in. Just define this method in ApplicationController and then use before_filter :authenticate! hook to execute it.
When before_filter \ before_action method returns false - rails won't execute your action. And if return true or just return - rails will execute action after hooks like authenticate! and render your views. To make it clear I'll show you some examples.
class FooController << ApplicationController
before_filter :dead_hook, only: :index
before_filter :nice_hook, only: :show
def index
# this action will be never executed because dead_hook method returns false.
end
def show
# this action will be executed right after nice_hook method because of 'return true' command in nice_hook method
end
def dead_hook
return false
end
def nice_hook
return true
end
end
Another way to do just like you trying to do - monkey-patch devise helper. You can do it like this:
module Devise
module Controllers
module Helpers
def authenticate_user!
# implement your logic here
end
end
end
end
Here you can check out whats going on in devise helpers:
Github Devise Helpers source code
Just for clarification: there is no difference between before_filter and before_action. Feel free to use any of them. before_action newer but before_filter not deprecated.

How to render json for all actions from the after_action filter in ApplicationController?

Is it possible to create an after_filter method in the Rails ApplicationController that runs on every action and renders to JSON? I'm scaffolding out an API, and I'd like to render output to JSON for every action in the controller.
clients_controller.rb
def index
#response = Client.all
end
application_controller.rb
...
after_action :render_json
def render_json
render json: #response
end
The after_action is never executed, and the code aborts with:
Template is missing. Missing template clients/index, ...
If the render json: #response is moved into the controller action, it works correctly.
Is there a filter that will allow me to DRY up the controllers and move the render calls to the base controller?
You can't render after_action/ after_filter. The callback after_action is for doing stuff after rendering. So rendering in after_action is too late.
But your exception is just because you miss the JSON template. I recommend using RABL (which offers a lot of flexibility to your JSON responses and there is also a Railscast about it). Then your controller could look like:
class ClientsController < ApplicationController
def index
#clients = Client.all
end
def show
#client = Client.find params[:id]
end
end
And don't forget to create your rabl templates.
e.g. clients/index.rabl:
collection #clients, :object_root => false
attributes :id
node(:fancy_client_name) { |attribute| attribute.client_method_generating_a_fancy_name }
But in the case you still want to be more declarative you can take advantage of the ActionController::MimeResponds.respond_to like:
class ClientsController < ApplicationController
respond_to :json, :html
def index
#clients = Client.all
respond_with(#clients)
end
def show
#client = Client.find params[:id]
respond_with(#client)
end
end
Btw. beware if you put code in an after_action, this will delay the whole request.

DRYing up respond_to code in Rails

I have several actions (lets call them action_a, action_b, etc.)
In each action I want to check if the user is logged in and if not to have a respond_to block as follows
format.js {
if current_user.nil?
render partial: 'some_partial', handler: [:erb], formats: [:js]
end
}
For one action this is fine, but not for many actions, as there will be many duplications of this code which will do exactly the same thing, it is not very pretty or maintainable
Is there a way to put this somewhere so I can reuse this code and not rewrite it in every needed action?
Use before_filter (rails <4.0) or before_action (rails 4.0)
class YourController < ApplicationController
before_filter :check_user
...
your actions
...
private
def check_user
redirect_to sign_in_path if current_user.nil?
end
end
or if you want specific actions and respond use around_action (filter):
class YourController < ApplicationController
around_action :check_user
...
your actions
def show
#variable = Variable.last
end
...
private
def check_user
yield #(you normal action without js respond)
format.js {
if current_user.nil?
render partial: 'some_partial', handler: [:erb], formats: [:js]
end
}
end
end
Read up on Responders. These are meant to help with that.
Your specific problem is what filters are generally used for. See this Section on Filters in the Action Controller Guide
More generally, this is just ruby code, so you can refactor everything out into methods:
def do_stuff_with(partial_name, format)
if current_user.nil?
render partial: partial_name, handler: [:erb], formats: [format]
end
end
format_js { do_stuff_with('some_partial', :js) }

Resources