Top Ranked Item from Associations - ruby-on-rails

I shall start by describing my associations. I have 6 resources. NationalOffice, Programme, Village, Discussion, Conversation, and Change. A NationalOffice has many Programmes. A Programme has many Villages. A Village has many Discussions. A Discussion has many Conversations. A Conversation has one (belongs_to) a Change.
In each Conversation a Change is talked about. The Conversation then gives the Change a rank. This is the table schema:
create_table "conversations", force: true do |t|
t.integer "discussion_id"
t.integer "change_id"
t.integer "rank" # 1 for 1st, 2 for 2nd, 3 for 3rd, ect.
end
What I want to do is the following: From the Discussion class I want to be able to pick out the top change. I have crudely implemented that with a helper:
def top_change discussion
conversation = discussion.conversations.order(:rank).first
# Incase there are no conversations for the discussion
if conversation.respond_to?('change')
conversation.change.name
else
'N/A'
end
end
If I take that up a level to the Village class I have an issue. How would I go through all the Discussions in a Village and find the top scoring Change? I would also have the same problem when trying it on the Programme class. And then the NationalOffice class.
This may be achievable through SQL or activerecord - I'm not certain.
I hope I have made myself clear - I sometimes have issues with clarity.
Edit:
It has been made apparent that I have not explained the Ranks correctly. I shall now attempt to explain how they work:
Each Conversation has a rank. Each Conversation is about a specific Change. So, if a Conversation about Change 1 is ranked 1st in that Discussion *Change* 1 will gain a 1st. In another Discussion a Conversation regarding Change 1 is ranked 2nd. So Change 1 now has a 1st and a 2nd (3 points?).
2 Discussions each have a Conversation that talks about Change 2. One Ranks it 1st the other 3rd. Change 2 has a 1st and a 3rd (4 points?)
Over all, Change 1 was the top change - it had less points (higher scoring) than Change 2.
Hopefully that is clear. This is the full application on github, just for context.

You can use has_many :through to achieve this.
http://ryandeussing.com/blog/2013/06/12/nested-associations-and-has-many-through/
Has_many through will add extra conditionals to join between 3 or more tables. So you should be able to use this for all the classes "above" Conversation.
Basically you can chain has_many :through as many times as you need to find the top conversation for any of the associated models.
In your models:
class Discussion < AR::Base
belongs_to :village
has_many :conversations
end
class Conversation < AR::Base
belongs_to :discussion
has_one :change
end
class Village < AR::Base
has_many :discussions
has_many :conversations, through: :discussions
end
class Programme < AR::Base
has_many :villages
has_many :conversations, though: :villages
end
In your controller:
class VillageController < ApplicationController
def top_change
village = Village.find(params[:id])
#change = village.conversations.order(:rank).first.change
end
end
class ProgrammesController < ApplicationController
def top_change
programme = Programme.find(params[:id])
#change = programme.conversations.order(:rank).first.change
end
end

I ended up working through the answer with James Strong in the #rubyonrails channel on freenode irc network. Here's what we got:
In the change model we added two actions:
def self.with_rank(load_conversation=true)
q = select("changes.*, SUM(11 - conversations.rank) as score").group("changes.id")
q = q.joins(:conversations) if load_conversation
return q
end
This went through all the conversations associated with a change, it then SUMmed all the ranks (but we made it so that higher was better. I.E. if the rank was 1: 1 - 11 = 10) and put it into the table as *conversations_rank*. It then joins the changes table with the conversations table.
def self.top_ranked(load_conversation=true)
with_rank(load_conversation).order("score desc").limit(1)
end
This next little snippet returns the top ranked change. It's pretty self explanatory.
In the Villages model we added a action that merged the top_rank query with the changes from that model (Villages.changes).
def top_change
changes.merge Change.top_ranked(false)
end
It also had the following association:
has_many :conversations, through: :discussions
This allows me to then do the following to get the top change's name:
#village.top_change.name
The biggest thing I have taken away from this experience it to break down big queries like this into small manageable chunks. For this we said:
Get changes with ranks based on conversions (their scores combined)
Get the highest rank
Get the highest rank within a village
Thanks again to jstrong in the IRC and all those who helped.

