Elegant way to abstract before_actions across controllers? - ruby-on-rails

I have a series of controllers in my Rails API that are all extremely similar -- they only have basic CRUD actions, and only differ in the shape of the underlying data they are storing.
The way that I'm implementing authorization, in each controller I have some before_action calls that check the permissions at the appropriate level for the given CRUD actions -- these permissions checks are literally duplicates except for each one takes in a differently named instance variable -- e.g. one might say
before_action -> { is_app_admin?(#app_name) } #where #app_name is the actual name of the app.
Now, if somehow the controller itself could take a parameter, I could put these before checks in the ApiController and not have to repeat them. Or, I could change the variable name in all of the controllers to something generic like #app_name, but in the controllers themselves that leads to less readable code.
Is there a standard way of abstracting the duplicate code in this type of scenario?

Keep in mind that before_action isn't special syntax, it is just a class method like any other. That means that you could write a class method that calls before_action:
def self.ensure_app_admin_in(var)
before_action ->{ is_app_admin?(instance_variable_get(var)) }
end
Throw that in a module, controller concern, ApplicationController, or wherever is convenient and then in your controllers say:
class Controller1
ensure_app_admin_in :#app_name
#...
end
class Controller2
ensure_app_admin_in :#my_other_app_name
#...
end

Is there a standard way of abstracting the duplicate code in this type of scenario?
Yes. It is, well, abstraction. Hide that varying name in a method with a meaningful name. If, for example, you have these:
class Controller1
before_action -> { is_app_admin?(#app_name) }
end
class Controller2
before_action -> { is_app_admin?(#my_other_app_name) }
end
Then here is what you could do:
class Controller1
before_action -> { is_app_admin?(app_name_for_authorization) }
private
def app_name_for_authorization
#app_name
end
end
class Controller2
before_action -> { is_app_admin?(app_name_for_authorization) }
private
def app_name_for_authorization
#my_other_app_name
end
end
The before actions are now identical and you can pull them up to a parent class or extract as a concern.

You could create a module and put your similar controllers inside it, with a base_controller similar to application_controller that will have your before_actions, and only the controllers inheriting from it would use them.
For example if you have some admin controllers:
class Admin::BaseController < ApplicationController
before_action :authorize_admin!
def authorize_admin!
redirect_to root_path unless user.admin?
end
end
class Admin::UsersController < Admin::BaseController
def index
end
end
Then in your routes you could either namespace, or add module to the route like this:
resources :users, module: 'admin'
Then put your admin controllers in app/controllers/admin and your views in app/views/admin/users.

Related

Required certain params on certain actions

I have a requirement to need to validate presence of some params in certain situations. Here is the example of that :
In my user controller, for update action, I am required to validate the presence of these params. Same deal for car controller, update action as well, you could see recurring theme here. Params are additional_info.
My base controller provides additional_info_params which pulls the right data from the request.
Here is what I tried so far. I created a AR controller concern and included it in the controller, here is some code:
module ClassMethods
def require_additional_info_for(*methods)
binding.pry
return unless methods.include?(action_name)
if additional_info_params.empty?
head 400
end
end
end
My idea was to be able to define methods that require these params on the top of controller file, just like before_action from rails or skip_authorization_check from cancan. Like so:
MyController < BaseController
include Concerns::AdditionalInformation
require_additional_info_for :update
def update
...
end
end
This code above however does not work as I intended, mainly because this fires on the request class without much knowledge about the request (where I need to derive action name from via action_name).
So how can I do something like this?
Yes, you can, but i suggest you to use the before_action callback!
In a 'abstract' controller, register your method like this:
class SameController < ApplicationController
...
protected
def require_additional_params
render status: :unprocessable_entity if additional_info_params.empty?
end
end
After this, all the controllers who will use this methods, must extends SameController, and runs before_action passing the above method for the wanted actions, for example:
class UserController < SameController
before_action :require_additional_params, only: [:action1, :action2]
end
Note: You can put the require_additional_params in a module and include in your controller, or just put it in the ApplicationController
You might also look at making these regular strong params in the respective controller. It looks something like this:
def update_params
params.require(:car).permit(:engine, :wheels, :rims).tap do |car_params|
car_params.require(:engine)
end
end
This would expect a top-level :car key params (which it strips), and require an :engine param, but allow the other 2 (:wheels and :rims). If :engine isn't present, it will raise a ActionController::ParameterMissing (just like if :cars was missing)
This is straight from the action controller strong params docs (last example at bottom)
I'll sometimes throw these into separate private methods on the respective controller, so there would also possibly be a create_params method with different requirements. I prefer this method over using a custom method as a before_action.

How do I initialise objects in a partial view for use by multiple controllers?

I hope this is something obvious that I've just consistently overlooked and the community can set me on the right path.
I have a news article controller, but I want to be able to use a "common" ticker list on different views. How do I initialise this "#article_list" if I'm using the partial in a few controllers? Apparently it is of the opinion that using a helper is not the solution, since helpers are just for view logic. So where do I put this initialiser that would be available to every controller as required? I shouldn't put them in application controller should I?
You can use before_filter method, i.e. something like this:
class ApplicationController < ActionController::Base
def set_article_list
#article_list = ArticleList.all # or any onther selection
end
end
class NewsArticleController < ApplicationController
before_filter :set_article_list, only: :action1
def action1
end
end
class AnotherNewsArticleController < ApplicationController
before_filter :set_article_list, only: :another_action1
def another_action1
end
end
UPDATE:
Indeed, there will be problem with a fat ApplicationController. To avoid it it's possible to use module (almost #carolclarinet describe it below):
module ArticleList
def set_article_list
#article_list = ArticleList.all # or any onther selection
end
end
class NewsArticleController < ApplicationController
include ArticleList
before_filter :set_article_list, only: :action1
def action1
end
end
class AnotherNewsArticleController < ApplicationController
include ArticleList
before_filter :set_article_list, only: :another_action1
def another_action1
end
end
And
You can create, essentially, a query object that is only responsible for returning what you need for #article_list, for example, building off of Psylone's answer:
class ArticleList
def ticker_articles
ArticleList.all # or any onther selection
end
end
This class could go in lib, app/models, app/query_objects, app/models/query_objects, wherever it makes sense for you. This is a bit outside The Rails Way so there's no convention about where these types of objects should live.
Then in whatever controller you need this, do:
#article_list = ArticleList.new.ticker_articles
For more explanation of query objects, see this codeclimate article #4. Depending on what you're doing to set #article_list, this might also be called a service object (#2) or something else entirely. No matter what you call it though, its responsibility would be to return the value you need for #article_list and that's it.

How to reuse the rendering actions from nested controllers in Rails?

I have a question regarding the reuse of code among controller actions. I think it is a fairly standard situation, so I am interested in what's the best practice in Rails.
Let's say I have a films resource with a corresponding FilmsController, which has a nested resource comments served by CommentsController. The nested resource can be rendered on its own using its index and show actions. However, it should also be possible to render the comments embedded in the corresponding film page.
Now, the question goes, what is the best way to reuse the code from CommentsController within FilmsController.show?
1) Force the CommentsController.index to render to a string and then pass it in a variable to the film view?
Or 2) call the CommentsController.index directly in the film view as a kind of "partial", executing the database queries from there?
Or 3) create a separate method in CommentsController responsible for the database handling, call it from both CommentsController.index and FilmsController.show, and use the corresponding view in both the places, too?
To me the options 1) and 2) seem a bit messy, while 3) is not modular and involves some repeating of code. Is there any better way to accomplish this?
Thanks a lot!
Now, the question goes, what is the best way to reuse the code from CommentsController within FilmsController.show?
You could move the shared controller logic into a inside your application controller (or a lib and require it appropriately), a la:
class ApplicationController < ActionController::Base
def foo
#foo = "foo"
end
end
Comments Controller:
class CommentsController < ApplicationController
before_filter :foo, :only => [:index]
def index
end
end
Films Controller:
class FilmsController < ApplicationController
before_filter :foo, :only => [:show]
def show
end
end
For repeated view logic you can move that to a common folder, say your_app/app/views/shared/_foo.html.erb and render that appropriately.
Another option is to place the relevant code into an external module:
lib/mymodule.rb
module MyModule
def foo
end
end
And then you can include the module inside your controller or anywhere you want access to your foo method.
class CommentsController < ApplicationController
include MyModule
def index
foo()
end
end

