How to setup and work with RoR Complex Joins? - ruby-on-rails

This code is taken from a previous question, but my question directly relates to it, so I've copied it here:
class User < ActiveRecord::Base
has_many :group_memberships
has_many :groups, :through => :group_memberships
end
class GroupMembership < ActiveRecord::Base
belongs_to :user
belongs_to :role
belongs_to :group
end
class Role < ActiveRecord::Base
has_many :group_memberships
end
class Group < ActiveRecord::Base
has_many :group_memberships
has_many :users, :through > :group_memberships
end
New Question Below
How you take the above code one step further and add and work with friends?
class User < ActiveRecord::Base
has_many :group_memberships
has_many :groups, :through => :group_memberships
has_many :friends # what comes here?
has_many :actions
end
In the code above I also added actions. Let's say that the system kept track of each user's actions on the site. How would you code this so that each action was unique and sorted with the most recent at the top for all the user's friends?
<% for action in #user.friends.actions %>
<%= action.whatever %>
<% end %>
The code above may not be valid, you could do something like what I have below, but then the actions wouldn't be sorted.
<% for friend in #user.friends %>
<% for action in friend.actions %>
<%= action.whatever %>
<% end %>
<% end %>
UPDATE
I guess the real issue here is how to define friends? Do I create a new model or join table that links users to other users? Ideally, I'd like to define friends through the common group memberships of other users, but I'm not sure how to go about defining that.
has_many :friends, :through => :group_memberships, :source => :user
But that doesn't work. Any ideas or best practice suggestions?

It's been a while since I've worked with Rails (think Rails 1.x), but I think you can do something like the following:
Action.find(:all, :conditions => ['user_id in (?)', #user.friends.map(&:id)], :order => 'actions.date DESC')
and then in your view:
<% #actions.each do |action| %>
<%= action.whatever -%>
<% end %>

To add friends you can use has_and_belongs_to_many or has_many :through. Here is example how to do it. You should self join users table.
To list recent actions, get them from db using :order => :created_at (or :updated_at). Than you can filter out actions that not belongs to users.
#actions = Action.all(:order => :created_at).select {|a| a.user.friends.include?(#user)}
Probably you can also write sql query to do this.

You could make a "friend" object that uses the "user" table and go from there or you could
define a friend as a partial entity (friend name and user_id reference) and pull the user object into the friend object.
I like this last model better because it allows you to represent "deleted" friends more easily. You would still have a copy of the friend name (even if it is a duplicate of the user name) and the ID to identify if the user is gone or not. Keeps the "friend" concept a little further away from the "user" concept and any security implications. I will argue it's appropriate de-normalization to duplicate the "name" field as it adds flexibility and "shadow" information about a user that used to exist.
My take on it, though. You would still have to eventually self-join back to the user table, but perhaps not as much. If you REALLY hate to de-normalize anything you can have a Person object that Friend and User both refer to for information. Personally, I think that is a nice theoretical construction, but not worth the complexity it would introduce into the bulk of your code (80% of it being talking about the user and 20% talking about the friend). I would make the user easy to talk about and the friend a little hard... not both. :)
Hope this helps. I enjoyed mentally toying with this one. :)

Related

Rails has_one :through with different model name

