Render a partial based on model attribute - ruby-on-rails

Just wondering if there is a way to make the following code more concise
<% #users.each do |user| %>
<%= render partial: "#{user.state}_user", locals: { user: user } %>
<% end %>
Each user has a state (either active or inactive) and, for each user, there correct partial (_active_user.hmtl.erb or _inactive_user.html.erb needs to be rendered).
Looking for using collection but I can't find any sample.

You could override ActiveModel::Conversion#to_partial_path which is how render looks up the partial implicitly:
class User < ApplicationRecord
# ...
def to_partial_path
"users/#{state}_user"
end
end
But that will override how its rendered implicitly everywhere which may not be desirable. Otherwise I would question if you really need to make this more succinct - what you are doing is off the rails and its better to have code that clearly shows its intent instead of something overly clever / force DRYied.
The reason you haven't found any examples using collection is that its not going to work. It iterates through the collection and calls #to_partial_path on each member.

Related

Is it ok to call ActiveRecord Methods in the view Rails

Using Rails 4
I am wondering (and having a hard time finding an answer) if it is OK to call an ActiveRecord method directly from the view, such as:
<%= Article.where(approved: true).count %>
or
<%= Article.where("short_answer is NOT NULL and short_answer != ''").count %>
I realize the normal practice would be to store these in an instance variable inside of the controller, but since I am using a partial, I cannot do that.
Is doing this ok? Can it hurt? Is there a better way to go about this (such as a helper method)? Thank you!
Is doing this ok? Can it hurt?
It's definitely okay, but the problem is that you'll be calling another db query - which is the most "expensive" part of a Rails app.
#instance_variables are set once, and can be used throughout the view:
#app/views/articles/show.html.erb
#Referencing #article references stored data, not a new DB query
<%= #article.title %>
<%= #article.description %>
<%= #article.created_at %>
Because the above all uses the stored #article data, the database is only hit once (when #article is created in the controller).
If you call AR methods in the view, you're basically invoking a new db call every time:
#app/views/articles/show.html.erb
#Bad practice
<%= Article.select(:name).find(params[:id]) %>
<%= Article.select(:description).find(params[:id]) %>
<%= Article.select(:created_at).find(params[:id]) %>
To answer your question directly, you would be okay to call that data IF you were only counting database-specific data.
IE if you were trying to count the number of #articles, you'd be able to call #articles.size (ActiveRecord: size vs count)
The prudent developer will determine which data they have in their controller, and which they need to pull from the db... doing all their db work in the controller itself:
#app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
def index
#articles = Article.where(approved: true) #-> could use a scope here if you wanted
end
end
#app/views/articles/index.html.erb
<%= #articles.size %>
Nithin's answer is great but won't get past the consideration that you have to determine whether you need to call the db explicitly, or use already-invoked data.
Finally, in regards to using a partial, if you have to pass that data every time, you may wish to use some sort of conditional data to determine whether you need to call the db or not:
#app/views/shared/_partial.html.erb
<% approved ||= Article.approved_articles.size %>
<% short ||= Article.short_answer_presence.size %>
This will allow you to set locals IF you want, and also have "defaults" set if they aren't set.
You should mostly do
class Article < ActiveRecord::Base
....
scope :approved_articles, where(approved: true)
scope :short_answer_presence, where("short_answer is NOT NULL and short_answer != ''")
end
In your controller method
#approved_articles_count = Article.approved_articles.count
#short_answer_presence_count = Article.short_answer_presence.count
and use those variables in view.
In case of partials, as said my Raman you can do that.
<%= render partial: "form", locals: {approved_articles_count: #approved_articles_count, short_answer_presence_count: #short_answer_presence_count} %>
You can always pass these variables inside a partial using locals:
<%= render partial: "form", locals: {zone: #zone} %>
Its always a good practice to define the instance variables in controller, it does not hurt but you don't end up doing business logic inside a view.

'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' %>

How to Handle Rails/Mongoid with no doc

When calling all posts for a user Posts.find(creator: current_user:_id), and the user hasn't made any...rails spits a "NoMethodError" for it...
What I want to do is have a pretty output for the user of "Why, no. You haven't posted anything, you lazy slob." instead of this scary error.
What's the best way to handle things like this?
You need to use where instead of find. By design, find method expect to actually find an existing thing you're looking for. Consult docs about querying here.
Also, you can try and use try method. Basically it's equal to the following:
object.try(:something_scary)
# is equal to
object && object.something_scary
This is how I handle nil entities. If you want to show some kind of message to user (about being slobby) you make a check inside of your template and render different partials. Example:
<% if #posts.present? %>
<%= render 'posts' %>
<% else %>
<%= render 'no_posts' %>
<% end %>
Then you can put your message inside of that no_posts partial.

Can View call some Model methods in Rails?

First let me explain an example:
In Model:
class Product < ActiveRecord::Base
has_many :line_items
def income
self.line_items.sum(:price)
end
def cost
self.line_items.sum(:cost)
end
def profit
self.income - self.cost
end
end
Then in Controller:
def show
#products = Product.all
end
And in View:
<% #products.each do |product| %>
Product Name: <%= product.name %>
Product Income: <%= product.income %>
Product Cost: <%= product.cost %>
Product Profit: <%= product.profit %>
<% end %>
Is it a good practice to call model methods from view?
When I searched for that, I found many people saying it is NOT a good practice to ever call model methods or access DB from views.
And on the other hand, some others said that don't call class methods or any method updates the DB from view but you can access any method that only retrieve data.
Then, is this code a good practice?
Its perfectly fine to call the object-methods/attributes from the view, as long as the call would not change the data. I mean, call readers/getters. A Bad practice would be to call/invoke methods that update/delete the data. Don't call setters.
Also, if there is any complex computation involved, resort to helpers.
Since your methods need to access line_items association, to avoid N+1 problem and calling DB queries from view, I'd advice fetching your line_items in show action, with includes:
def show
#products = Product.includes(:line_items)
end
With this adjustment, I think it's ok to call these methods in view.

ActiveRecord - how to do includes after the query was executed?

In a scenario with 1->N->N assocations. For example: Post->Comments->Votes (votes will be list of names of people who voted on the comment). To display a page the query with includes might look like:
#post = Post.where(:id => 100).includes({:comments => :votes}).first
I am starting to add caching support. Which means if the comments partial is already cached I will not need to run include the comments/votes all the time. So I wonder if there is a way to make the code appear like:
# controller
#post = Post.find(100)
# view
<% cache('comments', #post.last_comment_time do %>
<% #post.includes({:comments => :votes}).comments.each do |comment| # ???? %>
<% end %>
Running the "post-query" includes, will "fill in" the associations. So #post.comments will be populated and each comment will include all the votes. Is there a way to achieve this?
P.S. I am aware the view is not the best place to run the query, this is just an example.
in latest releases of rails, all the finder-methods return a proxy object, that will only trigger a database-call once you send it some iterator-method like all or first in your case. this is why you can chain all the calls like Post.where.order.sort.bla.
it's not possible though to load the post model and use an includes call later. includes works by using a join call on the relations that get loaded with the model instance, so that you have just one database-call instead of one for each relation.
executing active_record code in your view is also a bad practice. the data-retrieval is the responsibility of the controller, not the view.
This is a fairly old question but this can be done now like this
# controller
#post = Post.find(100)
# view
<% cache('comments', #post.last_comment_time do %>
<% ActiveRecord::Associations::Preloader.new.preload #post, comments: :votes # this will trigger one query %>
<% #post.comments.each do |comment| # this will not trigger any additional queries %>
<% end %>
Not the cleanest way but it does the job

Resources