Keep Controllers DRY : where to put shared methods? - ruby-on-rails

My code is starting to have duplicates.... because I don't know where to put methods/functions that need to be accessed by (2) different controllers, and just to get it up quickly I just duplicated it.
So I have a users_controller, and a pages_controller and posts_controller.
On the users page, I have posts being displayed.
On the homepage, I also have posts being displayed.
Where should I put shared/common code so that the users_controller and pages_controller and posts_controller can all have access to ithese methods?
Right now, I have duplicated def methods in some controllers, and also some duplicate private methods in each controller.
On a related note, how can I access methods defined in one controller from another controller?

Maybe a module?
module ProductSharedMethods
def product_list
Product.scoped
end
end
class UsersController < ApplicationController
include ProductSharedMethods
def index
#products = product_list
end
end

If you have posts being displayed on two separate pages then use a partial. I don't know what version of rails you are using but newer versions have a views/application folder for shared partials.
If you have a method that is shared across more than one controller then put it in the application controller. Controllers are classes so follow all of the normal rules.
Hope that helps.

Related

Share the same piece of data in multiple controllers and views

Lets say that we have a rails app which has a sidebar on every page that shows some data, like post archives, post categories etc. Which is the best way to share the same data on each of our controllers?
Iguess the most easy fix is to use the same before_actions on each controller, but this doesn't DRY up much our code, or maybe move all these into a parent class/cotrnoller that all controllers will inhert from, but is there a better way of doing this?
Simple Solution:
Normally, I just put these into application_controller.rb as a before_action.
Example
# app/controllers/application_controller.rb
before_action :set_sidebar_resources
# ...
private
def set_sidebar_resources
#sidebar_archives = Archive.all
#sidebar_categories = Category.all
end
Modular Solution:
Simple solution above works great until you define more and more methods and other global controller logic into ApplicationController, and then the file becomes too big to manage. The following is a less conventional approach favouring more of manageability rather than simplicity.
Example
# app/controllers/application_controller.rb
include WithSidebar
# app/controllers/concerns/with_sidebar.rb
module WithSidebar
extend ActiveModel::Concern
included do
before_action :set_sidebar_resources
private
def set_sidebar_resources
#sidebar_archives = Archive.all
#sidebar_categories = Category.all
end
end
end
Rails controllers inherit from application controller.
ex.
class ClientsController < ApplicationController
You could add before_actoion to ApplicationController that pulls the data you want into #variable and then use that variable in other controllers.
Just don't over-do it and keep your controllers skinny :)

Rails where do global variables go?

I'm learning alot about the DRY concept in Rails so here's my question:
I have a fixed sidebar in my application that always needs to load various Objects, such as Locations.
In the controller related to my index page, actually in controllers/pages_controller.rb I'm loading #locations:
#company = current_user.company
#locations = if #company
current_user.company.locations.order(:name)
else
[]
end
Which works fine for my navigation menu (sidebar), however, once I select a location and load a view delegated from a different controller (in this case locations/show.html.erb) my #locations variable is no longer found.
Sure, I could add it to the show method in my locations_controller.rb file, but that isn't very DRY considering I'd have to put the above code in every single controller method that relies on the sidebar navigation menu (all of them!) since it's fixed.
What's the DRY way to do this once and not have to do it in every controller?
You can create a method in your application_controller
def set_locations
....
end
Then in your pages_controller and locations_controller add a before_filter
before_filter :set_locations
It will set call set location before all all methods of the pages and locations controller.
If you want to set locations only for few controller actions then call:
before_filter :set_locations,:only=>[:show,:index, :your_action_name]
Updated:
In Rails 5 before_filter is deprecated and you can use before_action
You could add it to the ApplicationController in a method that runs before every action, every controller inherits from this ApplicationController so it will be available.

what is the best way to set author field on STI model

