Thinking Sphinx, Rails, :has_many :through => ... has - ruby-on-rails

Model: Product
has-many product-categories, :through => ...
Question 1) How do I index a many to many association with thinking sphinx
Must I use has?
Questions 2) How is this searched in the controller
ex. Product.search params[:search-params], :conditions => {some_conditions}

I've not tried this on a has_many :through so shoot me down in flames if you have, but I don't see why this wouldn't work for you too, (I'm using it on a has_many association) you basically use your association in the index definition. Then searches against that model will also search the child records.
class Product < ActiveRecord::Base
has_many :product_categories
define_index do
indexes a_product_field_to_index
indexes product_categories.name, :as => :categories
end
end
In the controller:
#products = Product.search(params[:query] || '')
#params[:query] is simply the search string, I can't remember if you need to sanitize this, I would always assume you do unless you find out otherwise
In the view:
#products.each do |p|
p.categories.each do |cat|
end
end
If you don't already have it I would highly recommend the thinking-sphinx book available on peepcode: https://peepcode.com/products/thinking-sphinx-pdf
Hope that helps.

Related

Rails 3.1 - Simple search across three (or more) models?

I have three models that i'd like to perform a simple search across:
class Release < ActiveRecord::Base
has_many :artist_releases
has_many :artists, :through => :artist_releases
has_many :products, :dependent => :destroy
end
class Product < ActiveRecord::Base
belongs_to :release
has_many :artists, :through => :releases
end
class Artist < ActiveRecord::Base
has_many :artist_releases
has_many :releases, :through => :artist_releases
end
In my Product Controller i can successfully render a product list searching across release and product using:
#products = Product.find(:all, :joins => :release, :conditions => ['products.cat_no LIKE ? OR releases.title LIKE ?', "%#{params[:search]}%","%#{params[:search]}%"])
I really need to be able to search artist as well. How would I go about doing that? I ideally need it within the Product Controller as it's a product list I need to display.
I've tried adding :joins => :artist and variations thereof, but none seem to work.
I'm aware there are options out there like Sphinx for a full search, but for now I just need this simple approach to work.
Thanks in advance!
if you only want products back, just add both joins:
#products = Product.joins(:release,:artists).where('products.cat_no LIKE :term OR releases.title LIKE :term OR artists.name LIKE :term', :term => "%#{params[:search]}%").all
You may also need group_by to get distinct products back.
if you want polymorphic results, try 3 separate queries.
I know I'm suggesting a simple approach (and probably not the most efficient) but it will get your job done:
I would create a method in your Product model similar to this:
def find_products_and_artists
results = []
Product.find(:all, :conditions => ['products.cat_no LIKE ?', "%#{params[:search]}%"]).each do |prod|
results << prod
end
Release.find(:all, :conditions => ['releases.title LIKE ?', "%#{params[:search]}%"]).each do |rel|
results << rel
end
Artist.find(:all, :conditions => ['artist.name LIKE ?', "%#{params[:search]}%"]).each do |art|
results << art
end
return results
end
Then when you call it the method and store the returned results in a variable (e.g. results), you can check what object each element is by doing
results[i].class
and can make your code behave accordingly for each object.
Hope I helped.

How to write rspec for a model method in rails?

Hi i have the following method in a my EnrolledAccount models for which i want to write rpec. My question is how can i create the association between Item and EnrolledAccount in rspec.
def delete_account
items = self.items
item_array = items.blank? ? [] : items.collect {|i| i.id }
ItemShippingDetail.destroy_all(["item_id in (?)", item_array]) unless item_array.blank?
ItemPaymentDetail.destroy_all(["item_id in (?)", item_array]) unless item_array.blank?
Item.delete_all(["enrolled_account_id = ?", self.id])
self.delete
end
Generally you would use factory_girl to create a set of related objects in the database, against which you can test.
But, from your code I get the impression that your relations are not set up correctly. If you set up your relations, you can instruct rails what to do when deleting an item automatically.
E.g.
class EnrolledAccount
has_many :items, :dependent => :destroy
has_many :item_shipping_details, :through => :items
has_many :item_payment_details, :through => :items
end
class Item
has_many :item_shipping_details, :dependent => :destroy
has_many :item_payment_details, :dependent => :destroy
end
If your models are defined like that, the deletion will be automatically taken care of.
So instead of your delete_account you can just write something like:
account = EnrolledAccount.find(params[:id])
account.destroy
[EDIT] Using a gem like shoulda or remarkable, writing the spec is then also very easy:
describe EnrolledAccount do
it { should have_many :items }
it { should have_many :item_shipping_details }
end
Hope this helps.

What is a good way to deal with categories in a rails application?

