How to reduce number of calls to a helper method - ruby-on-rails

In my view, I need a User object to display a few different properties. There is an instance variable #comments that's being sent from the controller. I loop through the comments and get the User information through a helper method in order to reduce db calls.
Here is the helper method:
def user(id)
if #user.blank? == false && id == #user.id
return #user
else
return #user = User.find(id)
end
end
And in the view, I display the details as follows:
<h4> <%=user(comment.user_id).name%> </h4>
<p><%=user(comment.user_id).bio%></p>
<p><%=user(comment.user_id).long_bio%></p>
<p><%=user(comment.user_id).email%></p>
<hr>
<p><%=user(comment.admin_id).bio%></p>
<p><%=user(comment.admin_id).long_bio%></p>
<p><%=user(comment.admin_id).email%></p>
I was told that assigning a variable in the view is bad practice and hence I am calling the helper method multiple times instead of assigning the returned User object.
Is there a better way to do this?

I think you are overcomplicating things here.
Let's say you have a user model
class User < ActiveRecord::Base
has_many :comments
end
an admin model
class Admin < User
end
a comment model
class Comment < ActiveRecord::Base
belongs_to :user
end
Now you only need a type column in your users table and you can do things like this:
Admin.all (All users with type "Admin")
User.all (Really all users including type "Admin" and all other types)
and for every comment you can just use
comment.user.bio
and it doesn't matter if it's an admin or not.
See http://www.therailworld.com/posts/18-Single-Table-Inheritance-with-Rails for example
Additional info: To reduce db calls in general(N+1 queries) watch http://railscasts.com/episodes/372-bullet

It's perfectly fine to pass models to your view and build the data on the view off of the data contained in the model. Keep in mind that I'm not entirely certain how you want your page to work, but one option you may have is to use a partial view and pass it the user object. This allows you to still only have the one model in your partial view without setting additional variables.
Also, without knowing what kind of database you're using or if your models have any associations, and assuming that you're doing some input validation, you may not need this helper method and may be able to lean on your ORM to get the user object.
For Example:
<%= comment.user.age %>
This isn't any more efficient than what you've currently got, but it certainly makes the code look cleaner.
Another alternative: set a user variable in the view. You're not performing logic in your view at this point, you're simply storing some data to the heap for later use.

Related

Rails 4 - coding through models

I have a user model, with a separate profile model. Each user has a profile. I then have 8 models for things within a profile (for example, each profile has a dashboard, feedback and publications). The profile belongs_to user and the dashboard etc belongs_to profile.
I am creating a profile view and would like to know how I write the line of code that will collate relevant information from the other models to display in the profile.
For example,the profile will be displayed with the name of its owner (which is stored in the user model). It will also have feedback stored in the feedback model. Is there a way to write that the profile view should display the user.first_name user.last_name, and user.feedback?
You can chain calls through Profile, like so:
#profile.user.first_name
But this violates a principle known as the “Law of Demeter”. There's a complex definition, but suffice to say that when you are accessing one object (User) through another (Profile), you begin to violate this law. It's not a huge deal when you're accessing user properties through the profile, necessarily, but things get messy quickly:
#dashboard.profile.feedback.order(:rating).where(user: #dashboard.profile.user)
Gross. And brittle, too. When you need to compose multiple models into a single view, there's a better pattern known as a Decorator. The job of a decorator is to give you a single object that appropriately collects data from the models for presentation, without tying your view code directly to your models. For example:
class DashboardDecorator
def initialize(dashboard, profile, user)
#dashboard = dashboard
#profile = profile
#user = user
end
def full_name
"#{#user.first_name} #{#user.last_name}"
end
def feedback_count
#profile.feedback.count
end
def days_since_last_post
Date.today - #dashboard.last_login
end
end
# /app/controllers/dashboard_controller.rb
def show
# ...
#dashboard = DashboardDecorator.new(dashboard, profile, current_user)
end
Then your view can access the data through the decorator:
<%= #dashboard.full_name %>
While you can write your own decorators like above, things get pretty tedious pretty quick. If you like to automate some of these parts, you should check out Draper, a handy gem that makes creating decorators a little easier, especially when your decorator methods map 1:1 with model methods.

Rails forms - Should I build `accepts_nested_attributes_for` associations in the Controller, Model, or View?