I have a basic model called Page and have many STI models based on Page such as
Drawing
Article
Story
etc...
I have separate controller and view for each of these STI models because I needed to customized the view layer based on the model type and have different logic for controller and hence separate controllers. However, I need the author field of all models to be set to current user. How do I do this in a single place?
For example, if I use before_action on Page controller and set the author, it affects the #page instance variable whereas my DrawingsController is using #drawing so it wont save the author of my #drawing unless I repeat the same code in DrawingsController.
Edit:
My controller hierarchy is
DrawingsController < PagesController
PagesController < ApplicationController
Both PagesController and DrawingsController have all the 7 restful actions. However, the actions on PagesController doesn't serve any purpose as I dont want any of my users to create Pages. I only want them to create the inherited STI classes like Drawings.
you could do this using some convention and meta programming in your controller hierarchy:
def add_author
model = instance_variable_get(:"##{controller_name.singularize}")
model.author = current_user
end
I'll be honest, I make no guarantees about the "best-practice"-ness of this answer, but I'll propose it anyways in case it helps to some degree. Also note that after rethinking the problem, I realized my first suggested solution in the comments was wrong, and the second is also not quite right either. So I'm posting a modified version of the second suggestion only:
Short answer: Let the PagesController handle most of the work, and delegate only to the subcontroller for model-specific things if needed. As phoet said, you can use a bit of meta programming (in a different way) to accomplish this.
class PagesController < ApplicationController
# pages controller stuff here
def create
#page = controller_name.classify.constantize.new(params[:page_params]) # I love Rails, don't you?
#page.author = current_user
handle_additional_create_actions
# For completeness of this example...
if #page.save
# render / redirect on success
else
# render errors
end
end
protected
# This method should be overwritten by sub controllers if needed
# Also, name this whatever you like, this is merely a verbose example for illustration purposes
def handle_additional_create_actions
# in the pages controller, this method does nothing
end
end
And, if there are additional things that need to be done by the model-specific controller:
class DrawingsController < PagesController
# drawing controller stuff here
protected
def handle_additional_create_actions
#page.some_other_field = some_other_data
end
end
A quick note: Note that in my suggestion, you're eliminating the model-specific variable names, meaning we don't have an #drawing and an #article, etc, anymore. Your models are all, essentially, types of Page objects, and so we're going to call it by its general name as a convention. That way, when you ask the DrawingsController to do something specific for the Drawing class, it knows that instance can be accessed via our generically named #page object.
So ultimately, the PagesController does the heavy lifting, regardless of which concrete model type you're dealing with. That way, only general page stuff is found in the pages controller, and drawing, article or story-specific stuff is found in their respective concrete controllers.

Menu displayed on all pages, code is replicated in all controllers

I have a collection model. I succesfully created a _collection.html.erb that i call with <%= render #collections%> in my application layout.
My problem is that in ALL my controlers method I must add #collections = Collection.all
I found it very very ugly,it will make my collection scope a pain to change, and I'm sure that I am missing a rails magic something that would be way nicer.
Is there a way to have a part of the layout generated by model data without having a identical piece of code in AAAALLLLLL the controllers?
Notice that your controllers all inherit from ApplicationController. Use this to your advantage. Add a before_filter to ApplicationController that loads your collections.
#cam was right. Any rails project has an ApplicationController. Your controllers all start with MyController < ApplicationController, right? If so, that means you can create a before_filter in your ApplicationController, which will be inherited by all your controllers. To do so :
/app/controllers/application_controller.rb
before_filter :load_collection
def load_collection
#collections = Collection.all
end
From now on, you can use #collections from all your controllers (as long as they inherited from ApplicationController)

where to put controller code for partials you render from different views in Ruby on Rails

A newbie question:
I have a partial that I'm loading on a website on different pages.
That partial needs some data, so in the controller of the parent view I do the query:
#properties = Property.find(:all)
I pass the query results to the partial in the view using :locals
Now, I would like to render the same partial from another view. This view has a different controller. Do I have to repeat the query in that controller also? Or can I have a separate controller for partials I use one more places in the website. In this last case, how do I indicate which controller to use in the view when I put the render command?
Or should I use somethink different than partials for this kind of use?
Kind regards,
Michael
How i would solve this, is to define a new file inside your lib folder, with a meaningful name. For now i will just use does_as_shared.rb:
module DoesAsShared
def setup_shared_data
#shared_data = ...do something useful here ...
end
end
and then inside your controllers that need that code you write:
class ThingsController < ApplicationController
include DoesAsShared
def index
setup_shared_data
.. and do some more stuff ...
end
end
The advantage of this code is that the shared code is limited to those controllers that really need it. And at the same time it is pretty readable: the include statement, given that the name is chosen well, clearly indicates what functionality is included.
You don't need to copy the code to set up the partial's data, but you would need to hoist it into a controller that your two controllers inherit from. Normally, this would be your ApplicationController. Define a method in there that loads the data you need. For example:
class ApplicationController < ActionController::Base
# ... your code ...
protected
def load_partial_data
#properties = Property.find(:all)
end
end
Now to actually call this method you have two options:
If all your controllers need this data, add a before_filter to your ApplicationController like before_filter :load_partial_data
Otherwise, add the before_filter to just the controllers that actually need to load the data.
I hope this helps.

Resources