ruby - Order a group_by collection desc? - ruby-on-rails

I have a collection of products users have purchased, grouped by their name, so I can count the number of unique products and how many of each has been purchased:
Controller:
#line_items = Spree::LineItem.joins(:order).where(spree_orders: {state: "complete"})
#products = #line_items.group_by(&:name)
View:
<% #products.each do |name, line_items| %>
<%= name %> - <%= line_items.count %><br>
<% end %>
Is there a way to order the .each loop so that it descends by line_items.count?
Thanks

It will perform better getting the correct data directly from the db:
#products = #line_items.group(:name).order("count_all DESC").count
That will give you the names and counts directly, e.g.
# => { "line_1" => 3, "line_2" => 2, "line_3" => 8 }
There's a bit of Rails magic at work here: the SQL generated using group, order and count will look like:
SELECT COUNT(*) AS count_all, name AS name FROM spree_line_items GROUP BY name ORDER BY count_all DESC
That's where count_all comes from: Rails attaches it to the count column automatically.
Then you can plug this directly into your view:
<% #products.each do |name, line_item_count| %>
<%= name %> - <%= line_item_count %><br>
<% end %>
Alternatively, if you're using the instance variable elsewhere, here's a simple Ruby solution:
#products = #line_items.group_by(&:name).sort_by { |_k, line_items| line_items.count }.reverse
This simply uses sort_by to get records ordered by the relevant count, then reverses to get decending order. There's a good benchmark on doing this here.
Hope that helps - let me know how you get on / if you've any questions.

Related

Rails HABTM query only returning matching associations

i'm trying create a record filter on their associations. So I have creatives and they have multiple talents. I want to have av view that filters the creatives with a specific talent. But still display each creatives multiple talents in the view.
class Creative
has_and_belongs_to_many :talents
end
Creative -> HABTM -> Talents
#creatives = Creative.includes(:talents, user: [:profile_image_attachment])
#creatives = #creatives.where(talents: { id: searched_talent_id })
The problem is that when displaying each creative it only returns the matching talent.
So rendering:
<% #creatives.each do |creative| %>
<% creative.talents.each do |talent| %>
<%= talent.name %>
<% end %>
<% end %>
Only shows the talent matched by the query, not all of them. I.e. the creative has multiple talents.
If I change the code to include a call to .all.
<% #creatives.each do |creative| %>
<% creative.talents.all.each do |talent| %>
<%= talent.name %>
<% end %>
<% end %>
Then I do get all talents BUT the database is hit with query for each creative.
Can I avoid this? I.e. eager loading all talents in creative and not getting limited by the one i search on!?
You can solve this by using a subquery:
#creatives = Creative.includes(:talents, user: [:profile_image_attachment])
.where(
id: Creative.joins(:talents)
.where(talents: { id: searched_talent_id })
)
This creates the following SQL:
SELECT
"creatives".* FROM "creatives"
WHERE
"creatives"."id" IN (
SELECT "creatives"."id"
FROM "creatives"
INNER JOIN "creatives_talents" ON "creatives_talents"."creative_id" = "creatives"."id"
INNER JOIN "talents" ON "talents"."id" = "creatives_talents"."talent_id"
WHERE "talents"."id" = $1
)
LIMIT $2
This only applies the WHERE clause to the subquery instead of the rows fetched by .includes.

Using nested form to update and create associated items with validation

I have a order form with restrictions on the number of items you can select that accepts nested attributes and I'd like to perform a create and an update on it's associated items when I update the order.
My form looks like:
<%= simple_form_for #food_order do |f| %>
<% #food_order.order_items.each do |oi| %>
<%= f.fields_for :order_items, oi do |oi_form| %>
<%= oi_form.input :quantity %>
<% end -%>
<% end -%>
<% end -%>
My validator looks like:
# OrderItem.rb
validate :food_order_quantity
def food_order_quantity
if (order.limit - order.order_items.sum(:quantity)) < self.quantity
errors.add(:base, "Too many products. Please update your cart.")
end
end
Let's imagine I create an order with a limit of 10 items and select 10 items:
food_order = FoodOrder.create(limit: 10)
order_item_1 = OrderItem.create(order: food_order, quantity: 10)
If I attempt to update the order by reducing order_item_1's quantity by 1 and adding a new order_item with a quantity of 1 I get an error even though the total quantity is correct:
order_item_1.quantity = 9
order_item_2 = OrderItem.new(order: food_order, quantity: 1)
put client_food_order_path(#client, food_order), params: {
food_order: {
id: food_order.id,
order_items_attributes: [
order_item_1.attributes,
order_item_2.attributes,
]
}
}
# returns the following error
#messages={:"order_items.base"=>["Too many products. Please update your cart."]}
I understand that it's attempting to save order_item_2 before updating order_item_1 and in that time, the controller believes there are 11 items (simply because order_item_1 has not yet been updated).
What can I do to allow this sort of operation?