Related

How do you identify models that contain related records in Ruby on Rails?

I'm sure my wording isn't great which explains why I'm failing at searching. I'm trying to work out how to identify records that are related to two others. Example will make it clearer.
Model Contest has a has_many relation to Results. The results have a Team number(but lets use letters for Clarity)
So in this example I'm trying to find all the Contests that Team A and Team D have both attended.
I want to get back a enumerator of all the Contests that fit this condition so I can then compare the two teams to each other.
I apologize for this not being the best write up, I'm struggling for the terms to define what I'm trying to do. Thank you for your help, time and patience!
Given:
class Contest < ApplicationRecord
has_many :results
has_many :teams, through: :results
end
class Result < ApplicationRecord
has_many :teams
belongs_to :contest
end
class Team < ApplicationRecord
belongs_to :result
delegate :contest, to: :result
end
This will set up your associations, and let you access Contest from Team. The delegate method says "if I call team.contest, do team.result.contest instead".
Hope that helps!
EDIT
And if you're wanting to gather the contests:
Contest.joins(results: :teams).where(teams: { id: team_a.id })
Contest.joins(results: :teams).where(teams: { id: team_b.id })
The problem is you would need to join the same table twice, once with team A results and once with team B results. You need to reference them as separate tables. You can use Arel and assign unique names to the two instance of the table.
contests_between = Arel::Table.new(:contests, as: 'contests_between')
team_a_results = Arel::Table.new(:results, as: team_a_results)
team_b_results = Arel::Table.new(:results, as: team_b_results)
relation = contests_between.project(Arel.sql('*')).
join(team_a_results).on(contests_between[:id].eq(team_a_results[:contest_id])).
join(team_b_results).on(contests_between[:id].eq(team_b_results[:contest_id]))
And then you can do...
relation = relation.where(team_a_results[:team_id].eq(1)).where(team_b_results[:team_id].eq(2))
#contests = relation.to_sql
# above assumes team_a is id 1 and team_b is id 2... if the teams are already loaded you could substitute team_a.id and team_b.id

Rails Sort by join table row

I have a many to many relastionship through join table :
team.rb
has_many :partners_teams
has_many :partners, through: :partners_teams
partner.rb
has_many :partners_teams
has_many :teams, through: :partners_teams
partners_team.rb
belongs_to :team
belongs_to :partner
self.primary_key = :partner_id
include RankedModel
ranks :row_order, with_same: :team_id
default_scope { order(row_order: :asc) }
I set an additional row "row_order" integer in the join table teams_partners
Partner is a nested resource of Team
How can I address the row_order column when I request the partners from a designed card, which happens in partners#index and cards#show
Currently, I do (it works correctly) :
Partners#index :
def index
#card = Card.find(params[:id])
#partners = #team.partners.all
end
Teams#show
def show
#partners = #team.partners.all
end
I tried several things with joins and include but with no success. It's still a bit complicated to my level.
Moreover, I use Harvest ranked-model gem to sort my partners. It seems to work well except the initial order (problem described above). Thing is ranked-model use the .rank() method to order things. Ex: Partners.rank(:row_order). I'm not sure if that's a thing to take into account, I mean I can use an Partners.order(row_order: :desc), but maybe that'll have an impact for the following sorting.
Any help appreciated, really.
Thank you a lot.

How to create voting to a multiple choice application using ruby on rails