The Question
I have a parent that accepts_nested_attributes_for a child. So, when I have a form for the parent, I need to build the child so I can display form fields for it as well. What I want to know is: where should I build the child? In the Model, View, or Controller?
Why I Am Asking This
You may be shaking your head and thinking I'm a madman for asking a question like this, but here's the line of thinking that got me here.
I have a Customer model that accepts_nested_attributes_for a billing_address, like so:
class Customer
belongs_to :billing_address, class_name: 'Address'
accepts_nested_attributes_for :billing_address
end
When I present a form for a new Customer to the user, I want to make sure there is a blank billing_address, so that the user actually sees fields for the billing_address. So I have something like this in my controller:
def new
#customer = Customer.new
#customer.build_billing_address
end
However, if the user doesn't fill out any of the billing_address fields, but tries to submit an invalid form, they will be presented with a form that no longer has fields for the billing_address, unless I put something like this in the create action of my controller:
def create
#customer = Customer.new(params[:customer])
#customer.build_billing_address if #customer.billing_address.nil?
end
There is another issue, which is that if a user tries to edit a Customer, but that Customer doesn't have an associated billing_address already, they won't see fields for the billing_address. So I have to add somethign like this to the controller:
def edit
#customer = Customer.find(params[:id])
#customer.build_billing_address if #customer.billing_address.nil?
end
And something similar needs to happen in the controller's update method.
Anyway, this is highly repetitive, so I thought about doing something in the model. My initial thinking was to add a callback to the model's after_initialize event, like so:
class CustomerModel
after_initialize :build_billing_address, if: 'billing_address.nil?'
end
But my spidey sense started tingling. Who's to say I won't instantiate a Customer in some other part of my code in the future and have this wreak havoc in some unexpected ways.
So my current thinking is that the best place to do this is in the form view itself, since what I'm trying to accomplish is to have a blank billing_address for the form and the form itself is the only place in the code where I know for sure that I'm about to show a form for the billing_address.
But, you know, I'm just some guy on the Internet. Where should I build_billing_address?
Though this advice by Xavier Shay is from 2011, he suggests putting it in the view, "since this is a view problem (do we display fields or not?)":
app/helpers/form_helper.rb:
module FormHelper
def setup_user(user)
user.address ||= Address.new
user
end
end
app/views/users/_form.html.erb:
<%= form_for setup_user(#user) do |f| %>
Note that I had to change the helper method to the following:
def setup_user(user)
user.addresses.build if user.addresses.empty?
user
end
The controller remains completely unchanged.
If you know your model should always have a billing address, you can override the getter for this attribute in your model class as described in the docs:
def billing_address
super || build_billing_address
end
Optionally pass in any attributes to build_billing_address as required by your particular needs.
You would use build if you want to build up something and save it later. I would say, use it in nested routes.
def create
#address = #customer.billing_addresses.build(params[:billing_address])
if #address.save
redirect_to #customer.billing_addresses
else
render "create"
end
end
Something like that. I also use the build when I'm in the console.
You have to remember the principles of MVC, which is to create DRY(don't repeat yourself) code, which is efficiently distributed between the various moving parts of the app
accepts_nested_attributes_for Is Great For Keeping Things DRY
accepts_nested_attributes_for is a model function which allows you to pass data through an association to another model. The reason why it exists is to give you the ability to populate another model's data based on a single form, and is excellent for extending functionality without too much extra code
The problem you're citing is that if you want to use the code in other areas of the app, you'll end up having all sorts of problems
My rebuttal to that is in order to create as efficient an application as possible, you want to write as little code as possible - letting Rails handle everything. The accepts_nested_attributes_for function does allow you to do this, but obviously has a cost, in that you have to accommodate it every time you want to use it
My recommendation is to use what you feel is the most efficient code you can, but also keep to conventions; as this will ensure speed & efficiency
You should handle all these scenarios in controller, since it is not a responsibility of model.
Just in terms of keeping things DRY, you can write a method,
def build_customer(customer)
customer.build_billing_address if customer.billing_address.nil?
#add more code if needed
end
And inside controller you can call this method wherever it is needed. e.g.
def create
#customer = Customer.new(params[:customer])
if #customer.save
redirect_to #customer.billing_addresses
else
build_customer(#customer)
render "new"
end
end

Rails: MVC How to use custom actions

I have a table output from entries using the rails generated scaffold: CRUD ops.
If I want to make another action on the table like the default "Show, Edit, Destory" like a library book "check in", that will update the status to "checked in"...
What would be the proper way to use the model and controller to update? (Using mongodb)
Better stated: What's the best way to have many custom actions? Think of it like many multi purpose "Facebook Likes".
On the table, list of actions "Punch this", "Check out this"...
There are lots of ways to handle this, but I typically like to isolate actions like this in their own controller action with it's own route.
Model
To keep things tidy I recommend adding a method to the model that updates the attribute you are concerned about. If you aren't concerned with validation you can use update_attribute. This method skips validations and saves to the database
class LibraryBook < ActiveRecord::Base
def check_in!
self.update_attribute(:checked_in, true)
end
end
View
You'll need to update the index.html.erb view to add the link to update the individual record. This will also require adding a route. Since you are updating the record you will want to use the PUT HTTP verb.
routes.rb
resources :library_books do
match :check_in, on: :member, via: :put # creates a route called check_in_library_book
end
index.html.erb
Add the link
link_to check_in_library_book_path(library_book), method: :put
Controller
Now you need to add the action within the controller that calls the #check_in! method.
class LibraryBooksController < ApplicationController
def check_in
#library_book = LibraryBook.find(params[:id])
if #library_book.check_in!
# Handle the success
else
# Handle the Failure
end
end
end
In my opinion, the best way to handle status workflows like this is to think about it in terms of events, and then just think of status as most recent event. I usually create an event_type table with a name and code (so, e.g. Check In and CHECK_IN for name and code, respectively), and then an event table with an event_type_id, timestamp, and usually some kind of user id, or IP address, or both.
Then you could say something like this:
class Thing < ActiveRecord::Base
has_many :events
def status
events.order("created_at DESC").first.event_type.name
end
end
There are also "audit trail" gems out there, but in my (limited) experience they aren't very good.
This doesn't speak to MongoDB, and may in fact be incompatible with Mongo, but hopefully it at least points you in the right direction or gives you some ideas.

Best MVC approach when passing objects to a view from a controller? (Rails)

I have three tables: User, Task, and UserTask (which has User and Task as foreign keys, no other columns). An entry in the UserTask table may or may not exist for a particular User/Task combination (UserTask records are created as needed for scalability reasons).
In my view, I'd like to show all Tasks for the currently logged in User. For each Task, I want to display the text "yes" if a corresponding record exists in the UserTask table; otherwise I want to display "no". But I'm a bit unsure of what objects I should pass to the view from the controller, and how I should interact with objects (i.e. what I am allowed to do) once they are passed to the view.
Should I pass the User, Tasks, and UserTasks as separate variables from the controller to the view? This way, I can show all Tasks by simply iterating through the Tasks variable, and for each Task I can use a where (ActiveRecord query) in the view to determine whether there is a UserTask for each Task. Thing is, is it okay to do where and find queries on object passed into a view? I always assumed you leave that to the controller and only do simple things in the view, like iteration over objects passed to the view (correct me if that is wrong).
Or should I build a Hash in the controller where each key is a Task and each value is a UserTask, and then pass that Hash to the view? This way, I can show all Tasks in the view by simply iterating through all of the keys, and for each Task key I can display "yes" or "no" by checking whether the value is null or not.
Or is there another way (i.e. joining tables or something... not sure how that would work)?
First, you should really read up on associations in rails:
http://guides.rubyonrails.org/association_basics.html
The way you intend to set up the relationship between users and tasks strikes me as a bit odd, but putting that aside, to implement it as you've described it you would define associations on your User, Task and UserTask models:
class User < ActiveRecord::Base
has_many :user_tasks
end
class Task < ActiveRecord::Base
has_one :user_task
end
class UserTask < ActiveRecord::Base
belongs_to :user
belongs_to :task
end
In your controller:
#user = User.includes(:user_tasks).find(...)
#tasks = Task.all
Then, in your view (in haml):
-#tasks.each do |task|
=(#user.user_tasks.include?(task)) ? "yes" : "no"
EDIT: Updated according to explanation in comments.

Rails 3 has_many, manual assignment in controller

I have a form that is set up to create an object (Device), and the Device has_many Abilities.
I can set this up fairly easily with a form where the Abilities are user selectable (e.g. with checkboxes). However, I would like to set these up in the controller, since they will not be user selectable (they will be based on some other parameters passed when the Device is created, e.g. the device type).
How can I set up the controller so that I can essentially hard-wire the has_many relationships based on the type of device that is created?
Thanks!
EDIT: Renames Actions to Abilities to avoid confusion
You can set associations like this using some of the methods Rails automatically defines when you set up the association. Basically, you should be able to go
def action_name
#other action code
#device = Device.find(params[:id]) #or however you get it
#if you want to grab abilities using SQL:
#device.abilities = Ability.where('some conditions')
#or if you have a particular group:
#device.abilities = [ability_1, ability_2]
#device.save!
end

Resources