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.
Related
In my Rails app, I have a store with products and users. Both of those have models and controllers.
What I want to achieve is to show on a view template a product to a current_user based on the attribute from a model that he has. For an example if a user has "Female" attribute from user model, and then to show some products related to this attribute. How can I achieve this?
These are my product views where all products are showed:
<% #products.each do |product| %>
<%= render "product_row", product: product, order_item: #order_item %>
<% end %>
_product_row.html.erb
<h4><%= product.name %></small></h4>
<div class="image">
<%= image_tag product.image.url(:original), class: "img-responsive" %></div>
<p>Some description.</p>
Add to Package
It sounds like you need to retrieve objects from your product class based on an attribute from the User class. Sounds like a basic service object or just a method on the user:
class User < ActiveRecord::Base
def products_for_gender
if gender == 'female'
Product.where("do some logic here based on female")
elsif gender == 'male'
Product.where("do some logic here based on male")
else
#do some other logic just in case it's nil
end
end
end
then in your controller you do this
#products = current_user.products_for_gender
in your view you then render a list with those products. This prevents you from putting logic in your view, which is rarely a good idea.
Also there's more abstraction possible, the if statement is not the prettiest, but this will cover your issue i believe. Eventually you could look into using service objects for example, https://blog.engineyard.com/2014/keeping-your-rails-controllers-dry-with-services
How are you saving the users model. If you are using devise then you have current_user helper method available in your views and you can use that to get the curren_user.gender attribute and show the view based on this
I'm adding a new model to my equasion and I'm wondering if there is a way to associate two models into one model then display any/all results within a view. For example, here is what I've currently have;
#tweet_category.order("position").each do |tweet|
<%= tweet.title %>
end
just a short example... now what if I added facebook into this. I was first thinking of creating a model thats named stuff then associate it to tweet_category and facebook_category like so;
class Stuff < ActiveRecord::Base
attr_accessible :title
belongs_to :user
has_many :tweet_category
has_many :facebook_category
end
Now in my controller I'm guessing I would do the following;
class StuffController < ApplicationController
def index
#stuff_list = Stuff.find(:all)
end
end
and in my view I would just simply do the following from above view;
#stuff_list.order("position").each do |stuff|
<%= stuff.title %>
end
am I understanding the logic here??? would that work having two models / two tables db.. etc..
First of all, I don't understand why you would need that "stuff" model. It belongs to users and has_many tweet_category and facebook_category, and just does nothing but offering a "title", when your User model could do the job ( I mean, each user could have many tweets and fb category, instead of having one or several "stuff" which has/have many of them ).
Anyway, if you want to make links between your models and then display everything in a view, first in your User model you just have to do :
class User < ActiveRecord::Base
...
has_many :facebook_categories #( I don't know how rails would pluralize it, btw, I'm just making an assumption )
has_many :tweeter_categories
end
and
class Facebook_category
...
belongs_to :user
end
and do the same fot the tweeter category
Then in your controller :
def show_everything #Here it's a custom action, but you can call it wherever you want
#users = User.all
end
And finally in your view :
<% #users.each do |user| %>
<% user.facebook_categories.all.each do |fb_c| %>
<%= fb_c.title %>
<% end %>
<% user.tweeter_categories.all.each do |t_c| %>
<%= t_c.title %>
<% end %>
<% end %>
Maybe just try to grab a better name for your models, so the pluralization doesn't get messy ( and I saw that the ".all" method is deprecated, so maybe replace it with something
Hope it helps !
Edit :
Basically, when you're doing
#users = User.all
What rails' doing is putting every hash defining every "User" in an array. So, if you want to mix two tables' arrays inside a single array, you can do something like this :
#categories = [] << Facebook_category.all, Tweeter_category.all
You will then have an array ( #category ), filled with 2 arrays ( one ActiveRecord relation for Facebook_category and one for Tweeter_category ). Themselves filled with hashes of their model. So, what you need to do is :
#categories.flatten!
Here's the API for what flatten does ( basically removing all your nested arrays inside your first tarray )
Now, you got a single array of hashes, being the informations from both your model's instances. And, if these informations can be ordered, in your view, you just have to :
<% #categories.order("updated_at").each do |i| %>
<%= i.title %>
<% end %>
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.
I'm building a martial arts related database, currently I have the following associations set up:
Student has_and_belongs_to_many :styles
Style has_many :ranks
Student has_many :ranks, through: :gradings (and vice versa)
I'm generating a form as follows, depending on the student's styles:
So the headings are generated by the Style model (Tai Chi, Karate...), then their rankings listed below (taken from the Rank model), and the "Dojo" and "Date" fields should belong to the Grading model once created.
The question: I know how to build a form that creates one association (or one association + its children), but how do I build a form that creates multiple associations at once?
Also, what would be a clean way to implement the following:
Only lines which are ticked become associations
Dojo and date must be filled in for ticked lines to save successfully
If a line is unticked it will destroy any previously created associations
This is what I've currently implemented to retrieve the correct records:
class GradingsController < ApplicationController
before_filter :authenticate_sensei!
def index
#student = Student.includes(:styles).find(params[:student_id])
#ranks = Rank.for_student_styles(#student)
split_ranks_by_style
end
private
def split_ranks_by_style
#karate = #ranks.select_style("Karate")
#tai_chi = #ranks.select_style("Tai Chi")
#weaponry = #ranks.select_style("Weaponry")
end
end
# Rank model
def self.for_student_styles(student)
includes(:style).where("styles.id in (?)", student.styles.map(&:id))
end
def self.select_style(style)
all.map { |r| r if r.style.name == style }.compact
end
Complicated forms like this are best handled in a service object initiated in the primary resource's create or update action. This allows you to easily find where the logic is happening afterwards. In this case it looks like you can kick off your service object in your GradingsController. I also prefer formatting a lot of the data in the markup, to make the handling easier in the service object. This can be done a'la rails, by passing a name like "grade[style]" and "grade[rank]". This will format your params coming in as a convenient hash: {grade: {style: "karate", rank: "3"}}. That hash can be passed to your service object to be parsed through.
Without really grasping the full extent of your specific requirements, let's put together an example form:
<%= form_for :grading, url: gradings_path do |f| %>
<h1><%= #rank.name %></h1>
<%- #grades.each do |grade| %>
<div>
<%= hidden_field_tag "grade[#{grade.id}][id]", grade.id %>
<%= check_box_tag "grade[#{grade.id}][active]" %>
...
<%= text_field_tag "grade[#{grade.id}][date]" %>
</div>
<%- end %>
<%= submit_tag %>
<%- end %>
With a form like this, you get your params coming into the controller looking something like this:
"grade"=>{
"1"=>{"id"=>"1", "active"=>"1", "date"=>"2013-06-21"},
"3"=>{"id"=>"3", "date"=>"2013-07-01"}
}
Nicely formatted for us to hand off to our service object. Keeping our controller nice and clean:
class GradingsController < ApplicationController
def index
# ...
end
def create
builder = GradeBuilder.new(current_user, params['grade'])
if builder.run
redirect_to gradings_path
else
flash[:error] = 'Something went wrong!' # maybe even builder.error_message
render :action => :index
end
end
end
So now we just need to put any custom logic into our builder, I'd probably recommend just making a simple ruby class in your /lib directory. It could look something like this:
class GradeBuilder
attr_reader :data, :user
def self.initialize(user, params={})
#user = user
#data = params.values.select{|param| param['active'].present? }
end
def run
grades = data.each{|entry| build_grade(entry)}
return false if grades.empty?
end
private
def build_grade(entry)
grade = Grade.find(entry['id'])
rank = grade.rankings.create(student_id: user, date: entry['date'])
end
end
There will obviously need a lot more work to pass all the specific data you need from the form, and extra logic in the GradeBuilder to handle edge cases, but this will give you a framework to handle this problem in a maintainable and extensible way.
To start, I'm a pretty new to Rails
I've created some methods and put them into my model, but it looks messy and just wondered if the code belongs in the model or the controller? What makes my code unique (not one model per controller anyhow) is that I have only one model "Products" but have 3 controllers that interact with it, "Merchants, Categories, Brands". Maybe there is an easier way I have completely overlooked?? I don't really want to split the data up into 3 tables / models with links between.
p.s. This is the first time I have slipped away from the comfort of a Rails book so please go easy on me! Any other general suggestions to my code will be much appreciated.
Product model
class Product < ActiveRecord::Base
validates :brand, :presence => true
def product_name
name.capitalize.html_safe
end
def product_description
description.html_safe
end
#Replace '-' with ' ' for nice names
def brand_name
brand.capitalize.gsub('-',' ')
end
def category_name
category.capitalize.gsub('-',' ')
end
def merchant_name
merchant.capitalize.gsub('-',' ')
end
#Replace ' ' with '-' for urls
def brand_url
brand.downcase.gsub(' ','-')
end
def category_url
category.downcase.gsub(' ','-')
end
def merchant_url
merchant.downcase.gsub(' ','-')
end
end
Merchants Controller
class MerchantsController < ApplicationController
def index
#merchants = Product.find(:all, :select => 'DISTINCT merchant')
end
def show
#products = Product.find(:all, :conditions => ['merchant = ?', params[:merchant]])
#merchant = params[:merchant].capitalize.gsub('-',' ')
end
end
Merchant view (index)
<h1>Merchant list</h1>
<%= #merchants.count%> merchants found
<% #merchants.each do |merchant| %>
<p><%= link_to merchant.merchant_name, merchant.merchant_url %></p>
<% end %>
Merchant view (show)
<h1>Products from merchant: <%= #merchant %></h1>
<%= #products.count%> products found
<% #products.each do |product| %>
<h3><%= product.product_name %></h3>
<p>
<img src="<%= product.image %>" align="right" alt="<%= product.product_name %>" />
<%= product.product_description %>
</p>
<p><%= product.price %></p>
<p>Brand: <%= product.brand_name %></p>
<p>Category: <%= product.category_name %></p>
<p>Sub category: <%= product.sub_category %></p>
<p>Merchant: <%= product.merchant_name %></p>
<p>More information</p>
<hr />
<% end %>
So your data model does seem to be getting to the point where you might at least want to split merchants out. You can tell this from the select 'DISTINCT merchant' query. If your merchants are user-based input and are saved inside your products table it seems like a good time to move them into their own model so that they are easily searchable and manageable. As you get more merchants and more products it will get harder and harder to perform this query. Once you want to add additional merchant information you'll be in a worse position as well. Just keep in mind Rails was made for easy refactoring. Making this change shouldn't be daunting, it should just be another regular task in your agile development process.
What the above change would also allow you to do is change these lines:
#products = Product.find(:all, :conditions => ['merchant = ?', params[:merchant]])
#merchant = params[:merchant].capitalize.gsub('-',' ')
into:
#merchant = Merchant.find_by_name(params[:name])
#products = #merchant.products
You could then have a capitalize and gsub name with a model function:
#merchant.display_name
The next step would be to DRY up your model code a little bit, for example:
class Product
def brand_name
make_name brand
end
def category_name
make_name category
end
def merchant_name
make_name merchant
end
private
def make_name name
name.capitalize.gsub('-', ' ')
end
end
You could do something similar to the _url functions as well. If you wanted to venture further you could clean this up using meta-programming as well.
Final Thoughts: make sure you actually want to be calling html_safe on your strings. If they are user based input it's best to let them go through the h function in your views. Do you want users to be able to enter HTML strings as brands, merchants and categories? If so, then leave the html_safe string there, otherwise let the strings be made html_safe in your views.
In general you are on the right path: Skinny Controllers and Views and Fat Models is the way to go. This means put your logic and your heavy-lifting into your Models and let your Controllers and Views be small and simple.
You should probably normalise your database. You need 3 tables instead of one: Products, Merchants and Brands. Your product table will then have references to merchant and brand tables. You can then have separate models (with belongs_to/has_many relationships between them) and separate controllers.
You will still be able to write things like product.merchant.name but some of your code will be simpler.
Conventions are just that, conventional. There is no right or wrong no matter who in Atlanta tells you that there are. F#$k him.
Anyways, if you are going with a Skinny Controller Fat model, then yeah, you're on the right track.
As they say, do all the heavy lifting in your model.
I'd look to refactor those methods personally in the model. All those places where you are calling *.downcase.gsub...
Also look into to_param, a method you can overwrite to get purdy urls.