Rails - moving out calculations from my views? - ruby-on-rails

Currently I'm performing some calculations in my views, which is a bad thing, of course:
<% categories.each do |c| %>
....
<%= c.transactions.sum("amount_cents") %>
....
<% end %>
I am researching ways that will help me to refactor the above issue.
One thing is to move the calculation to my controller
#category_sum = #transaction.sum("amount_cents")
Which is probably a better solution, but you know. Not perfect.
Since I have many users, I do not see how can I move the calculator logic into my Model. So I guess I might need to use a new Class, create a bunch of methods (sum, average, etc.) and use them in the views? Am I on the right track? Will be thankful for any advice on how to restructure my code and design and implement this Class.

One mean to isolate view logic is to use presenters.
A presenter allows you to do something like that :
<% categories.each do |c| %>
....
<% present c do |category| %>
<%= category.transaction_sum %>
<% end %>
....
<% end %>
You then have a presenter class in app/presenters/category_presenter.rb :
class CategoryPresenter < BasePresenter
presents :category
def transaction_sum
category.transactions.sum("amount_cents")
end
end
Of course, it is best used if you have many methods in that presenter (but once you begins to reduce view logic, it's quick to fill presenters).
The implementation used here rely on what is describe in this pro railscast. The basic idea is simply to have a #present helper that infers a class name based on object class, load and initialize the proper presenter class.
An other popular alternative is to use drapper, which use the concept of decorator, but a presenter is basically a decorator.

The main code smell you're seeing is called the law of Demeter (which, like many programming "laws", you should think of it more like a "guideline of Demeter").
What you could do is move the actual calculation step into a method on the category, e.g.
class Category < ActiveRecord::Base
def transaction_amount
transactions.sum("amount_cents")
end
end
<% categories.each do |c| %>
....
<%= c.transaction_amount %>
....
<% end %>
Technically speaking the calculation is still performed while rendering the view, but the logic for how that summed amount gets calculated is no longer inside the view itself. All the view now cares about is that it can send the message transaction_amount to the category objects. This also leaves room for you to add a cache for the sums, or for you to stop passing actual records, but instead pass static objects (that aren't ActiveRecord models) that come out of some piece of code that performs the sum in a more efficient manner.

Related

Move logic like this to the controller or model rather than the view?

I have this logic currently in my view
<% tools_count = #job.tools.count - 1 %>
<% count = 0 %>
<% #job.tools.each do |u|%>
<%= u.name %>
<% if count != tools_count %>
<% count += 1 %>
<%= "," %>
<%end%>
<% end %>
Which just loops through some users relations and puts in a , unless it is the end of the list.
My question: This kind of logic looks really messy and clogs up my views I know there must be a better way of doing this by moving it into the controller or maybe model, does anyone know the correct way to do this kind of logic?
You can add a method like this to your Job model:
def tool_names
tools.map(&:name).join(',')
end
And use it in your view like this:
<%= #job.tool_names %>
There are couple of ways to avoid putting this kind of logic in the view layer:
Create an instance method in the model class (as spickermann suggested)
This will work for simple logic and simple projects. However, when you will want to use some helpers from ActionView::Helpers such as jobs_path or number_to_currency, a model is not a good place for it.
Create a helper method in helper modules eq. JobHelpers
Generally you can put any helper methods related to view layer in helpers. For example to share common methods for building a view components.
Use the decorator/presenter pattern and put there the view logic so model won't be polluted. Here is some more explanation about the pattern and sample implementation using draper gem: http://johnotander.com/rails/2014/03/07/decorators-on-rails/
You can do it in a single line like
<%= #job.tools.map(&:name).join(',') %>

'Tell, Don't Ask' whilst maintaining Separation of Concerns

In my Rails app, I have the following association:
Video belongs to Genre (Video does not HAVE to have a genre)
Genre has many Videos (Genre can have no videos)
In the Video model, I have the following method.
# models/video.rb
def genre_name
genre.present? ? genre.name : ''
end
This is to avoid something like this in the view (which just seems messy):
# views/videos/show.html.erb
<% if #video.genre.present? %>
<%= #video.genre.name %>
<% else %>
No Genre Present
<% end %>
Instead, I can just do this (which looks much tidier)
# views/videos/show.html.erb
<%= #video.genre_name %>
However, it doesn't feel right asking for information about the genre in the Video model. What's the best way to organise this code? Should I be using helpers instead?
If you find yourself doing this kind of thing a lot, it may be worth looking into a decorator pattern, which can house view logic like this. (I quite enjoy using Draper for this purpose, but it's not very difficult to roll your own naive implementation.)
Then, your decorator logic can look like this:
class VideoDecorator
def genre_name
object.genre.try(:name).presence || "Fallback"
end
end
You can wrap up the model in a decorator as you render in the controller:
#video = Video.find(params[:id])
respond_with #video.decorate
And your view 'logic' (or lack thereof) can look like this application-wide:
<%= #video.genre_name %>
Thoughtbot has an excellent explanation of the decorator pattern here
You could write in your view
<%= #video.genre.try(:name) || 'No Genre Present' %>
If you don't need the fallback text, just
<%= #video.genre.try(:name) %>
Read more about Object#try here.
If you want the fallback also when name is an empty string (not just nil) you can use Object#presence
<%= #video.genre.try(:name).presence || 'No Genre Present' %>

Skinny controller passing hierarchical/nested data to view

I've come to appreciate the "skinny controllers" philosophy in Rails that says that business logic should not be in controllers, but that they should basically only be responsible for calling a few model methods and then deciding what to render/redirect. Pushing business logic into the model (or elsewhere) keeps action methods clean (and avoids stubbing long chains of ActiveRecord methods in functional tests of controllers).
Most cases I've run across are like this: I have three models, Foo, Bar, and Baz. Each of them has a method or scope defined (call it filter) that narrows down the objects to what I'm looking for. A skinny action method might look like:
def index
#foos = Foo.filter
#bars = Bar.filter
#bazs = Baz.filter
end
However, I've run into a case where the view needs to display a more hierarchical data structure. For example, Foo has_many bars and Bar has_many bazs. In the view (a general "dashboard" page), I'm going to display something like this, where each foo, bar, and baz has been filtered down with some criteria (e.g. for each level I only want to show active ones):
Foo1 - Bar1 (Baz1, Baz2)
Bar2 (Baz3, Baz4)
-----------------------
Foo2 - Bar3 (Baz5, Baz6)
Bar4 (Baz7, Baz8)
To provide the view with the data it needs, my initial thought is to put something crazy like this in the controller:
def index
#data = Foo.filter.each_with_object({}) do |foo, hash|
hash[foo] = foo.bars.filter.each_with_object({}) do |bar, hash2|
hash2[bar] = bar.bazs.filter
end
end
end
I could push that down to the Foo model, but that's not much better. This doesn't seem like a complex data structure that merits factoring out into a separate non-ActiveRecord model or something like that, it's just fetching some foos and their bars and their bazs with a very simple filter applied at each step.
What is the best practice for passing hierarchical data like this from a controller to a view?
You could get #foos like this:
#foos = Foo.filter.includes(:bars, :bazs).merge(Bar.filter).merge(Baz.filter).references(:bars, :bazs)
Now your relation is filtered and eager loaded. The rest of what you want to do is a concern of how you want it presented in the view. Maybe you'd do something like this:
<% Foo.each do |foo| %>
<%= foo.name %>
<% foo.bars.each do |bar| %>
<%= bar.name %>
<% bar.bazs.each do |baz| %>
<%= baz.name %>
<% end %>
<% end %>
<% end %>
Any kind of hash-building in the controller is unnecessary. The level of abstraction you're working with in the view is reasonable.
One of the widely accepted best practices for this kind of thing if Extracting a Form object. Bryan Helmkamp from Code Climate has written a very good blog post about this:
http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/
See section three "Extract Form Objects".
The thing is, yes, you should definitely move business logic out of your controllers. But it does not belong in your data models (Activerecord models) either. You will want to use a combniation of number 2, "Extract Service Objects", and 3, "Extract Form Objects", in order to build a good structure for your app.
You can also watch a good video of Bryan explaining these concepts here: http://www.youtube.com/watch?v=5yX6ADjyqyE
For more on these topics, it is also highly recommended that you watch Confreaks conference videos: http://www.confreaks.com/

Where should I put this, in the model, a helper, or a controller?

I have a view that I call school.html.erb and in that view I have Ruby code that calculates the average rating for that school.
Like this:
<span class="label label-info">
<%= #school.reviews.average(:rating).round(1) unless #school.reviews.blank? %>
</span>
I like to move this somewhere else, should I put this in the model, a helper or in a controller. And if I do that how can I call it from the view.
I have the following models: User, Review and School.
Calculating the average belongs in the model:
class School < ActiveRecord::Base
...
def average_review_rating
return nil if reviews.blank?
reviews.average(:rating)
end
end
Rounding the average belongs in the view, because it is formatting. Put calculation in the model, and formatting in the view (or a helper).
<%= #school.average_review_rating.round(1) if #school.average_review_rating %>
This can be shorted considerably using the andand gem.
<%= #school.average_review_rating.andand.round(1) %>
You may wish to push the rounding into the helper, where it can be independently tested:
class SchoolHelper
def format_rating(n)
n.andand.round(1)
end
end
which is used like this:
<%= format_rating(#school.average_review_rating) %>
I think I makes a lot of sense to put it into a model as a method.
class School
def avg_rating
reviews.average(:rating).round(1) unless reviews.blank?
end
end
Why it makes sense? Well, school rating is something you'll likely have to access in plenty of other places: other views, other models, etc. By putting it into a method, you may it look like just a property of School. school.avg_rating is pretty much saying for itself: "School, what's your rating?"
Why view won't do? You may want to access it in other views. There are helpers for that, right? But why helpers won't do? You may also want to access it from other models. Helpers aren't meant to calculate data, they're meant to format it and do other similar stuff.
Put in in the model (School):
def reviews_average
reviews.average(:rating).round(1) unless reviews.blank?
end
View:
<%= #school.reviews_average %>
Reason: Do not put DB intensive procedures into the view because the rendering speed depends on it.
I would say it works best in the model, because it's working with the data. This kind of follows the "fat model, skinny controller" principle.
Another rational option would be a decorator or presenter, using a library like Draper.

How to make the view simpler, the controller more useful?

This question relates to cleaning up the view and giving the controller more of the work.
I have many cases in my project where I have nested variables being displayed in my view. For example:
# controller
#customers = Customer.find_all_by_active(true)
render :layout => 'forms'
# view
<% #customers.each do |c| %>
<%= c.name %>
<% #orders = c.orders %> # I often end up defining nested variables inside the view
<% #orders.each do |o| %>
...
<% end %>
<% end %>
I am fairly new to RoR but it seems that what I'm doing here is at odds with the 'intelligent controller, dumb view' mentality. Where I have many customers, each with many orders, how can I define these variables properly inside my controller and then access them inside the view?
If you could provide an example of how the controller would look and then how I would relate to that in the view it would be incredibly helpful. Thank you very much!
I don't think there is anything drastically wrong with what you're doing. Looping through the customers and outputting some of their attributes and for each customer, looping through their orders and outputting some attributes is very much a view-oriented operation.
In the MVC architecture, the controller has responsibility for interacting with the model, selecting the view and (certainly in the case of Rails) providing the view with the information it needs to render the model.
You might consider extracting the code into a view helper though, if you have that exact code repeated more than once. You could even genericize it, passing in the name of a model and association. I haven't tested it, but you should be able to do something like this:
def display_attributes(models, association, attribute, association_attribute)
content = ''
models.each do |m|
content << "<p>#{m.attribute}</p>"
associated_models = m.association
associated_models.each do |am|
content << "<p>#{am.association_attribute}</p>"
end
end
content
end
Then in the view, you could use the helper like this:
<%= display_attributes(#customers, orders, name, name) %>
Obviously you would change the HTML markup within the helper method to suit your requirements. Note that if you're not using Rails 3 then you'll want to escape the output of the attribute names in the helper method.
I don't think there's anything wrong with your code. I'd just suggest for you to use a :include in your find
#customers = Customer.find_all_by_active(true, :include => :orders)
to reduce the number of queries.
I see nothing wrong with the code as you showed.
You are mixed up about the "intelligent controller, dumb view" approach though, i tend to prefer the "skinny controller, fat model", so indeed the view should be dumb, but you put the intelligence inside your model, and your helpers (or use a presenter), but definitely not in the controller.

Resources