Accessing values in a has_many :through join table - ruby-on-rails

I've got users who are members of groups through a membership join table, and one of the attributes of that join table is "administrator". I'm trying to do a check inside of a group's member view, looping through each member to see if they are an administrator.
In the view I tried the following:
for user in #group.users
if user.administrator?
...DO STUFF
end
end
I also tried this in the controller:
#administrators = #group.memberships.find(:all, :conditions => ["administrator = 1"])
But no luck. Any thoughts?
UPDATE - per below, put a method into the user model:
def is_administrator_of(group_id)
Membership.find(:first, :conditions => ['user_id = ? AND group_id = ? AND administrator = ?', self[:id], group_id, true])
end

I think this would be a cleaner way to do this
class Group < ActiveRecord::Base
has_many :memberships
has_many :users, :through => :memberships
has_many :admins, :through => :memberships, :source => :user,
:conditions => ['memberships.administrator = ?', true]
end
You now have a group.admins list
for user in #group.admins
...DO STUFF
end

Although I think you could setup associations to accomplish this I think the easiest way to do it would be to add a method to your User model that allows you to check for each user (this way it would fit in the loop you have provided). I don't know if it will drop right it, may take a few quick changes but you could start with something like:
User Model
def is_administrator_of(group_id)
Membership.find(:first, :conditions => ['user_id = ? AND group_id = ?', self[:id], group_id]).administrator == 1
end

Related

Is it possible to assign multiple has_many ":as" polymorphic relation on same field?

Having my db setup like this ("type" is always User although I have different kind of User via STI):
class User
# fields
# :id
# :sender_id, :sender_type
# :recipient_id, :recipient_type
end
Postcard model:
class Postcard < ActiveRecord::Base
belongs_to :owner, :class_name => User
belongs_to :recipient, :class_name => User
end
I'd like to setup the User model something like this:
class User < ActiveRecord::Base
has_many :postcards, :as => [:sender or :recipient] # this is not working
end
So I could say:
user.postcards
Is it possible?
PS: I've also tried this road:
has_many :postcards, :finder_sql => Proc.new { "SELECT * FROM postcards WHERE postcards.owner_id=#{id} OR postcards.recipient_id=#{id}" }
But found myself stuck on scopes as :finder_sql recreates a whole new SQL:
User.postcards.by_status('new').size
As mentioned by joelparkerhenderson I need to think differently my association strategy.
As I'd like to have:
user.postcards
My answer is simply to use scopes in Postcard model:
scope :of_user, lambda { |user| where("recipient_id = ? OR owner_id = ?", user.id, user.id) }
So I can invoke:
Postcard.of_user user
I could even wrap it in User model:
def postcards
Postcard.of_user self
end

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 do I access records from a 2 level nested model

I have 3 models User,Listing and Message. What I want is for an authenticated user to have many listings. The listings then can have multiple messages. So the messages are tied to the user through the listing model. I am able to get a users listings but not able to get the users messages which he owns through the listings. Here are the associations that I currently have.
class User < ActiveRecord::Base
has_many :listings, :dependent => :destroy
end
class Listing < ActiveRecord::Base
belongs_to :user
has_many :messages
end
class Message < ActiveRecord::Base
belongs_to :listing
end
To create a message I simply do this;
#listing = Listing.find(params[:listing_id])
#message = #listing.messages.build(params[:message])
And getting the user's listing i have this;
#user_listings = Listing.user_listings(current_user)
But getting the messages tied to the user's listings proves to be elusive. What am I doing wrong or how do I go about this? help appreciated.
Still not sure where user_listings comes from but why not this:
#user = User.find(params[:user_id], :include => {:listings => :messages})
#user.listings.each do |listing|
listing.messages.each do |message|
#or
#user.listings.collect(&:messages).each do |message|
#or (just read about using authenticated user so the same as above like this
current_user.listings(:all, :include => :messages)...
Include prefetches all the listings' associated messages in one query in order that they're not fetched in the loop causing n+1 querying.
----------
Or another approach, if you don't need the listings data.
#messages.rb
def self.user_messages user_id
find(:all, :joins => :listings, :conditions => ["listings.user_id = ?", user_id])
#with pagination
def self.user_messages user_id, page
paginate(:all, :joins => :listings,
:conditions => ["listings.user_id = ?", user_id],
:per_page => 10, :page => page)
updated regarding your comment.
You may want to just add has_many :messages to the user class as well and add a user_id column to Message. Then you could just do current_user.messages
How about something like this:
class User < ActiveRecord::Base
has_many :listings, :dependent => :destroy
has_many :listing_messages, :through => :listings
That way you dont have to "tie" the messages with the user because it is always accessed through the listing association:
current_user.listing_messages.all
Or have I misunderstood your question?
If you have current_user pulled already. You can just access listings directly by calling
current_user.listings
instead of
#user_listings = Listing.user_listings(current_user)

Rails, ActiveRecord: how do I get the results of an association plus some condition?

I have two models, user and group. I also have a joining table groups_users.
I have an association in the group model:
has_many :groups_users
has_many :users, :through=> :groups_users
I would like to add pending_users which would be the same as the users association but contain some conditions. I wish to set it up as an association so that all the conditions are handled in the sql call. I know there's a way to have multiple accessors for the same model, even if the name is not related to what the table names actually are. Is it class_name?
Any help would be appreciated, thanks
Use named_scopes, they're your friend
Have you tried using a named_scope on the Group model?
Because everything is actually a proxy until you actually need the data,
you'll end up with a single query anyway if you do this:
class User < ActiveRecord::Base
named_scope :pending, :conditions => { :status => 'pending' }
and then:
a_group.users.pending
Confirmation
I ran the following code with an existing app of mine:
Feature.find(6).comments.published
It results in this query (ignoring the first query to get feature 6):
SELECT *
FROM `comments`
WHERE (`comments`.feature_id = 6)
AND ((`comments`.`status` = 'published') AND (`comments`.feature_id = 6))
ORDER BY created_at
And here's the relevant model code:
class Feature < ActiveRecord::Base
has_many :comments
class Comment < ActiveRecord::Base
belongs_to :feature
named_scope :published, :conditions => { :status => 'published' }
This should be pretty close - more on has_many.
has_many :pending_users,
:through => :groups_users,
:source => :users,
:conditions => {:pending => true}
:pending is probably called something else - however you determine your pending users. As a side note - usually when you see a user/group model the association is called membership.
In the User model:
named_scope :pending, :include => :groups_users, :conditions => ["group_users.pending = ?", true]
That's if you have a bool column named "pending" in the join table group_users.
Edit:
Btw, with this you can do stuff like:
Group.find(id).users.pending(:conditions => ["insert_sql_where_clause", arguments])

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