Multiple association types between 2 models - ruby-on-rails

The following are my three models: many users can each have many products (and vice versa) through an associations model.
class Product < ActiveRecord::Base
has_many :associations
has_many :users, :through => :associations
end
class User < ActiveRecord::Base
has_many :associations
has_many :products, :through => :associations
end
class Association < ActiveRecord::Base
belongs_to :user
belongs_to :product
end
This works great. But now I want to add 2 different association types, a 'strong-association' and 'weak-association' on top of the original association.
What is the best way to go about this?
So far I've considered 1) adding a type column to my association table to specify whether it is a strong/medium/weak association 2) adding a strong-association and also a weak-association record. Both methods seem troublesome when I want to display only a particular association type in my rails view.
I also want to be able to easily change the association type in my project.

You should definitely add a new field to your associations join table: this is the correct way to store this relationship. There's various things you could do after that.
You could add some new has_many associations:
class Product < ActiveRecord::Base
has_many :associations
has_many :users, :through => :associations
has_many :weak_associated_users, :class_name => "User", :through => :associations, :source => :user, :conditions => ["associations.strength = ?", "weak"]
has_many :medium_associated_users, :class_name => "User", :through => :associations, :source => :user, :conditions => ["associations.strength = ?", "medium"]
has_many :strong_associated_users, :class_name => "User", :through => :associations, :source => :user, :conditions => ["associations.strength = ?", "strong"]
end
class User < ActiveRecord::Base
has_many :associations
has_many :products, :through => :associations
has_many :weak_associated_products, :class_name => "Product", :through => :associations, :source => :product, :conditions => ["associations.strength = ?", "weak"]
has_many :medium_associated_products, :class_name => "Product", :through => :associations, :source => :product, :conditions => ["associations.strength = ?", "medium"]
has_many :strong_associated_products, :class_name => "Product", :through => :associations, :source => :product, :conditions => ["associations.strength = ?", "strong"]
end
#fields: user_id, product_id, strength
class Association < ActiveRecord::Base
belongs_to :user
belongs_to :product
end
And then do something like (on the page)
<h2>Strongly association users</h2>
<% #product.strong_associated_users.each do |user| %>
...show user info here
<% end %>
Or, you could not bother with the new has_many associations, and just split the association records up on the page:
<% grouped = #product.associations.find(:all, :include => [:user]).group_by(&:strength) %>
<% ["weak", "medium", "strong"].each do |strength| %>
<% if associations = grouped[strength] %>
<h2><%= strength %> associations</h2>#
<% associations.each do |association| %>
<% user = association.user %>
...show user info here
<% end %>
<% end %>
<% end %>

If you want to change the association type I would recommend the extra column for your association table. You can filter out what kinds of records you want to show in your controller action by first gathering up all the associations and then calling a method like #select. For example, to get all the 'weak' associations you could use the following code in your controller action (I'm just using a "show" action for an example. I'm not sure what your situation calls for):
def show
#products = current_user.products.select! { |p| p.association_type == "weak" }
#associations scoped through current_user
#also, asociation_type is just a placeholder name for whatever you name that extra column
render :show ##products are all products with "weak" association
end
You could change what select looks for according to what type of association you want.

Related

Ruby on Rails has_many_through with validation and logic to ensure only one record in the join table which is updated as required

I have the Course model which has a number of has many through associations with the User model with join table CourseUser. The join table has an attribute type_str which specifies which role the user takes on. I have added validation to ensure that only one record is present in the join table for each course, user pair. The problem is ensuring that this record is updated if it is already present, rather than adding a new one which of course makes validation fail.
User class:
class User < ActiveRecord::Base
...
has_many :courses_enrolled_on, :through => :course_enrollees, :source => :course, :conditions => { :course_users => { :type_str => "enrollee" } }
has_many :course_users
has_many :courses, :through => :course_users, :source => :course, :readonly => true
end
Course class
class Course < ActiveRecord::Base
has_many :course_enrollees, :conditions => { :type_str => "enrollee" }, :class_name => CourseUser
has_many :enrollees, :through => :course_enrollees, :source => :user
has_many :course_users
has_many :users, :through => :course_users, :source => :user, :readonly => true
end
Course class:
class CourseUser < ActiveRecord::Base
belongs_to :course
belongs_to :user
validates_uniqueness_of :course_id, :scope => :user_id
end