A transaction_record has many workflows, and each workflow has many milestones. One of the milestones is marked current: true, and I want to go from the transaction_record to the current_milestone:
class TransactionRecord < ApplicationRecord
has_many :workflows
has_many :milestones, through: :workflows
# DOES NOT WORK, but what I want to do...
has_one :current_milestone, through: :workflows, class: Milestone, source: :milestones
# Works, but want to make an association for including
def current_milestone
milestones.where(current: true).first
end
end
class Workflow < ApplicationRecord
belongs_to :transaction_record
has_many :milestones
end
class Milestone < ApplicationRecord
belongs_to :workflow
end
I can create a method that returns the desired milestone, but I want to make it an actual association so I can include it for DB performance.
I have a transaction_records#index page where I list the transaction_records and the current_milestone for each one. That's an n+1 unless I can figure this out.
I really want to be able to do something like:
#transaction_records = TransactionRecord.includes(:current_milestone)
<% #transaction_records.each do |transaction_record| %>
<%= transaction_record.name %> - <%= transaction_record.current_milestone.name %>
<% end %>
update
I can specify a direction relationship between transaction_record and milestone, and then do transaction_record has_one :current_milestone, -> { where(current: true) }, class_name: Milestone. But now I'm changing my DB schema for a more efficient load query. Not the end of the world, but not my preference if I already have an association.
To be honest I don't like the concept, that transaction_record has some kind of active_milestone without any mentioning about joining current_workflow.
Good solution is to think about both workflow and milestone to have an ability to be current and then:
class TransactionRecord < ApplicationRecord
has_one :current_workflow, ....
has_one :current_milestone, through: current_workflow, ....
end
class Workflow < ApplicationRecord
has_one :current_milestone, condition: ....
end
This is far better for me, but you still need to add additional current flag attribute in workflow.
That's why better solution is rework your concept at all.
Remove current from milestone and add current_milestone_id to workflow. If it's nil, then this workflow has no current_milestone. If it contains some id, then this is your current_workflow and current_milestone_id.
Code will look pretty the same, but it will don't have ugly condition in Workflow

Rails has_many sums and counts with ActiveRecord

I've got a dilemma I think I may have coded myself into a corner over. Here's the setup.
My site has users. Each user has a collection of stories that they post. And each story has a collection of comments from other users.
I want to display on the User's page, a count of the total number of comments from other users.
So a User has_many Stories, and a Story has_many comments.
What I tried was loading all the users stories in #stories and then displaying #stories.comments.count, but I get undefined method 'comments' when I try to do that. Is there an efficient ActiveRecord way to do this?
class User < ActiveRecord::Base
has_many :stories
has_many :comments, through: :stories
end
class Story < ActiveRecord::Base
belongs_to :user
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :story
end
Now you should be able to get User.last.comments.count
I think you need to refine this more for a proper labelling.
The quick solution is to iterate over the #stories collection and add the counts up. This is not a purely active record solution though.
totalComments = 0
#stories.each do |story|
totalComments += story.count
end
For a pure active record solution I would need to assume that each has_many association has a corresponding belongs_to association. So a User has_many Stories and a Story belongs_to a User. If that is the case and comments have a similar association to stories then you can search comments by user_id. Something like:
Comments.where("comment.story.user" => "userId")
I hope that helps.
In your controller you should have something like this (note the use of includes):
#user = User.find( params[:id] )
#stories = #user.stories.includes(:comments)
Then in your view you can do something like the following to show the total number of comments for that particular user:
Total number of comments: <%= #stories.map{|story| story.comments.length}.sum %>

How do I sum a many to many value?

Each User can have many Resources, and each of those Resources has many Votes, and each of those votes have a value attribute that I want to sum all that particular users resources.
If I were to type this in a syntactically incorrect way I want something like...
#user.resources.votes.sum(&:value), but that obviously won't work.
I believe I need to use collect but I am not sure?
This is the closest I got but it prints them out, heh
<%= #user.resources.collect { |r| r.votes.sum(&:value) } %>
I'd recommend setting up a has_many :through relationship between the User and Vote objects. Set the models up like this:
class User < ActiveRecord::Base
has_many :resources
has_many :votes, :through => :resources
end
class Resource < ActiveRecord::Base
belongs_to :user
has_many :votes
end
class Vote < ActiveRecord::Base
belongs_to :resource
end
Once this is done you can simply call user.votes and do whatever you want with that collection.
For more info on has_many :through relations, see this guide: http://guides.rubyonrails.org/association_basics.html#the-has_many-through-association
How can you tell who voted having a Vote instance? Your Vote model has to have voter_id field and additional association:
# in Vote.rb
belongs_to :voter, class_name: 'User', foreign_key: 'voter_id'
And in your User model:
# in User.rb
has_may :submited_votes, class_name: 'Vote', foreign_key: 'voter_id'
So, #user.votes (as David Underwood proposed) will give you #user resources' votes. And #user.submited_votes will give you votes submitted by the #user.
Using just User <- Resource <- Vote relation won't allow you to separate some user's votes made by him and votes made for its resources.
For a total sum this should work or something real close.
sum = 0
#user.resources.each do |r|
r.votes.each do |v|
sum += v.value
end
end
This might work for you:
#user.resources.map {|r| r.votes.sum(:value)}.sum
How many records do you have, there is a way to push this to the database level I believe, I would have to check, but if it is only a few records then doing this in ruby would probably be ok
Try this code
#user.resources.map(&:votes).flatten.map(&:value).sum

