Is there a general pattern that is recommended for building a "summary detail" type view? Say for example, you have an invoicing app with a reporting view that needs to list headings for every customer with a detail of every open invoice for that customer under the heading.
Here's how I've attempted to deal with it in the past (using the customer/invoice example), none of which has been very satisfactory:
Method #1:
Create a join query which joins the customer details with the invoice. In the view loop over the collection using an instance variable to keep track of the customer being looped over. Have a conditional in the loop code that compares the current iteration's customer with the last interation's customer. If they are different, add in a heading for the new customer.
Method #2:
Query for customers with an open invoice, then in the view iterate over the customers and nested the customer iteration, do another iteration over that customer's open invoices, e.g:
<% #customers.each do |customer| %>
<%= customer.name %>
<% customer.invoices.open.each do |invoice| %>
<%= invoice.details %>
<% end %>
<% end %>
So, yeah. Neither one of these feels great. Method #1 seems like a hack and Method #2 seems like it would generate too many queries though its the most readable in the code.
Looks like a classical case to use ActiveRecord includes option
http://apidock.com/rails/ActiveRecord/QueryMethods/includes
so in your controller where to find customers
Customer.includes(:invoices).where('customer.name = ?', 'example')
Related
Using Rails 4, in a controller I would like to add an attribute to an instance variable.
Sorry for the poor example, I'm trying to keep it simple.
E.g. In a controller, I create a new instance variable by looking up some users named John. Now, in my controller, I would like to sum up all the ages for all Users named John, put that summed age back in to the instance variable so it is available to the view.
The User model has attributes 'id', 'name' and 'age'.
#foo_users = Users.where(name: 'John')
#foo_users.each do |foo|
#foo_users.age_sum = Users.where(name: 'John').sum(:age) <-- this does not work
end
I have no need to save that summed age back to a database, since I will only use it in one view. I would like to be able to display all the users:
<% #foo_users.each do |user| %>
User name: <%= user.name =>
Sum of ages: <%= user.age_sum %>
<% end %>
Update: I might have over simplified my example. Here is a closer to reality example.
A company owns hotels. Hotels have Rooms. Management software delivers to the company daily Hotel_Statistics via an API. For lack of a better word, these Hotel_Statistics contain the hotel_id, daily check-ins, daily check-outs. In the company's back-office Rails app that I am working on, on the page displayed there is a table of hotels with their given most recent statistics. One line would look like:
Hotel Id: 123
Daily check-ins: 50
Daily check-outs: 48
Hotel Id: 124
Daily check-ins: 35
Daily check-outs: 37
The company wants to also display the running sum of the last 30 days of check-ins (outs, net check-ins).
To accomplish this, in my controller, I find the Hotel_Statics for the most recent date (normally yesterday).
latest_stat = HotelStatistic.order('date DESC, hotel_id DESC').first
#latest_date = latest_stat.date
#recent_stats = HotelStatistic.where(date: #latest_date).order('hotel.id ASC').all
I display the details of #recent_stats in my view.
Now, I would like to display in my view the sum of the last 30 days of #recent_stats.check_ins for each Hotel. My idea was to sum up the the last 30 days of check_ins statistics for a given Hotel like:
#recent_stats.each do |stat|
#last_30_days_check_ins = HotelStatistic.where(hotel_id: stat.hotel_id).where("date >= ?", Date.today - 30).sum(:check_ins)
end
The math works, but I need a way to access the 30 day sum variable for each hotel. I was a hoping to make this easy in the view by adding the hotel 30 day sum to the #recent_stats instance variable so in my view I could do:
<% #recent_stats.each do |statistic| %>
Hotel Id: <%= statistic.hotel_id %>
Daily check-ins: <%= statistic.check_ins %>
Last 30 days check-ins: <%= statistic.last_30_days_check_ins %>
<% end %>
Does this more realistic example change anything in your suggested answers? Thanks
The type of #foo_users is ActiveRecord::Relation. Trying to add age_sum as a new attribute to an ActiveRecord::Relation object doesn't make sense because semantically age_sum is not an attribute of ActiveRecord::Relation objects. It's better to store the sum of ages in a new instance variable, for example #user_age_sum.
UPDATE
Try the following
class HotelStatistic < ActiveRecord::Base
belongs_to :hotel
end
class Hotel < ActiveRecord::Base
has_many :hotel_statistics
def last_30_days_check_ins
self.hotel_statistics.where("date >= ?", 30.days.ago).sum(:check_ins)
end
end
Keep the existing code for building #recent_stats in the controller
In the view
<% #recent_stats.each do |statistic| %>
Hotel Id: <%= statistic.hotel_id %>
Daily check-ins: <%= statistic.check_ins %>
Last 30 days check-ins: <%= statistic.hotel.last_30_days_check_ins %>
<% end %>
Using select should solve your problem:
#users = User.select("*, SUM(age) as age_sum").where(name: 'John')
Now each User in the #users array will have a age_sum property. This is not 100% ideal as the value of the property will be the same on each instance, but it will work with how you've setup your view.
Edit
It's possible to dynamically define a method on an instance manually:
#foo_users.each do |foo|
def foo.age_sum; Users.where(name: 'John').sum(:age); end;
end
However while this is possible, it would have to be a very good use case to justify the negative impact this may have (such as on how readable, efficient and maintainable the code is). There are probably much better OO ways to solve the same problem
I have two models, Company and User. A Company has many Users.
On one of my pages, I want to display all of my companies and users on a single page:
<% #companies.each do |c| %>
<%= c.name %>
<% c.users.each do |u| %>
<%= u.name %>
<% end %>
<% end %>
The only issue is that the page is starting to take 15-30 seconds minimum to load because I have so many companies and users attached to the company. My current solution takes O(n^2) (correct me if I'm wrong), so this solution is only going to get worse as my sample grows.
Is there a good way to improve performance? New users are added to company everyday so I have considered caching a hash with the company id as the key and an array of users as the value. Is this a viable solution?
You want to use eager loading.
http://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations
In short, if you do something like Company.includes(:users).find(2) in your controller, it will perform two queries - one to load the company, and then another to load all of the users all at once. Then you can safely iterate over the users in the view without incurring any extra performance overhead.
This also works if you have a relation of companies, eg. Company.includes(:users).where(active: true). It will load all of the active companies, and all of the users for all of the companies, in just two queries.
I'm attempting to make an invoice application. Here are my models which are related to my question:
UPDATE: Model information has changed due to recent suggestions
Invoice
> id
> created_at
> sales_person_id
LineItem
> id
> invoice_id
> item_id
> qty_commit (inventory only)
> qty_sold
> price (because prices change)
> ...etc
Item
> barcode
> name
> price
> ...etc
Invoice has_many items, :through => :line_items. Ditto for Item. What I want to do is that when I create a new invoice, I'd like the form to be populated with all available Items. The only time I don't want all items to be populated is when I'm viewing the invoice (so only items which exist in the LineItems table should be retrieved). Currently - and obviously - a new Invoice has no items. How do I get them listed when there is nothing currently in the collection, and how do I populate the form? Also I'd like all products to be available when creation fails (along with what the user selected through the form).
UPDATE: I can create items through the controller via the following:
#invoice = Invoice.new
# Populate the invoice with all products so that they can be selected
Item.where("stock > ?", 0).each do |i|
#invoice.items.new(i.attributes)
end
This is of course my crude attempt at doing what I want. Visually it works out great, but as predicted my form id's and such are not playing well when I actually attempt to save the model.
LineItem(#37338684) expected, got Array(#2250012)
An example of the form:
# f is form_for
<% #invoice.items.group_by{|p| p.category}.each do |category, products| %>
<%= category.name %>
<%= f.fields_for :line_items do |line_item| %>
<% for p in products %>
<%= line_item.hidden_field :tax_included, :value => p.tax_included %>
<%= p.name %>
$<%= p.price %>
<% end %>
<% end %>
<% end %>
First of all, if you explicitly want to have a join model with additional attributes in it, you should use has_many :through instead of has_and_belongs_to_many. See the RoR Guide to the differences of the two.
Second, there is no single solution for what you want to reach. I see there two typical usages, depending on the mass of possible instances, one is better than the other:
Use radio buttons to select (and deselect) where a relation should be created or deleted. See the railscast #165 how to do part of that.
You could use select menus with a button to add a relation. See railscast #88. The added relation could be shown in a list, with a delete button nearby.
Use token fields (see railscast #258) to autocomplete multiple entries in one single text entry field.
In all the situations, you normally have to check at the end, if
a relation should be deleted
kept
or created
I hope some of the ideas may show you the right solution for your problem.
I am new to rails, but not to programming or databases.
A BETTER PHRASING OF MY QUESTION IS IN MY ANSWER BELOW.
For simplicity in this example, I have 3 models:
User
Subscription
Default_Subscription
User has_many Subscriptions
Subscription belongs_to User
Default_Subscription has_many Subscriptions
Subscription belongs_to Default_Subscription
Default_Subscription is a pre-populated table with certain types of subscriptions.
During the subscription process, the default subscriptions are listed at one point, and there
is a quantity box alongside each one.
The user enters the quantities for each subscription and hits submit to continue on.
My question is:
How would one go about creating a new subscription for each quantity in a number form?
So you would have a list something like so:
<ol>
<%= each subscription with quantity box %>
</ol>
<%= button_to %>
When the user hits the button, how do you add up the quantity from each box and add a new subscription for each one? Do I have to use javascript to get the numbers? Do I somehow use rails forms even though this quantities are not associated with any specific database field? Any recommendations or pointing me in the right direction to figure this out on my own would be great.
This form box IS NOT A FIELD FOR ANY MODEL, it's a count for an association. Let me rephrase: Each quantity in the form boxes represent the number of NEW Subscriptions to be created. Each of these subscriptions BELONGS_TO 1 DEFAULT_SUBSCRIPTION. In essence, the number represents the number of new subscriptions ASSOCIATED WITH THAT DEFAULT SUBSCRIPTION.
I'm using rails 3.2.1, and ruby 1.8.7
Thank you
Not sure I totally understand your description, but I'll take a shot.
Use the 'form_for' function to build your form, based on an instance of #default_subscription (established in the "new" action in your controller). If there are default values in #default_subscription, will show in the fields and the user can change them as they see fit. (this assumes your DefaultSubscription model has three attributes: sub1, sub2 and sub3.)
<%= form_for #default_subscription do |f|
<%= f.label :sub1 %><br />
<%= f.number_field :sub1 %>
<%= f.label :sub2 %><br />
<%= f.number_field :sub2 %>
<%= f.label :sub3 %><br />
<%= f.number_field :sub3 %>
<% end %>
When the user clicks the submit button the contents of the form with we assembled into a hash and passed into your controller's "update" action via params[]. You can extract the subscription hash like this:
user_subscription = params[:default_subscription]
At this point you have all the numbers that the user entered into the fields in the user_subscription hash. You can now parse the hash to extract the numbers, do your math, and then create the appropriate subscriptions per the user's input. (one note: the numbers could be strings and you might need to convert them back to integers as I've shown below.)
For example, to total all the subscription values and save that total into a user's subscription:
total = 0;
user_subscription.each do |key, value|
total += value.to_i
end
new_sub = current_user.subscription.new
new_sub.total = total
new_but.save
As I said, I don't understand your description clearly, so this might not be germane, but hope it is close to what you were looking for.
Good luck.
I have figured out one way, and reading my original post again, the whole thing is really confusing because I didn't know how to say exactly what I was trying to accomplish. I must say a lot of the reason I was confused is because the form I wanted did not correspond to any model, just an association count, which is ultimately a basic html form if you want to create a bunch of new objects without having their attributes yet. I'll first clarify then show my solution.
Clarification:
I have 2 Tables:
Subscription
Default_Subscription (Pre-Populated)
Subscription belongs_to Default_Subscriptions
Default_Subscription has_many Subscriptions
A User is subscribing to my website. This process is a step by step: not everything happens on the same page.
This all happens in a subscribe_controller. Each action corresponds to a step in the process.
One of the actions is default_subscriptions. This action lists the Default_Subscriptions a User can choose from, except they do not just choose, they can enter an amount for each type of Default_Subscription they'd like.
When the Default_Subscriptions are listed on the default_subscriptions page, I wanted a form with an html number input alongside each of these Default_Subscription. When the form is submitted via a next button, I had no idea how to gather the quantities from each html input and create an array of Subscription.new, with each Subscription's default_subscription_id corresponding to the proper Default_Subscription.
One Possible Solution:
def default_subscriptions
#def_subscriptions = Default_Subscription.all
end
Lets say the page I want proceed to after all the quantities are entered on the default_subscriptions page is review_subscriptions.
Here's what I did to create the proper form to proceed to the next action in the controller:
<%= form_tag( {:controller => 'subscribe', :action => 'review_subscriptions'}, :method => 'post' ) do %>
<ol>
<% #def_subscriptions.each do |ds| %>
<li>
<%= ds.name + ' ' %>
<%= number_field_tag("subscription_counts[#{ds.id}]") %>
</li>
<% end %>
</ol>
<%= submit_tag('Next') %>
<% end %>
The trick here is that string passed to the number_field_tag. By placing a single set of square brackets at the end of the string for a field_tag method parameter, the part before the brackets is the name of the hash, and the thing in the brackets is a key in the hash, and the submit button causes the corresponding value for each key to be the value of the field. Pretty cool!
The parameters passed to the next action would contain a hash called subscription_counts, and iterating through this hash would give a corresponding new subscription amount for each default_subscription_id. Like so:
def review_subscriptions
subscription_counts = params[:subscription_counts]
subscription_counts.each do |id, amount|
counter = Integer(amount)
until counter == 0
new_subscription = Subscription.new
new_subscription.default_subscription_id = Integer(id)
#subscriptions << new_subscription # #subscriptions is an instance variable
counter -= 1
end
end
end
I'd just like to point out, the more I work with them, the more I love them; I love Rails, and I love Ruby. They are super fun and classy. An until loop... how cool is that? If you have other solutions, now that my question is more obvious, please chime in! I know others out there are trying to find some slick ways to create multiple new objects in a one to many association with a single post call like this. Technically my objects aren't saved in the database yet, but that wouldn't be to hard now.
The main reference which helped me the most in reaching this solution was:
http://guides.rubyonrails.org/form_helpers.html
If you are new to rails, and confused about forms, read this. I feel like a master now. Rails devs are really good at documenting things!
I'm attempting to make an invoice application. Here are my models which are related to my question:
UPDATE: Model information has changed due to recent suggestions
Invoice
> id
> created_at
> sales_person_id
LineItem
> id
> invoice_id
> item_id
> qty_commit (inventory only)
> qty_sold
> price (because prices change)
> ...etc
Item
> barcode
> name
> price
> ...etc
Invoice has_many items, :through => :line_items. Ditto for Item. What I want to do is that when I create a new invoice, I'd like the form to be populated with all available Items. The only time I don't want all items to be populated is when I'm viewing the invoice (so only items which exist in the LineItems table should be retrieved). Currently - and obviously - a new Invoice has no items. How do I get them listed when there is nothing currently in the collection, and how do I populate the form? Also I'd like all products to be available when creation fails (along with what the user selected through the form).
UPDATE: I can create items through the controller via the following:
#invoice = Invoice.new
# Populate the invoice with all products so that they can be selected
Item.where("stock > ?", 0).each do |i|
#invoice.items.new(i.attributes)
end
This is of course my crude attempt at doing what I want. Visually it works out great, but as predicted my form id's and such are not playing well when I actually attempt to save the model.
LineItem(#37338684) expected, got Array(#2250012)
An example of the form:
# f is form_for
<% #invoice.items.group_by{|p| p.category}.each do |category, products| %>
<%= category.name %>
<%= f.fields_for :line_items do |line_item| %>
<% for p in products %>
<%= line_item.hidden_field :tax_included, :value => p.tax_included %>
<%= p.name %>
$<%= p.price %>
<% end %>
<% end %>
<% end %>
First of all, if you explicitly want to have a join model with additional attributes in it, you should use has_many :through instead of has_and_belongs_to_many. See the RoR Guide to the differences of the two.
Second, there is no single solution for what you want to reach. I see there two typical usages, depending on the mass of possible instances, one is better than the other:
Use radio buttons to select (and deselect) where a relation should be created or deleted. See the railscast #165 how to do part of that.
You could use select menus with a button to add a relation. See railscast #88. The added relation could be shown in a list, with a delete button nearby.
Use token fields (see railscast #258) to autocomplete multiple entries in one single text entry field.
In all the situations, you normally have to check at the end, if
a relation should be deleted
kept
or created
I hope some of the ideas may show you the right solution for your problem.