Where should I store common function at?

I have three models.
users_controllers.rb
communities_controller.rb
community_tipics_controller.rb
I'm using the function called check_digit
def checkdigit
(transaction)
end
and I'm placing this in all the controllers.and calling it by before_filter.
But I think I'm wasting because I'm just pasting exactly the same code above to all of the controllers.
Where am I supposed to put it if my controller is using it in common?
Should I put it in controllers/application_controller.rb?
There are several ways of doing this , some possible ways are (If I'm to do this)
1st method
If this is a simple method used by controllers
inside application_controller.rb
private
def checkdigit
(transaction)
end
2nd method
if your function is used by a specific category , (this is normally I do very often), create a separate controller and have your all other controllers inherited by it
Ex: if your method used only my admins, and assuming you have some more methods like that
class AdminController < ApplicationController
layout 'admin'
private
def checkdigit
(transaction)
end
end
and
class UsersController < AdminController
end
3rd method
If your method is/will used by models/controllers etc.. consider making it a module
module CommonMethods
def checkdigit
(transaction)
end
end
class UsersController < ApplicationController
include CommonMethods
end
HTH
You can put it in helpers/application_helper.rb
You are correct, all common methods that all controllers need should be stored in the ApplicationController.
Furthermore, you should also keep common logic between all controllers in this controller.
Edit:
If they are just helpers, then you would put them where the helpers go, see the answer by #simone.

Calling a method from another controller

If I've got a method in a different controller to the one I'm writing in, and I want to call that method, is it possible, or should I consider moving that method to a helper?
You could technically create an instance of the other controller and call methods on that, but it is tedious, error prone and highly not recommended.
If that function is common to both controllers, you should probably have it in ApplicationController or another superclass controller of your creation.
class ApplicationController < ActionController::Base
def common_to_all_controllers
# some code
end
end
class SuperController < ApplicationController
def common_to_some_controllers
# some other code
end
end
class MyController < SuperController
# has access to common_to_all_controllers and common_to_some_controllers
end
class MyOtherController < ApplicationController
# has access to common_to_all_controllers only
end
Yet another way to do it as jimworm suggested, is to use a module for the common functionality.
# lib/common_stuff.rb
module CommonStuff
def common_thing
# code
end
end
# app/controllers/my_controller.rb
require 'common_stuff'
class MyController < ApplicationController
include CommonStuff
# has access to common_thing
end
Try and progressively move you methods to your models, if they don't apply to a model then a helper and if it still needs to be accessed elsewhere put in the ApplicationController
If you requirement has to Do with some DB operations, then you can write a common function (class method) inside that Model. Functions defined inside model are accessible across to all the controllers. But this solution does to apply to all cases.
I don't know any details of your problem, but maybe paths could be solution in your case (especially if its RESTful action).
http://guides.rubyonrails.org/routing.html#path-and-url-helpers

Resources