Nested form - how to add existing nested object to parent form?

I've got my models setup for a many-to-many relationship:
class Workshop < ActiveRecord::Base
has_many :workshop_students
has_many :students, :through => :student_workshops
accepts_nested_attributes_for :students
end
class Student < ActiveRecord::Base
has_many :student_workshops
has_many :workshops, :through => :student_workshops
accepts_nested_attributes_for :products
end
class StudentWorkshop < ActiveRecord::Base
belongs_to :student
belongs_to :workshop
end
As you can see above, a student can have many workshops and workshop can have many students.
I've looked at the following Rails casts: here and here. And most of the online sources I stumble across only show how to do nested forms for creating new objects within the parent form.
I don't want to create a new object. I only want to add an existing object to the parent form. So for example. If I decide to create a new workshop, I'd like to assign existing students to the workshop.
One thing I don't understand is, how do I link students into the workshop form? Second, when the params are passed, what should be in the controller method for update/create?
If anyone can point me to the right direction, I would appreciate it.
The easiest thing to do is:
<%= f.collection_select(:student_ids, Student.all, :id, :name, {:include_blank => true}, {:selected => #workshop.student_ids, :multiple => true} )%>
You should not have to do anything in the create action.
Ok, for anyone coming across the same issue in the future. The solution I came up with was in def create. I am able to access a POST attribute called student_ids, which comes in the form of an array

Trying to get counts through multiple associations

I have created a question an answer app for a client much like StackOverflow. I am not trying to implement some sort of point system (like SO reputation). I am trying to get certain record counts through associations (which I believe are set up correctly). Primarily I am trying to get counts for votes on users answers. Here is an example.
In /views/questions/show page I list all the answers to that question by calling a partial _answer.html.erb. With each answer I pull in the answer.user information (username, email, etc.) by simply doing answer.user.username. I am wanting to display in a badge like format some total point calculations. So if User A answered Question A, next to User A's answer I want to display a total of all User A's answer votes.
I can successfully get a count for a users answers in /views/answers/_answer.html.erb by doing the following:
<%= answer.user.answers.count %>
but when I try to extend that syntax/association to get a count of votes on all User A's answers I get undefined method errors.
<%= answer.user.answers.votes.count %>
Is my set up fundamentally wrong here or am I missing something.
That is a bit confusing so let me know if you need more detail.
UPDATE:
Here are the associations:
Answers
class Answer < ActivRecord::Base
belongs_to :question
belongs_to :user
has_many :votes, :dependent => :destroy
end
Votes
class Vote < ActiveRecord::Base
belongs_to :answer
belongs_to :user
belongs_to :question
end
Users
class User < ActiveRecord::Base
has_many :questions, :dependent => :destroy
has_many :answers, :dependent => :destroy
has_many :votes, :through => :answers , :dependent => :destroy
end
<%= answer.user.answers.votes.count %>
but
answer.user.answers
is an array of answers so I suppose you wanted something like
<%= answer.user.answers[id].votes.count %>
UPDATE
<% answer.user.answers.each do |answer| %>
<%= answer.votes.count%>
<% end% >
UPDATE
<%= (answer.user.answers.map{|x| x.votes.count}).sum%>
I LOVE rails for such things
but when I try to extend that syntax/association to get a count of votes on all User A's answers I get undefined method errors.
You can find complete listing of what you can do with user.answers collection in here (besides standard Array/Enumerable methods). And it's only collection instance, not Answer object, so you can't invoke methods from Answer model on it.
You can try setupping has_many :votes, :through => :answers relationship. (See here for details) Although, I'm not sure if :through would work in such case.
Alternatively, you can create a method in User model to return all Vote objects (simply by iterating through all answers)
But frankly, creating a horde of Vote objects simply to count them sounds like a terrible waste of resources to me.

Resources