Rails extra columns in group_by query

My models are like this:
Product belongs_to Category
Product belongs_to OrderItem
I want to select total value of products sold in a certain day, grouped by Categorylike this:
#items = OrderItem.joins(:product => [:category]).where('order_items.sale_date = ?', 1.day.ago.strftime('%Y-%m-%d')).group(:'categories.name').sum(:total_value)
The query works fine, returning an array with Category name and Total value.
I need in the result some extra columns, like the Id of the Category. How can I do that?
Thanks
Try the following to get an array of categories and corresponding sums:
#items = OrderItem.joins(:product => [:category]).where('order_items.sale_date = ?',
1.day.ago.strftime('%Y-%m-%d')).group('categories.id').
sum(:total_value).map {|k, v| {category: Category.find(k), total_value: v}}
Now in the view you can use something like:
<% #items.each do |item| %>
<p><%= item[:category].name %> - <%= item[:total_value] %></p>
<% end; %>

Rails Single Results from Multiple Tables

Using Rails 4.2
I have two models, suppliers and clients. Both models contain a name (string) and email (string). They do not have any relationship between them.
I would like to generate a list of all the names and emails from both suppliers and clients. In this list I would also like to know if the partner is a supplier or client.
Controller
#suppliers = Supplier.all
#clients = Client.all
#all_partners = (#suppliers + #clients).sort { |x, y| x.name <=> y.name }
View
<% #all_partners.each do |partner| %>
<%= partner.name %>, <%= partner.email %>, <%= partner.type %>
<!-- I need some way to know if the partner type is a supplier or client -->
<% end %>
How can I put in which type of partner it is? Is there a way to do this with one single AR call or query? This is basically how to use an SQL Union statement in Rails.
You could get the class name of the object I believe <%= partner.class.model_name.human %>
Thanks for the help all.
I ended up using the same controller as in the question, with some additional information in the view.
View
<% #all_partners.each do |partner| %>
<%= partner.name %>, <%= partner.email %>, <%= partner.try(:client_type) %>, <%= partner.class.model_name.human %>
<% end %>
Union in ActiveRecord works only within a single model. You could use union for two different tables using raw SQL, something like this:
Supplier.connection.execute("(SELECT id, ..., 'suppliers' as table FROM suppliers WHERE...) UNION (SELECT id,... 'clients' as table FROM clientsWHERE...)")
but the result would be of type PG::Result.
So the best way, unfortunately, is to use two ActiveRecord queries.
OR if clients and suppliers have similar fields, you could put them in one table
class Partner < ActiveRecord::Base
default_scope where(is_supplier: true)
scope :clients, -> { where(is_supplier: false) }
end
so Partner.all will output only suppliers, Partner.unscoped - all partners

Rails. Sum a specific attribute on a collection

I have a list of invoices...
#invoices_1_week = Invoice.order("due_date DESC").where("status != 'paid' AND due_date >= ? AND due_date < ?", Date.today, 1.week.from_now)
The invoices model has a total attribute. How can I get a sum of the totals in the #invoice_1_week collection?
I know I can do it in the view like this...
<% week_1_total = 0 %>
<% #invoices_1_week.each do |invoice| %>
<% week_1_total = week_1_total + invoice.total %>
<% end %>
<%= week_1_total %>
But I'm wondering if there is a more Railsy way of doing it.
Here's a Rails way, using ActiveRecord's sum method:
#invoices_1_week.sum("total")
Here are the docs.
You might want to consider using the symbol notation
#invoices_1_week.sum(:total)
or use single quotes
#invoices_1_week.sum('total')
In both cases, the attribute name is immutable.

Resources