Let's say that I have a widget that displays summary information about how many posts or comments that I have on a site.
What's the cleanest way to persist this information across controllers?
Including the instance variables in the application controller seems like a bad idea. Having a before filter that loads the data for each controller smells like code duplication.
Do I have to use a plugin like the Cells Plugin (http://cells.rubyforge.org/) or is there a simpler way of doing it?
Presumably, you have a single partial that displays this info. You can put the methods that fetch the data you need in ApplicationHelper or as class methods on whatever model(s) you're getting the data from. Then call that method in the partial when you need to display it.
I wound up doing something similar to this:
In controllers/application.rb
def load_sidebar
#posts = Post.find(:all)
end
To include the sidebar in various actions I did this:
before_filter :load_sidebar, :only => [ :index ] #load from application.rb file
I made the sidebar into a shared partial.
def load_sidebar
#posts = Post.find(:all)
end
You mentioned that you were displaying summary info. If you don't really want to load all of your posts into memory, you can do the following.
def load_sidebar
#post_count = Post.count(:id)
end
Related
I'm new to Rails (I've worked in MVC but not that much) and I'm trying to do things the "right" way but I'm a little confused here.
I have a site navigation with filters Items by different criteria, meaning:
Items.popular
Items.recommended
User.items
Brand.items # by the parent brand
Category.items # by a category
The problem is that I don't know how to deal with this in the controller, where each action does a similar logic for each collection of items (for example, store in session and respond to js)
Either I have an action in ItemsController for every filter (big controller) or I put it in ItemsController BrandsController, CategoriesController (repeated logic), but neither provides a "clean" controller.
But I don't know witch one is better or if I should do something else.
Thanks in advance!
You're asking two separate questions. Items.popular and Items.recommended are best achieved in your Item model as a named scope This abstracts what Xavier recommended into the model. Then in your ItemsController, you'd have something like
def popular
#items = Item.popular
end
def recommended
#items = Item.recommended
end
This isn't functionally different than what Xavier recommended, but to me, it is more understandable. (I always try to write my code for the version of me that will come to it in six months to not wonder what the guy clacking on the keyboard was thinking.)
The second thing you're asking is about nested resources. Assuming your code reads something like:
class User
has_many :items
end
then you can route through a user to that user's items by including
resources :users do
resources :items
end
in your routes.rb file. Repeat for the other nested resources.
The last thing you said is
The problem is that I don't know how to deal with this in the controller, where each action does a similar logic for each collection of items (for example, store in session and respond to js)
If what I've said above doesn't solve this for you (I think it would unless there's a piece you've left out.) this sounds like a case for subclassing. Put the common code in the superclass, do the specific stuff in the subclass and call super.
There's a pretty convenient way to handle this, actually - you just have to be careful and sanitize things, as it involves getting input from the browser pretty close to your database. Basically, in ItemsController, you have a function that looks a lot like this:
def search
#items = Item.where(params[:item_criteria])
end
Scary, no? But effective! For security, I recommend something like:
def search
searchable_attrs = [...] #Possibly load this straight from the model
conditions = params[:item_criteria].keep_if do |k, v|
searchable_attrs.contains? k
end
conditions[:must_be_false] = false
#items = Item.where(conditions)
end
Those first four lines used to be doable with ActiveSupport's Hash#slice method, but that's been deprecated. I assume there's a new version somewhere, since it's so useful, but I'm not sure what it is.
Hope that helps!
I think both answers(#Xaviers and #jxpx777's) is good but should be used in different situations. If your view is exactly the same for popular and recommended items then i think you should use the same action for them both. Especially if this is only a way to filter your index page, and you want a way to filter for both recommended and popular items at the same time. Or maybe popular items belonging to a specific users? However if the views are different then you should use different actions too.
The same applies to the nested resource (user's, brand's and category's items). So a complete index action could look something like this:
# Items controller
before_filter :parent_resource
def index
if #parent
#items = #parent.items
else
#items = Item.scoped
end
if params[:item_criteria]
#items = #items.where(params[:item_criteria])
end
end
private
def parent_resource
#parent = if params[:user_id]
User.find(params[:user_id])
elsif params[:brand_id]
Brand.find(params[:brand_id])
elsif params[:category_id]
Category.find(params[:category_id])
end
end
I want to log the number of cache misses for certain blocks in my views. I do the log storing in an after_filter in my controller.
It doesn't seem possible to manipulate instance variables inside of views. What's the best way to store information during view rendering that is then made available to a controller's after_filter?
Not sure if this is the best solution, but it works in a test rails app:
Add a method to your controller. It must be public, unless you're ok using send to call it.
def increment_miss
#miss_count ||= 0
#miss_count += 1
end
Then an after_filter (I've restricted mine to :index):
after_filter :only => :index do
Rails.logger.info("miss_count: #{#miss_count.to_i}")
end
Then call the method in your view as many times as you need to:
<% controller.increment_miss %>
You may want to modify the method to take an optional increment_by param if you want to be able to increment by more than 1 per call.
I would consider moving the logic into a dedicated model object.
class MyController > ApplicationController
around_filter :record_cache_misses
def record_cache_misses
#cache_miss_logger = CacheMissLogger.new
yield
#cache_miss_logger.persist_count
end
end
And in the view it would be a normal method call.
<% #cache_miss_logger.increment_count %>
I suggest,
There is no straight forward way to achieve this as its not per rails MVC rule, and in any way it is not doable(unless you change rails)
You deal with data that has scope starting from and ending with views, so why to go reverse and make controller responsible for logging/utilizing it, let view/helper method log it
So you may initiate it in controller, you may have referred to cache in controller, take it forward, let view manipulate it, and in views/helpers you can log it, keep a separate log storing module/file take care of this concern, let helper include it, call it from views.
I found one more way out, I hope/guess it would be thread-safe, would be great if anybody confirms
# in controller
before_filter :foo
after_filter :bar
def foo
class << #cache_miss
attr_accessor :count
end
#cache_miss.count = 1
puts "----------#{#cache_miss.count}" # => 1
end
def bar
puts "----------#{#cache_miss.count}" # => 2
end
#in views
<% #cache_miss.count = 2 %>
I'm new to Rails and just wondering when I should put code into a Helper as opposed to putting the code into the Model.
Is there a 'rule of thumb' so to speak for this?
Use helpers if you're working in a view (template) and you need to build a complex bit of HTML such as a <table>. Or, if you want to change some presentation data that's not connected to the database.
def truncate_html( html, options = {} )
options[:length] = 35 unless options[:length]
truncate( strip_tags( html ), options )
end
Use models when you're working with database objects, and you want to simplify the business logic.
def one_day?
start_date.to_s[0,9] == end_date.to_s[0,9]
end
Here's Helpers in the guides: http://guides.rubyonrails.org/form_helpers.html
And here's Models: http://guides.rubyonrails.org/active_record_querying.html
It's best to use helpers when the code that the helper is creating is meant to be displayed in the view only. For example if you want to have methods that help create HTML links, they should go in the helper:
def easy_link user
link_to(user.name, user)
end
If your code is business logic it should go in your models. You should also aim to put as much business logic in your models, you don't want this code in your views and controllers. For example, if you want to process an order, that code should go in the model:
def process
raise NotReadyToProcess unless ready_to_process?
raise NotValidPaymentDetails unless valid_payment_details?
process_payment
end
Helpers should only contain logic for the view
Models should contain only logic related to the object modeled, never related with the transaction performed neither the view rendered
by default, when i ask rails controller to do messages/index, he does
def index
respond_to{|fmt| fmt.html}
end
and shows app/views/messages/index.html.erb
there is a customer which wants his instance of the platform to display views differently (and changes cannot be done with css only).
solution i think of would be
create directory app/views/#{customername}, which would have same structure as app/views, but would only have views which have to override default ones.
setting an array constant containing list of views which have to be overriden (if not, they should load the default views)
CUSTOM_VIEWS["messages"]=["index","show","edit"]
somewhere in the customer-specific config file
in all controller actions do something like
def index
respond_to do |fmt|
fmt.html do
if CUSTOM_VIEWS[params[:controller]].include?(params[:action])
#override default app/views/messages/index.html.erb with app/views/customername/messages/index.html.erb
render "#{customername}/#{params[:controller]}/#{params[:action]}"
end
end
end
end
or is there better/faster solution/plugin to do that?
This should help:
http://railscasts.com/episodes/125-dynamic-layouts
i believe "view_paths" along with "prepend_view_path" can be an answer to my question
for example
http://www.axehomeyg.com/2009/06/10/view-path-manipulation-for-rails-with-aop/
upd:
solved with simple add to application_controller
def override_views
if APP_CONFIG['pr_name']!=nil
ActionController::Base.view_paths=[RAILS_ROOT+"/app/custom_views/"+APP_CONFIG['pr_name'],RAILS_ROOT+"/app/views/"]
end
end
where APP_CONFIG['pr_name'] is specific product name.
basically what it does is loading custom view from app/custom_views/customername/ if it exists for specific controller action, if not it loads default view from app/views/
I'd like to be able to dispatch from one controller action to another conditionally, based on a combination of query parameters and data in the database.
What I have right now is something like:
class OldController < ApplicationController
def old_controller_action
if should_use_new_controller
new_params = params.dup
new_params[:controller] = "new_controller_action"
redirect_to new_params
return
end
# rest of old and busted
end
end
class NewController < ApplicationController
def new_controller_action
# new hotness
end
end
This works just fine, but it issues an HTTP redirect, which is slow. I'd like to be able to do this same thing, but within the same HTTP request.
Is there a clean way to do this?
Edit: The bounty will go to someone who can show me a clean way to do this that leaves the controllers and their actions relatively untouched (other than the redirect code itself).
Instead of calling code across actions, extract the code to lib/ or something, and call that code from both controllers.
# lib/foo.rb
module Foo
def self.bar
# ...
end
end
# posts_controller
def index
Foo.bar
end
# things_controller
def index
Foo.bar
end
Create an instance of the controller class:
#my_other_controller = MyOtherController.new
Then call methods on it:
#my_other_controller.some_method(params[:id])
I prefer the module idea, but this should do the trick.
You can also pass parameters as a whole from another controller:
#my_other_controller.params = params
I suspect you want option 3, but lets go through the some alternatives first
Option 1 - Push the controller selection logic into a helper that inserts the right link into your view. Benifits - controllers remain clean, Cons - if decision logic depending on submitted values this approach won't work. If URL is being called by external websites then this won't work.
Option 2 - Push the logic back into your model. Pro's - keeps controller clean. Cons - doesn't work well if you've got lots of sesson, params or render / redirect_to interaction.
Option 3 - Stay within the same controller. I suspect you are trying to replace some existing functionality with some new functionality, but only in some cases. Pro's - Simple and have access to everything you need. Cons - only works if it makes sense to use the same controller i.e. you're working with the same entity such as user, place or company.
Lets look an an example for option 3. My links controller has totally diferent behavour for admins than other users ...
class LinksController < ApplicationController
#...
def new
#Check params and db values to make a choice here
admin? ? new_admin : new_user
end
#...
private
def new_admin
#All of the good stuff - can use params, flash, etc
render :action => 'new_admin'
end
def new_user
#All of the good stuff - can use params, flash, etc
render :action => 'new_user'
end
end
If two controllers are trying to do the same thing, there's a very good chance this should be in a model. Take a good look at your design and -- I'm sorry I don't know your experience level with MVC -- read up on thin controller techniques:
http://weblog.jamisbuck.org/2006/10/18/skinny-controller-fat-model
http://www.robbyonrails.com/articles/2007/06/19/put-your-controllers-on-a-diet-already
http://andrzejonsoftware.blogspot.com/2008/07/mvc-how-to-write-controllers.html
If the problem is that you need the other controller to do the render, then maybe the route should have pointed there to begin with, and still the skinny controller technique should save the day.
If extracting the common code between controllers into a module doesn't work for you, I would use Rack middleware. I haven't seen code that uses ActiveRecord within middleware but I don't know of any reason why it shouldn't be possible since people have used Redis and the like.
Otherwise I think your only option would be to restart processing of the request with something like (untested, pseudo example):
env['REQUEST_URI'] = new_controller_uri_with_your_params
call(env)
This is similar to how integration tests are implemented. But I don't know if everything from call until you hit a controller is idempotent and safe to rerun like this. You could trace through the source and see. But even if it's ok now, it might break in any future version of rails or rack.
Using middleware would avoid this by letting you intercept the request before it's been run. You should still be able to share code with your rails application by extracting it out into common modules included in both places.
Honestly I think just doing the simple thing of factoring the common controller code is likely cleaner, but it's hard to know without the details of your situation so I thought I'd go ahead and suggest this.
Do this:
class OldController < ApplicationController
def old_controller_action
if should_use_new_controller
new_controller_action
end
# rest of old and busted
end
end
and the new controller
class NewController < OldController
def new_controller_action
# new hotness
end
end