I have three models. One is an Employee, one is an Item, and one is a Transaction that belongs to both Employee and Items. It's a simple app that allows Employees to check in and check out items - 'Transaction' has a boolean column for checked-in/checked-out.
What I'm trying to do is show within the employee/show view the current list of Items that an Employee has checked out. This is some rough code that I sketched out, but I'm not sure that it's going to work, and I was told not to use a lot of nested conditionals in my views anyway.
<% if #employee.transactions.exists? %>
<h3>Currently Checked-OUT Items</h3>
<table>
<tr>
<th>Item Asset Tag</th>
<th>Item Description</th>
</tr>
<% #employee.transactions.each do |transaction| %>
<% if item.transaction.last? && transaction.status == false %>
<tr>
<td><% transaction.assettag %></td>
<td><% transaction.description %></td>
</tr>
<% else %>
NO CHECKED OUT ITEMS
<% end %>
</table>
<% end %>
<% end %>
Basically, I'm trying to:
checks all employee transactions
compares the item involved in the transaction and sees if it's the .last transaction record for item
if it is, and if it's false, then it's a current checkout.
Is this a better job for a scope within the Transaction model, or a helper method? I've never used either, I'm really new at rails.
You should do a couple of things in here.
First - create a scope that will fetch last item transaction for you. There's no point in going through al item transactions if you're interested in the last one only, right?
Second, use partials. In this example it's hard to show how I would refactor code to use them (some things doesn't make sense here, ex. where does item variable come from?)
Scope example (take last transaction)
#item.transactions.order('created_at DESC').first
You can as well add scopes for checkin / checkout
class Transaction
scope :checkin, -> { where(status: true) }
scope :checkout, -> { where(status: false) }
end
First, you are on the right track. When views get ugly and hard to read because of extensive embedded ruby conditionals and such, think about moving the logic into a helper.
If you have a typical rails app, you'll already have app/helpers/application_helper.rb
So you could just create a helper in that file
def make_employee_list(employee)
if employee.transactions.exists?
content_tag(:div) do
content_tag(:h3, "Currently Checked-OUT Items")
content_tag(:table) do
employee.transactions.each do |transaction|
# you get the idea
end
end
end
end
end
Then in your view you could do this:
<%= make_employee_list(#employee) %>
Related
I have a model Schools and a model PerformanceStats.
PerformanceStat
belongs_to :school
School
has_one :performance_stat
the index page for PerformanceStat shows all 2,000 performance stats, and also the school.name, school.score, and school.city, and I need access to the school.id and school.slug.
Controller:
def index
#performance_stats=PerformanceStat.all
end
My view code:
<tbody>
<% #performance_stats.each do |stat| %>
<% school = School.find(stat.school_id)%>
<tr>
<td><%= link_to school.name, school_path(city: school.city.parameterize.truncate(80, omission: ''), slug: school.slug) %></td>
<td><%= number_with_precision(school.score, precision: 2)%></td>
then the view goes on to display the performance stats.
This view load very slowly....10-20 seconds. How can I speed things up? I've tried PerformanceStats.scoped, and plucking school stats and selecting from an array, but these don't seem to help. Is there a way for me to access the school attributes without finding a School for every PerformanceStat? I believe the School.find bit is slowing things down considerably.
I have indexes on :school_id in PerformanceStat, and :score, :slug in the School model.
UPDATE:
The suggestion in the selected answer to add a cache resulted in this line of code in the index action of the SchoolsController:
fresh_when etag: #performance_stats
The load time dropped to 18ms. This solution works great for me because the content of the index action does not change often. This data gets updated once a year. This link has other suggested cache solutions for data that changes frequently.
PerformanceStat.all is a heavy query if you've a lot of data in this table and it'll be finding school for each performance stat.
What I can understand from your code is that you're facing (N + 1) problem over here.
NOTE: you should not fire queries from your views or helpers and let the controller do all the action.
For instance in your code:
<% #performance_stats.each do |stat| %>
<% school = School.find(stat.school_id)%> <- #THIS IS WRONG & LET THE ASSOCIATIONS DO ALL THE ACTION ON ITS OWN
<tr>
<td><%= link_to school.name, school_path(city: school.city.parameterize.truncate(80, omission: ''), slug: school.slug) %></td>
<td><%= number_with_precision(school.score, precision: 2)%></td>
you can use includes, PerformanceStat.includes(:school) it will fetch all the schools for each PerformanceStat.
your controller code should be:
#performance_stats = PerformanceStat.includes(:school)
instead of : #performance_stats = PerformanceStat.all
and your view code will now be:
<% #performance_stats.each do |stat| %>
<% school = stat.school %> #make sure all stats have a school assigned to them otherwise you can put a check below whether the school is nil or not
<tr>
<td><%= link_to school.name, school_path(city: school.city.parameterize.truncate(80, omission: ''), slug: school.slug) %></td>
<td><%= number_with_precision(school.score, precision: 2)%></td>
Quite a few things here. First of all change your controller method to this one, otherwise you will run into n+1 queries
def index
#performance_stats=PerformanceStat.includes(:school)
end
Since you have eagerly loaded the school, now you can access it directly in your view as
<% stat.school %>
Secondly loading almost 2000 records in one go is not optimal at all, it's gonna take a while to load all records. For this you must add pagination by using following gems
kaminari
will_paginate
I get that one should not ping the database in the view... but wondering about the right solution. In one of my views, I need to pull info on an #order, it's child items, and also Amount, another model, based on each child item. Something like this:
<% #order.items.each do |item| %>
<td><%= item.name %></td>
<td><%= Refund.where(item_id:item.id).first.amount %></td>
<td><%= Amount.where(item_id: item.id).first.amount %></td>
<% end %>
For the sake of avoiding the db hits in the view, the only solution I've thought of is to create a huge hash of all the relevant data in the controller, which is then accessed from the view. So it would be something like this:
# controller (writing quickly, code may not be totally right, hopefully you get gist
data = Hash.new
data["items"] = []
#order.items.each do |item|
item_hash = {
"name" => item.name,
"amount" => Amount.where(item_id: item.id).first.amount,
"refund" => Refund.where(item_id:item.id).first.amount
}
data["items"] << item_hash
end
# view code
<% data["items"].each do |item| %>
<td><%= item["name"] %></td>
<td><%= item["refund"] %></td>
<td><%= item["amount"] %></td>
<% end %>
And I know SO hates this type of question... but I really need to know... is that the best solution? Or are there are best practices? The reason I ask is because it seems very clean in the view, but very bulky in the controller, and also it gets quite unwieldy when you have a much more complex set of nested tables, which is what I actually have (i.e., the data hash would be quite funky to put together)
First of I would use associations between item and the 2 other classes, so that you can do
item.refund
item.amount
Instead of Refund.where(...). You could further define methods such as
def refund_amount
refund.amount
end
And similarly for the other one (and hopefully come up with a better name than amount_amount.
This keeps both your view and controller clean but it won't be any faster. So far all of the approaches involve running 2 database queries per item which is the real issue as far as I'm concerned - whether those excess queries happen in the view or the controller is of lesser concern.
However you can avoid this with Active Record's include mechanism:
Item.include(:amount,:refund).where("your conditions here")
Will load the named associations in bulk rather than loaded them one at a time as each item is accessed.
This may sound strange, but none the less I want to learn how to do it and I need some help getting there. I'm not sure how to approach this. I'm hoping to get some dev love on this.... Let me explain by giving an example. (Btw thank you---you are awesome!)
Instead of this in my view:
<table>
#users.map do |user|
...
</table>
I want to extract it away into a helper that I can reuse for other collections.
So I want to say instead:
#users.to_table({
template: "simple_template",
header: ["Full Name","Email"],
column: ["name", "email"]
})
So in my application_helper I have something like this: (pseudo-ish code)
class ActiveRecord::Relation
def to_table *args
load args.template
self.map do |j|
args.header do |header|
j.header
end
args.column do |column|
j.column
end
end
end
end
I have no idea how to wire this up. (helper or table template) Definitely an order of magnitude above my current skill level. Need some serious direction.. I'm asking this because I feel like I hit a learning plateau and need help busting through to something more challenging (hence this question)... Hope it's clear, if not ask for clarification. Thanks for reading... Thanks for helping! =)
Not guaranteeing this will work it is just to show the syntax issues:
class ActiveRecord::Relation
def to_table(options={})
load options[:template]
self.map do |j|
Hash[
args[:headers].zip(args[:columns].map{ |column| j.send(column) }
]
end
end
end
Not Sure about the load part I think this should be handled outside of the relation as it is a view issue and has nothing to do with the ActiveRecord::Relation but this method will return an Array of Hashes like
[{"Full Name" => "USER 1 Name", "Email" => "USER1#email.com},{"Full Name" => "USER 2 Name", "Email" => "USER2#email.com"}]
In your current method args which is an array now based on the * will not respond to things like template or column. Like I said I have never really tried to implement anything in this way but the syntax change might get you headed in the right direction. Also handling should be put in place for when template is not passed or headers.count != columns.count.
Best bet is probably something like this
<%= render "template", obj: #user.to_table(headers: ["Full Name","Email"],columns: ["name", "email"]) %>
in _template.rb
<table>
<thead>
<tr>
<% obj.first.keys.each do |header|
<th><%= header %></th>
<% end %>
</tr>
</thead>
<tbody>
<% obj.each do |row|
<tr>
<% row.values.each do |cell| %>
<td><%= cell %></td>
<% end %>
</tr>
<% end %>
</tbody>
Although if I had more time to think there are probably far simpler implementations of this maybe something like
<%= render 'template', locals:{collection: #users, headers: ["Full Name","Email"], columns: ["name", "Email"]} %>
UPDATE
I think making a view helper might be better in this instance like this
def make_table(collection,options={})
content_tag(:table,options[:table_options]) do
content_tag(:thead) do
content_tag(:tr) do
options[:headers].map do |header,header_options|
content_tag(:th,header,header_options,false)
end.join.html_safe
end
end
content_tag(:tbody,options[:body_options]) do
collection.map do |obj|
content_tag(:tr,options[:row_options]) do
options[:columns].map do |column,cell_options|
content_tag(:td,obj.public_send(column),cell_options,false)
end.join.html_safe
end
end.join.html_safe
end
end
end
call as
<%= make_table(#users,columns:{name:{class: "name"},email:{}},headers:{"Full Name"=>{class:"name_header"},"Email"=>{}}) %>
or without formatting
<%= make_table(#users,columns:[:name,:email],headers:["Full Name","Email"]) %>
This method requires an object collection and will accept the following through the options Hash
:table_options as a Hash to pass to the content tag for formatting the table
:headers as an Array or Hash (for formatting header rows)
:body_options as a Hash to pass to the content tag for formatting the table body
:row_options as a Hash to pass to the content tag for formatting the rows
:columns as an Array or Hash (for formatting the individual cells)
You can place this method in helpers/application_helper.rb and you will have access to it throughout the application. Although I have not fully vetted this method and it is currently more conceptual than anything else.
I'm trying to make a class to populate a deals tab on my website.
Part 1. Take an items close date (CatalogItem.close_date) and use all items within 12 hours of closing. Part 2. Calculate the deal percentage by using the current price (CatalogItem.current_price) and estimated value (Item.estimated_price) <-- You'll notice they're in different tables but they're identified by an identical item_id.
I'm green in RoR, so I'm having trouble connecting this in a class, but I can make it work individually in the console:
hour_diff = (CatalogItem.last.close_date - Time.now) / 1.hour
deal_percentage = (CatalogItem.last.current_price.to_f / Item.last.estimated_price)*100
As you can see I'm using my .last piece of data, but I want to create an array that runs through all my items, that's where my knowledge goes dry, any help would be much apreciated
I'm assuming you are using a belongs_to, but I think what you want to do is use
an instance method. This would be your model, app/models/catalog_item.rb
class CatalogItem < ActiveRecord::Base
belongs_to :item
def hours_remaining
(close_date - Time.now) / 1.hour
end
def deal_percentage
(current_price.to_f / item.estimated_price)*100
end
end
Then, you could access them in a view something like this:
<table>
<% CatalogItem.all.each do |ci| %>
<tr>
<td><%= ci.hours_remaining %></td>
<td><%= ci.deal_percentage %></td>
</tr>
<% end %>
</table>
In a Rails 3.2 app I have a model Project, which has many Tasks. Each Task has a :status field, which is an integer as follows
1=Normal
2=Urgent
In the Project show view, I want to display a text alert if any of the associated tasks are flagged as urgent.
If the status field was within the Project model, I would do something like this:
<% if Project.status == 2 %>
<div class="alert">URGENT TASKS!</div>
<% end %>
How can I set up a similar if statement, that will cycle through all associated Tasks, and return true if at least one task is marked as urgent?
I'm not sure what terms I should be searching on for this sort of functionality. Or maybe I'm not looking at the problem the right way. I'd be grateful for any pointers in the right direction.
Thanks
This method in Project will do it:
def urgent?
tasks.detect{|t| t.status==2}
end
Then you can do, if you have #project set to the project you're looking at:
<% if #project.urgent? %>
...whatever ...
<% end %>
This next bit was added in answer to your comment. This method in Project will return the highest priority set (lowest number in your example) for any task in a particular project:
def highest_priority
tasks.map{|t| t.status}.min
end
You can then switch between them in your view:
<% case #project.highest_priority
when 1 %>
...priority 1 stuff...
<% when 2 %>
...priority 2 stuff...
<% when 3 %>
...and so on...
<% end %>
I guess that you want to check if a project has some urgent task to be completed. If thats the case I think the best way to achieve that would be to create new method in the Project model, something like this:
def has_urgent_task?
tasks.map(&:status).include?(Task::URGENT)
end
Assuming you have defined your statuses as constants in your Task model, if not just replace Task::URGENT for 2.
So in your view you only need to do this:
<% if #project.has_urgent_task? %>
<div class="alert">URGENT TASKS!</div>
<% end %>