ordering alphabetically in a select_tag, joined table in rails

I have this line of code:
<%= select_tag :friendship_id, options_from_collection_for_select(current_user.friendships, "id", "name", selected = nil) %>
Which selects the 'name' of the record in the 'friends' table and puts it as an option on the select_tag
Friendships has a friendship_id that links to the friends table. I call the name using
def name
self.friend.name
end
In the friendships controller
The association works cause I see the list of names on the webpage.
I would like to:
order that alphabetically
add a "select a friend..." at the top of the list
And I haven't really found anything yet.
For issue 2, i add selected = nil to no avail.
Thanks in advance for your help
EDIT (ANSWER):
I ended up chaging the way the collection was fetched, and looked for the friends instead of the friendships.
<%= select_tag :friend_id, options_from_collection_for_select(current_user.friends, "id", "name"), :prompt => 'Select a friend...', :id => 'thought_contact_select' %>
In my User model
has_many :friends, :through => :friendships, :order => :name
The i just look for the friendship associated for that friend and that user on the controller
friendship = Friendship.find_by_user_id_and_friend_id(current_user.id, params[:friend_id])
Use default_scope :name but use it once! You should have one default scope. Or, as you are accessing the friendships through has_many association use has_many :friendships, :order => 'name DESC' see description of has_many here: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
Use :prompt option to select_tag, see: http://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html#method-i-select_tag
EDIT:
If I understood you, you should have:
class User < ActiveRecord::Base
has_many :friendships
has_many :friends, :through => :friendships
end
class Friendship < ActiveRecord::Base
belongs_to :user
belongs_to :friend
end
class Friend < ActiveRecord::Base
has_many :friendships
has_many :users, :through => :friendships
end
Try to change the User class to:
class User < ActiveRecord::Base
has_many :friendships
has_many :friends, :through => :friendships, :order => 'name'
end

Rails - Troubles in getting attributes from relations

