I've been drying some code, one of this refactors is as following:
I have 3 controllers ( ConstructionCompanies, RealEstateCompanies, People) all of which had the following pattern:
class ConstructionCompaniesController < ApplicationController
before_filter :correct_user, :only => [:edit, :update]
private
def correct_user
#company = ConstructionCompany.find(params[:id])
if(current_user.owner != #company.user)
redirect_to(root_path)
end
end
class RealEstateCompaniesController < ApplicationController
before_filter :correct_user, :only => [:edit, :update]
...
private
def correct_user
#company = RealEstateCompany.find(params[:id])
if(current_user.owner != #company.user)
redirect_to(root_path)
end
end
As you can see the correct_user is repeated in each controller.
So what I did inside I helper that is included for all of them I created a method:
def correct_user_for_seller_of_controller(controller)
#"User".classify will return the class User etc.
#seller = controller.controller_name.classify.constantize.find(params[:id])
redirect_to(root_path) unless (current_user == #seller.user)
end
Know inside each controller I have:
class ConstructionCompaniesController < ApplicationController
before_filter :only => [:edit, :update] do |controller| correct_user_for_seller_of_controller(controller) end
class RealEstateCompaniesController < ApplicationController
before_filter :only => [:edit, :update] do |controller| correct_user_for_seller_of_controller(controller) end
I like the fact that is DRY now, but the problem is that it seems a little to complex for me, hard to understand. Did I went too far ?
Add the correct_user method to the ApplicationController class.
class ApplicationController
def correct_user_for_seller_of_controller
#"User".classify will return the class User etc.
#seller = controller_name.classify.constantize.find(params[:id])
redirect_to(root_path) unless (current_user == #seller.user)
end
end
In your controllers use the new method as a filter method:
class RealEstateCompaniesController < ApplicationController
before_filter :correct_user_for_seller_of_controller, :only => [:edit, :update]
end
It's definitely good to clean that up. I think you may have made things slightly more complex than necessary, though, what with all the Proc whatnot.
If the filter is being run on an instance of the controller, then there's no need to pass the controller to itself. Just call controller_name within the method and you're good to go.
I'd personally DRY it a bit more and move the filter to a superclass (or AppplicationController).
The method itself could also definitely be simplified. Use some introspection for example.
Related
I have a controller named HomeController with index and show actions. I want to check if the user subscription has ended and show him a message and redirect to HomeController#index.
Currently i am doing it as below
class HomeController < ApplicationController
before_action :check_if_trial_expired, only: [:index]
before_action :redirect_if_trial_expired, only: [:show]
protected
def check_if_trial_expired
#trial_expired = current_user.trial_expired?
end
def redirect_if_trial_expired
redirect_to home_path if current_user.trial_expired?
end
end
Is there a better way to do this? I want to redirect the user to HomeController#index in case a condition satisfies.
Many Thanks in advance.
You'll at least need the index and show methods defined on the controller; make sure you have them in your routes. I don't think you need to use before_action for the index. Also, you can memoize trial_expired if it is an expensive operation.
class HomeController < ApplicationController
before_action :redirect_if_trial_expired, only: [:show]
def index; end
def show; end
private
def redirect_if_trial_expired
redirect_to home_path if current_user.trial_expired?
end
end
I have this in my application_controller
class ApplicationController < ActionController::Base
before_action :login_required, :only => 'users/login'
protect_from_forgery with: :exception
protected
def login_required
return true if User.find_by_id(session[:user_id])
access_denied
return false
end
def access_denied
flash[:error] = 'Oops. You need to login before you can view that page.'
redirect_to users_login_path
end
end
I want to use the login_required for each controller def method
Is there a better way instead of this?
class UsersController < ApplicationController
before_action :set_user, :login_required, :only => 'users/login'
#before_action only: [:show, :edit, :update, :destroy, :new]
def index
login_required
#users = User.all
end
def new
login_required
#user = User.new
end
end
Is there a better way to include login_required for all controllers methods since before_action doesn't seem to work?
I don't know the motivation of your logic, so I'll just focus on how you can solve this particular problem.
You can do something like this:
In your application controller:
class ApplicationController < ActionController::Base
before_action :login_required
private
def login_required
current_params = params["controller"] + "/" + params["action"]
if current_params == "users/new" or current_params == "users/index"
return true if User.find(session[:user_id])
access_denied
return false
end
end
def access_denied
flash[:error] = 'Oops. You need to login before you can view that page.'
redirect_to users_login_path
end
end
The login_required method will just run only on users controller's index and new action, for the rest, it'll just ignore. Also you can just use User.find() and no need to use User.find_by_id()
Now, in your users_controller.rb, you don't need to mention anything about login_required, everything will happen already in application_controller before coming here.
class UsersController < ApplicationController
before_action :set_user, :only => 'users/login'
#before_action only: [:show, :edit, :update, :destroy, :new]
def index
#users = User.all
end
def new
#user = User.new
end
end
Firstly, I'm going to suggest that you use devise for authentication, it's a lot more secure and should deal with this for you.
As for your problem, you should be able to specify the before_action like this:
before_action :set_user, :login_required, only: [:new]
Which you can put in your UserController. However if you want this globally, just put it in the ApplicationController, without the only: key.
If you want to require login for all pages except /users/login, then you almost have it right except you are specifying only: when you should be using except::
class ApplicationController < ActionController::Base
before_action :login_required, except: 'users/login'
...
end
This configuration will be applied to all sub-classes of ApplicationController as well.
I'm using Pundit to authorize actions in my controllers. My first try was to authorize the model in an after_action hoook:
class CompaniesController < InheritedResources::Base
after_action :authorize_company, except: :index
def authorize_company
authorize #company
end
This let me use the default controller actions which define #company so I wouldn't hit the database twice. But, this is bad for destructive actions because it's going to not authorize the action after I've already messed up the database.
So, I've changed to using a before_action hook:
class CompaniesController < InheritedResources::Base
before_action :authorize_company, except: :index
def authorize_company
#company = Company.find(params.require(:id))
authorize #company
end
Now, I'm not allowing unauthorized people to delete resources, etc... but I'm hitting the database twice. Is there anyway to access #company without hitting the database twice?
Since your asking for the "rails way" this is how you would set this up in "plain old rails" without InheritedResources.
class CompaniesController < ApplicationController
before_action :authorize_company, except: [:new, :index]
def new
#company = authorize(Company.new)
end
def index
#companies = policy_scope(Company)
end
# ...
private
def authorize_company
#company = authorize(Company.find(params[:id]))
end
end
If you really want to use callbacks you would do it like so:
class CompaniesController < ApplicationController
before_action :authorize_company, except: [:new, :index]
before_action :authorize_companies, only: [:index]
before_action :build_company, only: [:new]
# ...
private
def authorize_company
#company = authorize(Company.find(params[:id]))
end
def authorize_companies
#companies = policy_scope(Company)
end
def build_companies
#company = authorize(Company.new)
end
end
Yeah you could write a single callback method with three code branches but this has lower cyclic complexity and each method does a single job.
Turns out rails controllers have a resource if the model exists and build_resource for actions like new.
class CompaniesController < InheritedResources::Base
before_action :authorize_company, except: :index
private
def authorize_company
authorize resource
rescue ActiveRecord::RecordNotFound
authorize build_resource
end
end
i've some problem with the skip_before action:
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
before_action :require_login
before_action :inc_cookies
def inc_cookies
if cookies[:token] != nil
#name = cookies[:name]
#surname = cookies[:surname]
#user_roomate = cookies[:roomate]
end
end
def require_login
if cookies[:token] == nil
puts "No token"
redirect_to '/'
end
end
end
and my other controller:
class UsersController < ApplicationController
skip_before_action :require_login, :except => [:landing, :connect, :create]
end
I don't know why, but when I'm on the root (the :landing action from UsersController), Rails try to pass in the require_login...
I've misundertood something with this filter, or do I something wrong?
Thanks for any help!
This sounds normal to me - you've asked rails to skip your before action, except if the action is :landing, :connect or :create whereas it sounds as though you want the opposite. If you want those 3 actions not to execute the require_login then you should be doing
skip_before_action :require_login, :only => [:landing, :connect, :create]
Before any of my article controller crud actions can run (excluding index), i want to make sure that the article's active field is true.
I thought about doing this in a before_filter, but at that point #article has not been set, any ideas please?
Thanks
You could set the article in a helper method and remove some code duplication while you're at it.
class .. < ApplicationController
helper_method :current_article
def index
# your code etc..
end
private
def current_article
#article ||= Article.find(params[:id], :conditions => { :active => true }) ||
raise(ActiveRecord::RecordNotFound)
end
end
Basically you can now call current_article in your show, edit (etc) actions and views instead of #article.
You just need to do 2 before_filter.
1 with load the article and the second one to check if field exist
before_filter :load_article, :only => [:show, :edit, :update]
before_filter :has_field, :only => [:show, :edit, :update]
...
private
def load_article
#article = Article.find(params[:id])
end
def has_field
unless #article.active
redirect_to root_url
end
end