Showing a count of associated records - ruby-on-rails-3

As part of my site admin, I need to occasionally update project categories to reflect changing user requirements.
in my show method (CategoriesController) , I have the following code:
#projects = Project.count.where(:project.category_id => :id)
My controller looks like this:
class ProjectCategory < ActiveRecord::Base
attr_accessible :project_category
belongs_to :project
end
I can't seem to get it to populate the count of all projects for each category. Any help?

You should change the line
#projects = Project.count.where(:project.category_id => :id)
to
#projects = Project.where(:project.category_id => :id).count

Related

Rails multiple joins condition query for exact tags with has many through relationships

I have a problem with filtering products by exact tags as my current query does not return exact matches and I can't seem to get the right query conditions.
Example
Area = ["small","big"] ,Surface = ["smooth","rough"]
Product A has only ["small","smooth","rough"] as tags
If I filter products using ["small","big","smooth","rough"] as the tags, I get product A in my search results but ideally, it should not return any search results.
I have three models, Product,Area and Surface. Area & Surface are linked to Product by a has_many through relationship.
class Product < ActiveRecord::Base
has_many :product_areas
has_many :areas, :through => :product_areas
has_many :product_surfaces
has_many :surfaces, :through => :product_surfaces
class Area < ActiveRecord::Base
#Surface model looks exactly the same as Area model
has_many :product_areas,dependent: :destroy
has_many :products, :through => :product_areas
My Query
area_ids = params[:area_ids]
surface_ids = params[:surface_ids]
#products = Product.where(nil)
#products = #products.joins(:areas).where('areas.id' => area_ids).group('products.id').having("count(areas.id) >= ?",area_ids.count) unless area_ids.blank?
#products = #products.joins(:surfaces).where('surfaces.id' => surface_ids).group('products.id').having("count(surfaces.id) >= ?",surface_ids.count) unless surface_ids.blank?
I solved this problem just now with this solution.
First I used the names of the models for Area & Surface as their unique identifer as they can have conflicting ids and added them to an array.
Next I looped through the products and created an array of the name identifers and compared the two arrays to check if they intersect. Intersection would mean that the search filters were a correct match and we add the product ID to a third array which stores all the product_ids before doing a query to get the products with those product ids.
#area = Area.all
area_ids = params[:area_ids]
#uniq_names = #area.where(id: area_ids).collect { |m| m.name }
#products.each do |product|
#names = product.areas.map { |m| m.name }
# if intersect, then we add them to filtered product
if (#uniq_names - #names).empty?
product_ids << product.id
end
end
#products = Product.where(id: product_ids)

Query a 3-way relationship in Active Record

I'm trying to figure out how to query this relationship without using find_by_sql
class User < ActiveRecord::Base
has_many :lists
end
class List < ActiveRecord::Base
has_many :list_items
belongs_to :user
end
class ListItem < ActiveRecord::Base
belongs_to :list
belongs_to :item
end
class Item < ActiveRecord::Base
has_many :list_items
end
this should be what we are using but How would I do this not by find_by_sql
in user.rb
def self.find_users_who_like_by_item_id item_id
find_by_sql(["select u.* from users u, lists l, list_items li where l.list_type_id=10 and li.item_id=? and l.user_id=u.id and li.list_id=l.id", item_id])
end
I've tried several different includes / joins / merge scenarios but am not able to get at what I'm trying to do.
thx
It's a bit difficult to tell exactly what query you're trying to do here, but it looks like you want the user records where the user has a list with a particular list_type_id and containing a particular item. That would look approximately like this:
User.joins(:lists => [:list_items]).where('lists.list_type_id = ? and list_items.item_id = ?', list_type_id, item_id)
This causes ActiveRecord to execute a query like the following:
SELECT "users".* FROM "users" INNER JOIN "lists" ON "lists"."user_id" = "users"."id" INNER JOIN "list_items" ON "list_items"."list_id" = "lists"."id" WHERE (lists.list_type_id = 10 and list_items.item_id = 6)
and return the resulting collection of User objects.

Rails: How to set up an IF condition with a JOIN in a has_many :through relationship

I have an application where users can customize a calendar and fill it with a given pool of events. A user can also overwrite a title for his own calendar by an alias. So I have the following has_many :through relation:
class Calendar < ActiveRecord::Base
has_many :event_aliases
has_many :events, :through => :event_aliases
end
class Event < ActiveRecord::Base
attr_accessible :title
has_many :event_aliases
has_many :calendars, :through => :event_aliases
end
class EventAliases < ActiveRecord::Base
attr_accessible :course_id, :calendar_id, :custom_name
belongs_to :event
belongs_to :calendar
end
No I want to deliver the calendar with the aliases. If an event has an alias (custom_name), it should be displayed. Otherwise the default event name (title) should be displayed.
Is there a way to easily set up a query that returns all events for the current calendar whether with a custom_name (if exists) or with the default title?
My current solution is to hardcode an if condition into the query which I would like to avoid.
title_column = "case when custom_name IS NOT NULL then custom_name else title end as title"
# assume we are given a calendar_id
Calendar.find(calendar_id).event_aliases.joins(:event).select(title_column, :event_id).each do |event_alias|
# do further stuff here
end
I also could fetch all event_aliases and run through each of them to get the default title if necessary.
# assume we are given a calendar_id
Calendar.find(calendar_id).event_aliases.each do |event_alias|
title = event_alias.custom_name
if title.nil?
title = Event.find(event_alias.event_id).title
# do further stuff here
end
But this one results in too many queries to me.
So is there any smarter way of accomplishing what I want? Maybe using named scopes or another fancy rails technique?
UPDATE
I ended up with making a "custom" select via the has_many :through relationship. So the only thing changes is the Calendar model:
class Calendar < ActiveRecord::Base
has_many :event_aliases
has_many :events, :through => :event_aliases,
:select => "event_aliases.custom_name as custom_name, events.*"
end
So accessing the custom_name / the title now happens a little like #Doon suggested:
Calendar.find(1).courses.each do |course|
title = course.custom_name || course.title
end
This creates only 2 queries instead of 3:
Calendar Load (0.6ms) SELECT `calendars`.* FROM `calendars` WHERE `calendars`.`id` = 1 LIMIT 1
Event Load (0.7ms) SELECT event_aliases.custom_name as custom_name, events.* FROM `events` INNER JOIN `event_aliases` ON `events`.`id` = `event_aliases`.`event_id` WHERE `event_aliases`.`calendar_id` = 1
what about using includes to grab the events at the same time as you pull the aliases.
Calendar.find(1).event_aliases.includes(:event).each do |e|
puts e.custom_name.blank? ? e.event.title : e.custom_name
end
the SQL Rails generates will look something like this:
Calendar Load (0.2ms) SELECT "calendars".* FROM "calendars" WHERE "calendars"."id" = ? LIMIT 1
EventAlias Load (0.2ms) SELECT "event_aliases".* FROM "event_aliases" WHERE "event_aliases"."calendar_id" = 1
Event Load (0.2ms) SELECT "events".* FROM "events" WHERE "events"."id" IN (1, 2)
also if you want to clean it up a bit you can add a virtual field to the EventAlias
class EventAlias < ActiveRecord::Base
def name
custom_name || self.event.title
end
end
As long as you use the includes, the queries will be be the same.

Can I push this rails calculation into the database?

I'm trying to increase my app's efficiency by doing work in the database rather than in the app layer, and I'm wondering if I can move this calculation into the database.
Models:
class Offer < ActiveRecord::Base
has_many :lines
has_many :items, :through => :lines
end
class Line < ActiveRecord::Base
belongs_to :offer
belongs_to :item
# also has a 'quantity' attribute (integer)
end
class Item < ActiveRecord::Base
has_many :lines
has_many :offers, :through => :lines
# also has a 'price' attribute (decimal)
end
What I want to do is calculate the price of an offer. Currently I have a price method in the Offer class:
def price
self.lines.inject(0) do |total, line|
total + line.quantity * line.item.price
end
end
I suspect it may be possible to do a Offer.sum calculation instead that would get the answer directly from the DB rather than looping through the records, but the Calculations section of the ActiveRecord query guide doesn't have enough detail to help me out. Anybody?
Thanks!
You're correct that you can do this with sum. Something like this:
class Offer < ActiveRecord::Base
# ...
def price
self.lines.sum 'lines.quantity * items.price', :joins => :item
end
end
When you call e.g. Offer.find( some_id ).price the above will construct a query something like this:
SELECT SUM( lines.quantity * items.price ) AS total
FROM lines
INNER JOIN items ON items.id = lines.item_id
WHERE lines.offer_id = <some_id>
;
Sometimes you're better off with SQL.
SELECT SUM( lines.quantity * items.price ) AS total
FROM offers
INNER JOIN lines ON offers.id = lines.offer_id
INNER JOIN items ON items.id = lines.item_id
WHERE offers.id = 1
;

Writing a named scope in rails

I have three models: Products, Placements, Collections
I'm trying to write a name scope that only chooses products NOT in a certain collection.
products has_many :collections, :through => :placements
collections has_many :products, :through => :placements
I got about this far:
scope :not_in_front, joins(:collections).where('collections.id IS NOT ?', 4)
But that generated the opposite of what I expected in the query:
Product Load (0.3ms) SELECT "products".* FROM "products" INNER JOIN "placements" ON "products"."id" = "placements"."product_id" WHERE "placements"."collection_id" = 4
Any idea how to write this to only select the products not in that particular collection?
Instead of collections.id IS NOT 4 try collections.id != 4
The named scope was getting too ugly, so I went with this. Not sure it's the best way, but, it works...
def self.not_on_top_shelf
top_shelf = Collection.find_by_handle('top-shelf')
products = Product.find(:all, :order => "factor_score DESC")
not_on_top_shelf = products.map {|p| p unless p.collections.include?(top_shelf)}
not_on_top_shelf.compact #some products may not be in a collection
end