Rails 3: Many to many relationship with polymorphic association - ruby-on-rails

I have 2 models e.g task model and task_relation model
Task has many parent tasks and child tasks.
Have added following associations -
Task.rb
has_many :from_tasks, :as => :relation, :class_name => "TaskRelation",
:foreign_key => "task_from_id", :source => :parent,
:conditions => {:relation_type => 'Parent'}, :dependent => :destroy
has_many :to_tasks , :as => :relation, :class_name => "TaskRelation",
:foreign_key => "task_to_id", :source => :child,
:conditions => {:relation_type => 'Child'}, :dependent => :destroy
has_many :child_tasks, :through => :from_tasks, :dependent => :destroy
has_many :parent_tasks, :through => :to_tasks, :dependent => :destroy
accepts_nested_attributes_for :to_tasks, :reject_if => :all_blank, :allow_destroy => true
accepts_nested_attributes_for :from_tasks, :reject_if => :all_blank, :allow_destroy => true
TaskRelation.rb
belongs_to :parent_task, :class_name => "Task", :foreign_key => "task_from_id"
belongs_to :child_task, :class_name => "Task", :foreign_key => "task_to_id"
belongs_to :relation, :polymorphic => true
When I save task form, it also saves parent_tasks and child tasks in task_relations table with relation_type as 'Task' but I want to store relation_type as 'Parent' for parent tasks and 'Child' for child tasks.
Can anyone please help me on this.

Firstly, remove the relation polymorphic association - it's not needed. Now, modify your Task model to look like this:
# This association relates to tasks that are parents of self
has_many :parent_task_relations, class_name: 'TaskRelation', foreign_key: 'child_task_id'
# And this association relates to tasks that are children of self
has_many :child_task_relations, class_name: 'TaskRelation', foreign_key: 'parent_task_id'
has_many :child_tasks, :through => :child_task_relations
has_many :parent_tasks, :through => :parent_task_relations
And you should be done.
To illustrate how this might be used - say you have a Task a and need to assign task B as a parent, and task C as a child. You could accomplish this like so:
a.parent_tasks << b
a.child_tasks << c
This would have the same effect on your database as this code:
a.parent_task_relations.create(parent_task: b)
a.child_task_relations.create(child_task: c)
Which is the same (to the database) as:
TaskRelation.create(parent_task: b, child_task: a)
TaskRelation.create(parent_task: a, child_task: c)

Related

How do I set up a directed many-to-many self-join using :through?

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).

has_many through relationship with conditions based on relationship table (Rails 2.3.5)

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"]

Reverse has_many with polymorphism

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"

Writing to has_many :through associations and callbacks

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.

How can one obtain a row count from has_many :through relations with :uniq => true

This is my model:
class Tag < ActiveRecord::Base
# id, name
has_many :taggings
end
class Tagging < ActiveRecord::Base
# id, tag_id, owner_id, target_type, target_id
belongs_to :tag
belongs_to :owner, :class_name => 'User'
belongs_to :target, :polymorphic => true
validates_uniqueness_of :tag_id, :scope => [ :target_id, :target_type, :owner_id ]
end
class Asset < ActiveRecord::Base
# id, owner_id, title, type, etc
belongs_to :owner, :class_name => 'User'
has_many :taggings, :as => :target
has_many :taggers, :through => :taggings, :source => :owner, :uniq => true
has_many :tags, :through => :taggings, :uniq => true
end
class User < ActiveRecord::Base
# id, name, email, etc
has_many :assets, :foreign_key => 'owner_id'
has_many :my_taggings, :class_name => 'Tagging', :foreign_key => 'owner_id'
has_many :my_tags, :through => :my_taggings, :source => :tag, :uniq => true
has_many :taggings, :as => :target
has_many :taggers, :through => :taggings, :source => :owner, :uniq => true
has_many :tags, :through => :taggings, :uniq => true
end
All of the relations are working but I have an additional requirement that I can't find the solution for:
consider this relation in the Asset class
has_many :tags, :through => :taggings, :uniq => true
calling Asset.find( :first ).tags returns an array of Tags as expected but I need for each Tag to contain a count attribute indicating how many times the row would have appeared if :uniq => true was not specified.
eg. more than one User could apply the same Tag to an Asset. I'd like to display the tag name plus the number of users that applied it.
This should do exactly what you want.
has_many :tags_with_count, :source => :tag, :through => :taggings,
:group => "tags.id", :joins => :taggings,
:select = "tags.*, COUNT('taggings.id') AS frequency"
In terms of rows returned :group => :id will return the same set as :uniq => true, but it will also allow you to perform the calculations you want. This statement is more labour intensive than :uniq => true, so I've given it a different name allowing you to choose whether to fetch the unique tags with their grouped counts, or just the list of unique tags.
The above statement will add the frequency attribute to the records returned. Through the magic of method_missing, you can access that with #tag.frequency.
Usage:
#tags = #asset.tags_with_count
#tags.each{|tag| puts [tag.id, tag.name. tag.frequency].join "\t"}
Will print the id, name, and number of occurrences of each tag for #asset.

Resources