I am trying to get my head around how to deal with my Products <-> Categories relation.
I am trying to build a small shop in rails and I want to make a navigation out of the category tree.
The navigation will look something like this:
- Men
|--Shirts
|--Pants
- Woman
|--Shirts
|--Dresses
-Accessoires
You get the idea...
Now, the problem is that these appear to be all different scopes on the same model, Product, with different find conditions on the associated Category.
My models so far:
class Product < ActiveRecord::Base
# validations...
has_many :categorizations
has_many :categories, :through => :categorizations
# more stuff ...
end
class Category < ActiveRecord::Base
acts_as_nested_set
has_many :categorizations
has_many :products, :through => :categorizations
end
class Categorization < ActiveRecord::Base
belongs_to :product
belongs_to :category
end
Also, I want to have multiple categories on my products and maybe make it possible to create new categories "on-the-fly" when adding a product. So the whole category management should be as easy as possible. If someone can point me in the right direction or link me to a tutorial, best practice or anything would be really awesome!
UPDATE
Ok, so now I can creating categories on the fly using virtual attributes, the question is how do I search for articles of a specific category?
What I tried:
#products = Product.scoped(:include => :categorizations, :conditions => {:category_names => params[:category]})
or
#products = Product.where("categorization = ?", params[:category])
but both didnt work. basically i want all products of one category...
You can allow users to create new categories at the same time as creating new products by using accepts_nested_attributes_for in your model. Have a look through the documentation for that to get you started.
So I ended up creating a many-to-many relation through categorizations. This railscast explains perfectly how to do this and create new categories (or tags) on-the-fly.
After I loop through the categories to make them links in my product overview:
# app/views/products/index.html.erb
<ul class="categories">
<% for category in #categories %>
<li><%= link_to category.name, :action => "index" , :category => category.id %></li>
<% end %>
</ul>
and then in the controller I build the products from the category if there is any:
# products_controller.rb
def index
if params[:category]
#products = Category.find(params[:category]).products
else
#products = Product.scoped
end
#products = #products.where("title like ?", "%" + params[:title] + "%") if params[:title]
#products = #products.order('title').page(params[:page]).per( params[:per_page] ? params[:per_page] : 25)
#categories = Category.all
end
for sure there is a more elegant way to do it but this wors for now.. any improvement appreciated.

Include with condition, but always show the primary table results

I have a function like :
# get all locations, if the user has discovered them or not
def self.getAll(user)
self.find(:all, :order => 'min_level asc', :include => 'discovered_locations',
:conditions => [ "discovered_locations.user_id = ? OR discovered_locations.id is null", user.id] )
end
self is actually the BossLocation model. I want to get a result set of the bosslocation and the discovered location IF that location was discovered by my user. However, if it was not discovered, i still need the bosslocation and no object as a discovered location. With the above code, if the user has not discovered anything, i don't get the bosslocations at all.
EDIT :
My associations are like :
class BossLocation < ActiveRecord::Base
has_many :discovered_locations
has_many :users, :through => :discovered_locations
class DiscoveredLocation < ActiveRecord::Base
belongs_to :user
belongs_to :boss_location
class User < ActiveRecord::Base
has_many :discovered_locations
has_many :boss_locations, :through => :discovered_locations
I think the problem is that you specify the user_id in the where conditions and not in the join condition. Your query will only give you the BossLocation if the user has discovered it or if no user at all has discovered it.
To make the database query match your need, you could change the include to the following joins:
:joins => "discovered_locations ON discovered_locations.boss_location_id = boss_locations.id
AND discovered_locations.user_id = '#{user.id}'"
BUT, I don't think it would help that much since the eager loading of Rails will not work when using joins like this instead of include.
If I where to do something similar, I would probably split it up. Perhaps by adding associations for user like this:
class User < ActiveRecord::Base
has_many :disovered_locations
has_many :discovered_boss_locations, :through => :discovered_locations
Update:
That way, in your Controller you can get all BossLocations and all discovered BossLocations like this:
#locations = BossLocation.all
#discovered = current_user.discovered_locations.all.group_by(&:boss_location_id)
To use these when you loop through them, do something like this:
<% #locations.each do |location| %>
<h1><%= location.name %></h1>
<% unless #discovered[location.id].nil? %>
<p>Discovered at <%= #discovered[location.id].first.created_at %></p>
<% end %>
<% end %>
What this does is it groups all discovered locations into a hash where the key is the boss_location_id. So when you loop through all boss_locations, you just see if there is an entry in the discovered hash that matches the boss_id.
"a result set of the bosslocation and the discovered location IF that location was discovered by my user."This is not a left outer join.You will get all bosslocations anytime.So,your conditions are wrong!This will get the bosslocation that it's discovered_locations.user_id = user.id OR discovered_locations.id is null".In this condition, this may be difficult for one sql statement. Also you can use union in your find_by_sql,but i suggest you use two find function.

