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
Related
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.
I have two models:
Student
Classroom
Both of them have an action that does the same exact thing: it shows a report of daily activity. That is:
/students/1
/classrooms/1
Grabs activity for the model in question and displays it on the page.
In an attempt to dry this up, I created a ReportsController which extracts all the common logic of building a report.
If I leave the routes like this:
/students/1/report
/classrooms/1/report
Then I can have the ReportsController#show action look for params for :student_id or :classroom_id to determine which model type it is dealing with (for purposes of querying the database and rendering the correct view).
But I would prefer the URLs to be cleaner, so I also changed my routes.rb file to pass the show action for these models to the reports#show controller action:
resources :students, :classrooms do
member do
get :show, to: 'reports#show'
end
end
This works, but I can no longer depend on params to identify which model to work with and which view to render.
Question: should I parse request.fullpath for the model? Or is there a better way to make a shared controller understand which model it is working with?
Routing both show methods to the same controller method for code reuse is somewhat like banging a nail in with a dumptruck.
Even if you can find the resource by looking at the request url you would start splitting the ResortsController into a bunch of ifs and switches even before you got off the ground.
One solution is to add the common action in a module:
module Reporting
extend ActiveSupport::Concern
def show
# the Student or Classroom should be available as #resource
render 'reports/show'
end
included do
before_action :find_resource, only: [:show]
end
private
def find_resource
model = self.try(:resource_class) || guess_resource_class
#resource = model.find(params[:id])
end
# This guesses the name of the resource based on the controller name.
def guess_resource_class
self.class.name[0..-11].singularize.constantize
end
end
class StudentController < ApplicationController
include Reporting
end
# Example where resource name cannot be deduced from controller
class PupilController < ApplicationController
include Reporting
private
def resource_class
Student
end
end
self.class.name[0..-11].singularize.constantize is basically how Rails uses convention over configuration to load a User automatically in your UsersController even without any code.
But the most important key to DRY controllers is to keep your controllers skinny. Most functionality can either be moved into the model layer or delegated out to service objects.
I would put the common logic in the Event Model:
#Event Model
class Event < ...
def self.your_event_method
#self here will be either student.events or classroom.events
#depending on which controller called it
end
end
class StudentsController < ...
...
def show
student = Student.find(params[:id])
student.events.your_event_method
end
end
class ClassroomsController < ...
...
def show
classroom = Classroom(params[:id])
classroom.events.your_event_method
end
end
I'm a Rails beginner and I learn that I always must try to be more DRY.
I'm have a comment system associated to my content model, and I load my comment with ajax on page scroll.
In my view I have:
%section.article-comments{'data-url' => content_comments_path(#content)}
and in my routes.rb file I have the route
resources :contents, only: :index do
resources :comments, only: :index
end
My comment controller of course is
def index
#content = Content.find(params[:content_id])
#comments = #content.comments
render ...
end
Now I want to add comments also to videos and gallery.
So I need to add a route for every resource and I need a gallery_index and a video_index.
Content, video and gallery index method in comment controlelr are repeated, and I cannot understand how can I be more DRY.
All your controllers presumably inherit from ApplicationController:
class CommentsController < ApplicationController
If you find yourself with a lot of repetition in any of the controller methods you could define it in ApplicationController instead, with maybe some specific processing in each controller.
For example:
class ApplicationController < ActionController::Base
def index
...some common processing...
specific_index_processing
end
private
def specific_index_processing
# empty method; will be overridden by each controller as required
end
end
class CommentsController < ApplicationController
private
def specific_index_processing
...specific procesing for the comments index method...
end
end
And of course, if one of your controllers needs to be completely different from this common approach you can always just override the entire index method.
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.
So I have a method and corresponding partial for including a set of random photos in the sidebar of certain areas of our site.
Right now I have a random_photos method in ApplicationController set with a before_filter.
That works in the sense that it makes the contents of the random_photos method available wherever I need it, but it also unnecessarily executes some complex SQL queries when I don't know it too (ie, when I don't need to access those random photos).
So, how can I limit the accessing of the random_photos method to only when I really need it?
You can add an :if condition to the before_filter call, like so:
class ApplicationController < ActiveController::Base
before_filter :random_photos, :if => is_it_the_right_time?
Yet another option is to use skip_before_filter. It just depends in how many controllers you want to be different. Use skip_before_filter if there are only a handful of controllers you want to be the exception. Use one of the other suggestions if there are many controllers where you want to bypass the filter.
class ApplicationController < ActiveController::Base
before_filter :random_photos
def random_photos
#photos = Photo.random
end
end
class OtherController < ApplicationController
skip_before_filter :random_photos
...
end
You can keep the random_photos method in ApplicationController, and put the before_filters in your other controllers.
class ApplicationController < ActiveController::Base
...
def random_photos
#photos = Photo.random
end
end
class OtherController < ApplicationController
before_filter :random_photos, :only => 'show'
...
end
It depends on how many functions are making use of random_photos...
If a handful then use vrish88's approach but with an after_filter:
class ApplicationController < ActiveController::Base
after_filter :random_photos, :if => is_it_the_right_time?
...
private
def is_it_the_right_time?
return #get_random_photos
end
end
class SomeController < ApplicationController
def show
#get_random_photos = true
...
end
end
If every function in a controller will make use of it then use the skip_before_filter or move the before_filter in the controller and out of the application controller.
Many ways to get it done, and none is more correct then the next. Just try to keep it as simple and transparent as possible so you don't recreate the functionality months down the road because you forgot where all the pieces are located.