I have some accounts, and users, which are disjointed at the moment.
I need users to be able to be admins, or editors, or any (and many) accounts.
At the moment, I have this:
account.rb
has_many :memberships, :dependent => :destroy
has_many :administrators, :through => :memberships, :source => :user, :conditions => {'memberships.is_admin' => true}
has_many :editors, :through => :memberships, :source => :user, :conditions => {'memberships.is_editor' => true}
user.rb
has_many :memberships
has_many :accounts, :through => :memberships
has_many :editor_accounts, :through => :memberships, :source => :account, :conditions => {'memberships.is_editor' => true}
has_many :administrator_accounts, :through => :memberships, :source => :account, :conditions => {'memberships.is_admin' => true}
Essentially, what I am trying to acheive is a nice simple way of modelling this that works in a nice simple way. For instance, being able to do the following this would be really useful:
#account.administrators << current_user
current_user.adminstrator_accounts = [..]
etc
You should be able to do this, but it might be the notation you've used that interferes with the auto scope application:
has_many :memberships,
:dependent => :destroy
has_many :administrators,
:through => :memberships,
:source => :user,
:conditions => { :is_admin => true }
The conditions should be applied if and only if the condition keys match the column names on the association. So long as the users table doesn't have a is_admin column, this will be fine.
As a note, having multiple boolean flags for something like this can be awkward. Is it possible to be an admin and an editor? You may be better off with a simple role column and then use that:
has_many :administrators,
:through => :memberships,
:source => :user,
:conditions => { :role => 'admin' }
A multi-purpose column is often better than a multitude of single-purpose columns from an indexing perspective. You will have to index each and every one of these is_admin type columns, and often you will need to do it for several keys. This can get messy in a hurry.
Related
I'm setting up a Rails project for a client, and they want Users (a model) to be able to follow each other (as in Twitter). They also want to be able to keep track of when one user started following another.
Since I need to keep track of the creation date, I figured, a has_many X, :through => Y relation would be the way to go, so the Y will keep track of the date it was created.
I have my Follow model set up:
class Follow < ActiveRecord::Base
attr_accessible :actor_id, :observer_id, :follower, :followee
attr_readonly :actor_id, :observer_id, :follower, :followee
belongs_to :follower, :class_name => 'User', :foreign_key => :observer_id
belongs_to :followee, :class_name => 'User', :foreign_key => :actor_id
validates_presence_of :follower, :followee
validates_uniqueness_of :actor_id, :scope => :observer_id
end
The question is how do I set up the relations in the User model?
Ideally I'd like it to have the following:
:follows would be the associated Follow objects where self is the follower (observer_id)
:followed would be the associated Follow objects where self is the followee (actor_id)
:following would be the associated User objects where self is the follower (observer_id)
:followers would be the associated User objects where self is the followee (actor_id)
I'm not sure how to write the has_many :through parts, though? Should I be using :source => X or foreign_key => X? And which key (actor_id or observer_id) should I put in each?
Edit: I'm currently doing this
has_many :follows, :foreign_key => :observer_id
has_many :followed, :class_name => 'Follow', :foreign_key => :actor_id
has_many :following, :class_name => 'User', :through => :follows, :source => :followee, :uniq => true
has_many :followers, :class_name => 'User', :through => :follows, :source => :follower, :uniq => true
and it's mostly working. All of them except :followers work fine, but user.followers is doing something weird. It seems like it's checking whether user is followING someone, and if they are then user.followers returns an array containing only user; if they're not, it returns an empty array.
Does anyone have any advice?
It looks like this is the correct format:
has_many :follows, :foreign_key => :observer_id
has_many :followed, :class_name => 'Follow', :foreign_key => :actor_id
has_many :following, :class_name => 'User', :through => :follows, :source => :followee, :uniq => true
has_many :followers, :class_name => 'User', :through => :followed, :source => :follower, :uniq => true
For Rails newbies, the :uniq => true is important (as I discovered while doing this) because it keeps has_many X, :through => Y relations from returning duplicates (that is, without it, you might get multiple distinct objects, each with their own object ID, all referring to the same record/row).
I have a relationship that is as follows.
companies_employee.rb
belongs_to :employee
belongs_to :company
validates_presence_of :role
employee.rb
has_many :companies_employees
has_many :companies, :through => :companies_employees
company.rb
has_many :companies_employees
has_many :managers, :through => :companies_employees, :source => :employee, conditions => {:role => "Manager"}
has_many :owners, :through => :companies_employees, :source => :employee, :conditions => {:role => "Owner"}
My problem is that when it checks the conditions, it tries to find the role column in the employees table, but the role column is in the companies_employees table.
Is there a way to make it use things in this table for the conditions?
Try something like this:
has_many :managers, :through => :companies_employees, :source => :employee, conditions => ["employees.role = 'Manager"]
has_many :owners, :through => :companies_employees, :source => :employee, conditions => ["employees.role = 'Owner"]
I have two models: Users and Projects. The idea is that Users can follow both projects AND other users. Naturally, Users and Projects are part of a polymorphic "followable" type. Now, using the user model, I'd like to get three things:
user.followed_users
user.followed_projects
user.followers
The first two work fine; It's the third that I'm having trouble with. This is sort of a reverse lookup where the foreign key becomes the "followable_id" column in the follows table, but no matter how I model it, I can't get the query to run correctly.
User Model
has_many :follows, :dependent => :destroy
has_many :followed_projects, :through => :follows, :source => :followable, :source_type => "Project"
has_many :followed_users, :through => :follows, :source => :followable, :source_type => "User"
has_many :followers, :through => :follows, :as => :followable, :foreign_key => "followable", :source => :user, :class_name => "User"
Follow Model
class Follow < ActiveRecord::Base
belongs_to :followable, :polymorphic => true
belongs_to :user
end
My follows table has:
user_id
followable_id
followable_type
Whenever I run the query I get:
SELECT `users`.* FROM `users` INNER JOIN `follows` ON `users`.`id` = `follows`.`user_id` WHERE `follows`.`user_id` = 7
where it should be "followable_id = 7 AND followable_type = 'User", not "user_id = 7"
Any thoughts?
Figured it out. Took a look at a sample project Michael Hartl made and noticed that the correct way to do this is to specify not only a relationship table (in this case follows) but also a reverse relationship table (which I called reverse follows).
has_many :follows,
:dependent => :destroy
has_many :followed_projects,
:through => :follows,
:source => :followable,
:source_type => "Project"
has_many :followed_users,
:through => :follows,
:source => :followable,
:source_type => "User"
has_many :reverse_follows,
:as => :followable,
:foreign_key => :followable_id,
:class_name => "Follow"
has_many :followers,
:through => :reverse_follows,
:source => :user
Hope this helps some people out down the line!
I think you need to explicitly spell out the foreign key. You have:
:foreign_key => "followable"
you need:
:foreign_key => "followable_id"
full code:
has_many :followers, :through => :follows, :as => :followable, :foreign_key => "followable_id", :source => :user, :class_name => "User"
Consider a "Name" model which has a required "label" attribute and an arbitrary Rails 3 model "Foo" with the following associations:
has_many :names, :dependent => :destroy
has_many :special_names, :through => :names, :source => :label, :conditions => { 'special_names.label' => 'special' }, :dependent => :destroy
Now it's possible to access the "special_names" attribute for reading the association, but writing to it fails because AR cannot infer from the condition that the "label" attribute needs to be set to "special" for all members of the "special names" association.
I attempted to use the "add_before" association callback, but that never gets called with the join model (instead the ":source" and "Foo" are used).
Any ideas on how to handle this in the model (as opposed to: using special logic in the controller to deal with this - that's how I handle it currently)?
Edit: (regarding the answer from Ray Baxter)
The relationship expressed is actually a "has_many :through" association. I'll try again, this time with a (hopefully) better example:
# Label is a shared entity which is used in many contexts
has_many :labels, :through => :user_labels
# UserLabel is the join model which qualifies the usage of a Label
has_many :user_labels, :dependent => :destroy
# special_user_labels is the topic of this question
has_many :special_user_labels, :through => :user_labels, :source => :label, :conditions => { 'user_labels.descriptor' => 'special' }, :dependent => :destroy
If my comment above is correct, and you aren't doing a has_many :through, this works:
has_many :special_names, :class_name => 'Name', :conditions => {:label => 'special'}, :dependent => :destroy
so now you can do
foo = Foo.create
foo.special_name.build
and ActiveRecord will correctly instantiate your special_name with the label attribute having the value "special".
I found the solution (thanks x0f#Freenode) - one needs to split the 'special' associations in two. has_many :special_user_labels, :through => :user_labels, :source => :label, :conditions => { 'user_labels.descriptor' => 'special' }, :dependent => :destroy becomes
1) has_many :special_labels, :class_name => 'UserLabel', :conditions => { :descriptor => 'special' }, :dependent => :destroy
2) has_many :special_user_labels, :through => :special_labels, :source => :label, :dependent => :destroy
Works for reading & writing as well as a seamless replacement for (scoped) hbtm associations.
I am trying to use one join model for two separate but very similar associations. Here is what I have:
Two primary models: Package, Size
Pacakges have many sizes but there's a wrinkle. The sizes need to be allocated as a size for top or bottom. My current associations on Package are:
has_many :package_sizes
has_many :sizes, :through => :package_sizes
has_many :bottoms_sizes, :through => :package_sizes, :scope => {:package_sizes => {:bodylocation => "B"}}, :source => :size
has_many :tops_sizes, :through => :package_sizes, :scope => {:package_sizes => {:bodylocation => "T"}}, :source => :size
PackageSize is a join model with: size_id | package_id | bodylocation:string
I have a failing test to verify it is working:
#p = Package.new
#size1 = Size.first
#p.tops_sizes << #size1
#p.save
#p.reload
#p.tops_sizes.should include(#size1)
This should work properly but for some reason the bodylocation field does not get automatically set.
Any ideas?
There is (IMHO) a better solution to this in the answer to: Scope with join on :has_many :through association.
Essentially it would be something like:
has_many :package_sizes
has_many :sizes, :through => :package_sizes do
def tops
where("package_sizes.bodylocation = 'T'")
end
def bottoms
where("package_sizes.bodylocation = 'B'")
end
end
You would then query for them like:
#p.sizes.tops
Try creating two separate through associations for this.
has_many :bottom_package_sizes, :class_name => 'PackageSize', :conditions => {:bodylocation => 'B'}
has_many :top_package_sizes, :class_name => 'PackageSize', :conditions => {:bodylocation => 'T'}
has_many :bottom_sizes, :through => :bottom_package_sizes
has_many :top_sizes, :through => :top_package_sizes