how to access rails join model attributes when using has_many :through

I have a data model something like this:
# columns include collection_item_id, collection_id, item_id, position, etc
class CollectionItem < ActiveRecord::Base
self.primary_key = 'collection_item_id'
belongs_to :collection
belongs_to :item
end
class Item < ActiveRecord::Base
has_many :collection_items
has_many :collections, :through => :collection_items, :source => :collection
end
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position
has_many :items, :through => :collection_items, :source => :item, :order => :position
end
An Item can appear in multiple collections and also more than once in the same collection at different positions.
I'm trying to create a helper method that creates a menu containing every item in every collection. I want to use the collection_item_id to keep track of the currently selected item between requests, but I can't access any attributes of the join model via the Item class.
def helper_method( collection_id )
colls = Collection.find :all
colls.each do |coll|
coll.items.each do |item|
# !!! FAILS HERE ( undefined method `collection_item_id' )
do_something_with( item.collection_item_id )
end
end
end
I tried this as well but it also fails with ( undefined method `collection_item' )
do_something_with( item.collection_item.collection_item_id )
Edit: thanks to serioys sam for pointing out that the above is obviously wrong
I have also tried to access other attributes in the join model, like this:
do_something_with( item.position )
and:
do_something_with( item.collection_item.position )
Edit: thanks to serioys sam for pointing out that the above is obviously wrong
but they also fail.
Can anyone advise me how to proceed with this?
Edit: -------------------->
I found from online documentation that using has_and_belongs_to_many will attach the join table attributes to the retreived items, but apparently it is deprecated. I haven't tried it yet.
Currently I am working on amending my Collection model like this:
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position, :include => :item
...
end
and changing the helper to use coll.collection_items instead of coll.items
Edit: -------------------->
I've changed my helper to work as above and it works fine - (thankyou sam)
It's made a mess of my code - because of other factors not detailed here - but nothing that an hour or two of re-factoring wont sort out.
In your example you have defined in Item model relationship as has_many for collection_items and collections the generated association method is collection_items and collections respectively both of them returns an array so the way you are trying to access here is wrong. this is primarily case of mant to many relationship. just check this Asscociation Documentation for further reference.
do_something_with( item.collection_item_id )
This fails because item does not have a collection_item_id member.
do_something_with( item.collection_item.collection_item_id )
This fails because item does not have a collection_item member.
Remember that the relation between item and collection_items is a has_many. So item has collection_items, not just a single item. Also, each collection has a list of collection items. What you want to do is probably this:
colls = Collection.find :all
colls.each do |coll|
coll.collection_items.each do |collection_item|
do_something_with( collection_item.id )
end
end
A couple of other pieces of advice:
Have you read the documentation for has_many :through in the Rails Guides? It is pretty good.
You shouldn't need the :source parameters in the has_many declarations, since you have named your models and associations in a sensible way.
I found from online documentation that using has_and_belongs_to_many will attach the join table attributes to the retreived items, but apparently it is deprecated. I haven't tried it yet.
I recommend you stick with has_many :through, because has_and_belongs_to_many is more confusing and doesn't offer any real benefits.
I was able to get this working for one of my models:
class Group < ActiveRecord::Base
has_many :users, :through => :memberships, :source => :user do
def with_join
proxy_target.map do |user|
proxy_owner = proxy_owner()
user.metaclass.send(:define_method, :membership) do
memberships.detect {|_| _.group == proxy_owner}
end
user
end
end
end
end
In your case, something like this should work (haven't tested):
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position
has_many :items, :through => :collection_items, :source => :item, :order => :position do
def with_join
proxy_target.map do |items|
proxy_owner = proxy_owner()
item.metaclass.send(:define_method, :join) do
collection_items.detect {|_| _.collection == proxy_owner}
end
item
end
end
end
end
Now you should be able to access the CollectionItem from an Item as long as you access your items like this (items.with_join):
def helper_method( collection_id )
colls = Collection.find :all
colls.each do |coll|
coll.items.with_join.each do |item|
do_something_with( item.join.collection_item_id )
end
end
end
Here is a more general solution that you can use to add this behavior to any has_many :through association:
http://github.com/TylerRick/has_many_through_with_join_model
class Collection < ActiveRecord::Base
has_many :collection_items, :order => :position
has_many :items, :through => :collection_items, :source => :item, :order => :position, :extend => WithJoinModel
end

Resources