I am working on a multiple choice question and answer application using Ruby on Rails and I have the following model.
class User < ActiveRecord::Base
has_many :questions
end
class Question < ActiveRecord::Base
belongs_to :user
has_many :answers
end
class Answer < ActiveRecord::Base
belongs_to :question
has_many :votes
end
class Vote < ActiveRecord::Base
belongs_to :user
belongs_to :answer
end
My problem is a user can choose all the answers.
How can I fix it so that it updates to the new answer?
For example, a has 5 votes and b 3 votes.
User clicks on a and a increments to 6 votes, same user comes back and clicks on b, a decrements back to 5 votes and b increments to 4 votes.
My first guess is that I need to add another model for example, user_choice with user_id and vote_id to track the previous answer.
You have your answer within the models. Just add a point system to the question model.
Under the question model,
def points
self.answers.count
end
Or, if answers have different values attached to them, make points an attribute of the answer instances.
def points
self.answers.pluck(:points).inject(:+)
end
This will sum up all the points from an answer belonging to a question, which allows you to order the questions by point count.
However, I am assuming you will need a Choice model for a question, unless that is in fact what the votes are for.
question has_many choices
choices belong_to question, etc.
I'm not sure what you mean when you say:
How can I fix it so that it updates to the new answer?
EDIT
Er, okay so you just need exactly what I mentioned earlier. A vote is really just the number of times an answer has been chosen.
If your problem is that users can choose more than 1 answer, it is likely that you should implement view based validations via javascript, or better yet, just disable their ability to select multiple choices, which if you were using a select tag, is actually the default. You must've added
multiple: true
in the select tag options for the user to select multiple entries.
On the backend, you can do this:
class Choice < ActiveRecord::Base
validates_uniqueness_of :user_id, scope: [:question_id, :answer_id]
end

Rails Eager Load and Limit

I think I need something akin to a rails eager loaded query with a limit on it but I am having trouble finding a solution for that.
For the sake of simplicity, let us say that there will never be more than 30 Persons in the system (so Person.all is a small dataset) but each person will have upwards of 2000 comments (so Person.include(:comments) would be a large data set).
Parent association
class Person < ActiveRecord::Base
has_many :comments
end
Child association
class Comment < ActiveRecord::Base
belongs_to :person
end
I need to query for a list of Persons and include their comments, but I only need 5 of them.
I would like to do something like this:
Limited parent association
class Person < ActiveRecord::Base
has_many :comments
has_many :sample_of_comments, \
:class_name => 'Comment', :limit => 5
end
Controller
class PersonController < ApplicationController
def index
#persons = Person.include(:sample_of_comments)
end
end
Unfortunately, this article states: "If you eager load an association with a specified :limit option, it will be ignored, returning all the associated objects"
Is there any good way around this? Or am I doomed to chose between eager loading 1000s of unneeded ActiveRecord objects and an N+1 query? Also note that this is a simplified example. In the real world, I will have other associations with Person, in the same index action with the same issue as comments. (photos, articles, etc).
Regardless of what "that article" said, the issue is in SQL you can't narrow down the second sql query (of eager loading) the way you want in this scenario, purely by using a standard LIMIT
You can, however, add a new column and perform a WHERE clause instead
Change your second association to Person has_many :sample_of_comments, conditions: { is_sample: true }
Add a is_sample column to comments table
Add a Comment#before_create hook that assigns is_sample = person.sample_of_comments.count < 5

Rails: How to average multiple database entries after create

Rails 3.1
I'll simplify my application to get at my question.
I have two tables: Items and Reviews
Items has a column "Average_Rating" and Reviews has a column "Item_ID" and "Rating"
For each item, I'd like to store the average rating for its corresponding reviews. Although I can't figure it out, I feel like what I want to do is add something to the create and update methods in the Reviews Controller along the lines of:
#review = Review.find(params[:id])
#item = Item.find(#review.Item_ID)
reviews_to_sum = Reviews.find_by_item_id(#item.id)
#item.Average_Rating = reviews_to_sum.Rating.sum/reviews_to_sum.count
I recognize, however, that the above probably isn't close to correct... I'm a beginner and I'm stuck.
And I do want to store the Average_Rating in the database, as opposed to calculating it when I need it, for a variety of reasons.
class Item < ActiveRecord::Base
has_many :reviews
end
class Review < ActiveRecord::Base
belongs_to :item
after_save do
item.update_attributes average_rating: item.reviews.average(:rating)
end
end

Resources