Those are my models:
class Person < ActiveRecord::Base
has_many :honors, :dependent => :destroy, :foreign_key => "honored_id"
has_many :honor_creators, :through => :honors, :source => :person, :foreign_key => "person_id"
class Group
has_many :honorss,:foreign_key => "group_id"
class Honor
belongs_to :person, :class_name => 'Person', :foreign_key => "person_id"
belongs_to :honored, :class_name => 'Person', :foreign_key => "honored_id"
belongs_to :group, :class_name => 'Group', :foreign_key => "group_id"
Since the honors are shown at the person#show page, here is my controller:
def show
...
#honors = #person.honors.paginate(:page => params[:page], :per_page => Honor.per_page)
end
And my view:
<% unless #honors.empty? %>
<% #honors.each do |ho| %>
My question is: using the ho I get all the attributes from the honor, but I want to get the creater of the honor and the group that it belongs. How can I do that?
Thanks!
You can do that by accessing
ho.person
or
ho.group
Note:
belongs_to :group, :class_name => 'Group', :foreign_key => "group_id"
and
belongs_to :group
will do the same thing because Rails will assume that class and foreign key when you give it :group.
To get ho.honor_creator to work (If I'm assuming correctly), you'll want:
class Person < Model
has_many :honors, :foreign_key => :creator_id
end
class Honor < Model
belongs_to :creator, :class_name => "Person"
end
That way, you can set honor.creator_id = params[:user_id] and then access the Person with ho.creator.
In other words, I'd redo your models like this:
class Person < ActiveRecord::Base
belongs_to :honor, :foreign_key => :recipient_id
has_many :honors, :foreign_key => :creator_id
class Group
has_many :honors
class Honor
has_many :recipients, :class_name => 'Person' # recipient_id is assumed
belongs_to :creator, :class_name => 'Person' # creator_id is assumed
belongs_to :group # group_id is assumed
I renamed some columns to hopefully make better sense, and this is done off the top of my head, so I'll look at some reference to ensure my accuracy.
This will give you access to honor.creator and honor.group.
<% #honors.each do |honor| %>
Honor Creator: <%= honor.creator.name %>
Honor Group: <%= honor.group.name %>
Honor Recipients: <% honor.recipients.each {|recipient| puts recipient} %>
<% end %>
Might have messed up the recipient association, but I'll go look.

Eager loading nested association and scope

I'm a beginner and it's hard to explain my problem:
My models:
class Skill
has_many :categories
has_many :positions, :through => :categories
end
class Category
belongs_to :skill
has_many :positions
end
class Position
belongs_to :category
has_one :skill, :through => :category
end
I can successfully eager load everything, like this:
#skills = Skill.includes(:positions)
However sometimes I want to apply a scope on the Positions:
class Position
...
scope :active, where(:hidden => false)
end
I wish I could do:
#skills = Skill.includes(:positions.active)
Instead, I apply the scope in the views, but the eager loading doesn't work anymore:
<%= skill.positions.acitve ... %>
Is it possible to have both eager loading and scope?
You could use another association:
class Skill
has_many :categories
has_many :positions, :through => :categories
has_many :active_positions, :through => :categories
end
class Category
belongs_to :skill
has_many :positions
has_many :active_positions, :class_name => "Position", :conditions => {:hidden => false}
end
class Position
belongs_to :category
has_one :skill, :through => :category
end
And then
#skills = Skill.includes(:active_positions)
But then you'll get two associations. If you ever use skill.positions, all the skill's positions will be loaded from the database. You should only use skill.active_positions.
Try this:
#skills = Skill.includes(:positions).where('position.active = TRUE')

Rails - Two-way "friendship" model (cont'd)

This is a continuation of this question:
Original Question (SO)
The answer to this question involved the following set of models:
class User < ActiveRecord::Base
has_many :friendships
has_many :friends, :through => :friendships #...
end
class Friendship < ActiveRecord::Base
belongs_to :user
belongs_to :friend, :class_name => 'User', :foreign_key => 'friend_id'
end
<% for friendship in #user.friendships %>
<%= friendship.status %>
<%= friendship.friend.firstname %>
<% end %>
This works fine if say, I have a user and I want to get all the "friendships" for which his or her id is the :user_id FK on the Friendship model. BUT, when I run something like
#user.friendships.friends
I would like it to return all User records for which that User is either the :user or the :friend in the friendship - so, in other words, return all friendships in which that user is involved.
Hopefully the above makes sense. I'm still quite new to rails and hope there is a way to do this elegantly without making just a standard link table or providing custom SQL.
Thank you!
Tom
railscasts episode on this topic
You cannot just use #user.friendships here because it will only give you those friendships where #friendship.user_id == #user.id.
The only thing I can think of right now is just to do
Friendship.find_by_user_id(#user.id).concat Friendship.find_by_friend_id(#user.id)
my solution is a a scope:
# model
class Message < ActiveRecord::Base
belongs_to :sender, :class_name => "User"
belongs_to :recipient, :class_name => "User"
scope :of_user, lambda { |user_id| where("sender_id = ? or recipient_id = ?",
user_id, user_id) }
end
# in controller
#messages = Message.of_user(current_user)
From the railscast link:
# models/user.rb
has_many :friendships
has_many :friends, :through => :friendships
has_many :inverse_friendships, :class_name => "Friendship", :foreign_key => "friend_id"
has_many :inverse_friends, :through => :inverse_friendships, :source => :user

Resources