A join table for users and article scoring

So –

  • A user can score many articles
  • An article can be scored by many users, but only once by each user

So, it’s a habtm relationship, but not between users and articles, since user already “has_many articles” and articles already “belong_to user”. I’d better rename both – ‘articles’ will be ‘scored_articles’ and ‘users’ will be ‘scorers’. So the table will be called ‘scored_articles_scorers’ What we want is a table that has scored_article_id (which uses has article_ids), scorer_id (which is users) and score (-1 or +1).

Here’s the migration:

class CreateScoredArticlesScorers < ActiveRecord::Migration
def self.up
create_table :scored_articles_scorers do |t|
t.column :scored_article_id, :integer t.column :scorer_id, :integer
t.column :score, :integer

def self.down
drop_table :scored_articles_scorers

Something odd – this is the same (with different names) as my articles_links migration, but the articles_scorers table has a unique id field as well, while articles_links doesn’t. Weird. Will need to keep an eye out for any problems associated with that.

Now, the tricky part (for me) – setting up the models. Should be similar to what we did for articles_links. That had this, in Article:

has_and_belongs_to_many :links,
:class_name => “Article”,
:foreign_key => “article_id”,
:association_foreign_key => “link_id”,
:join_table => “articles_links”

So, now, in Article we have

has_and_belongs_to_many :scorers,
:class_name => “User”,
:foreign_key => “scored_article_id”,
:association_foreign_key => “scorer_id”,
:join_table => “scored_articles_scorers”

And in User we have

has_and_belongs_to_many :scored_articles,
:class_name => “Article”,
:foreign_key => “scorer_id”,
:association_foreign_key => “scored_article_id”,
:join_table => “scored_articles_scorers”

So, let’s test it out! Restarted the web server. First lets do a ‘score up’ link in the view, next to articles in the article list: “article”, :action => “score_up”, :article_id => article, :user_id => session[:user] %>

Next of course we need to do the controller method. Here’s my first stab – it’s incomplete for now – it doesn’t try to write the score into the scored_articles_scorers table, just to create a record and change the score:

def score_up
@article = Article.find(params[:article_id])
@scorer = User.find(params[:user_id])
@article.scorers < ‘list’

You’ll notice that the instance method being called “increase_score” is named differently to this controller method, “score up”. That’s because “increase_points” is a general kind of event that could happen for a variety of reasons, not just someone hitting the ‘score_up’ button. However, ‘score_up’ is a particular event where a particular user is involved. So, “points” is a dumb number, whereas a ‘score’ is a more meaningful event with more information attached to it: who scored it for example.

OK – this works, it increases the article’s points and it makes a new record in the scored_articles_scorers with the proper article and user ids. What i need to do next is make it a) Check that the user hasn’t already marked it up b) Put the score ( 1 or -1) in the record.

I’ll do b first: this looks complicated. Reading up on it, it used to be simple: we’d use the method push_with_attributes (“push” is an alias for “<<“) and send the value along. push_with_attributes has been deprecated however – it looks like the proper way is to make an actual class that is instanced as a join table row. It says “we’ll discuss that in the next section- let’s have a look. ahhhh – these are the through tables that i keep seeing references to in the forums!

So, i should make a new class called Score, and rename scored_articles_scorers as ‘scores’ (it’s a nicer name anyway). Then i say, in Article

has_many :scores
has_many :scorers, :through => :scores

and in User

has_many :scores
has_many :scored, :through => :scores

I still need to tell it somehow that ‘scorers’ means “users” for example. Wait, we can do that with :source. So –

has_many :scores
has_many :scorers, :through => :scores, :source => :user

has_many :scores
has_many :scored, :through => :scores, :source => :article

So, i need to set up the new model, change the database again and then change the above models.

OK, done that, and it’s working. Excellent. However i still need to add the score (1) to the row in scores.

(skipped a bit of reworking and re-re-working here) Here’s the final code:

In the view, i just call the change_score controller method with either 1 or -1:
<%= link_to “score up”, :controller=>”article”, :action => “change_score”, :article_id => article, :user_id => session[:user], :points => 1 %>

In the controller, i pass these params through to the instance method of the same name (most of the below is messages): def change_score
@article = Article.find(params[:article_id])
rescue ActiveRecord::RecordNotFound
flash[:message] = “Couldn’t find article##{params[:article_id]}, redirecting to article list.”
redirect_to :controller => “article”, :action => “list”
flash[:message] = @article.change_score(params)
redirect_to :controller => ‘article’, :action => ‘list’

Then, in the model i do the logic: 

  def change_score(params = {})
new_points = params[:points].to_i
@score = Score.find(:first, :conditions => [“article_id = ? and user_id = ? “, params[:article_id], params[:user_id]])
if @score == nil
#@score not found
#no score exists from this user so make a new one then add the points
return_string = “Didn’t find score, making a new one”
@score = Score.new(:article_id => params[:article_id], :user_id => params[:user_id], :points => new_points)
self.points += new_points
else #found something for @score
if @score != nil #@score was already there
#look for a nil and turn it into a 0 if present
@score.points = 0 if @score.points == nil
if @score.points != new_points
#score already there but points are different
#subtract old points then add new points
self.points -= @score.points
self.points += new_points
return_string = “changed your score for this article from #{@score.points} to #{new_points}”
#then change @score to have new points
@score.points = new_points
return_string = “you already gave this article #{@score.points} point”
end #if
end #if
end #if
return return_string
end #method

Cool.  That’s enough for today.


One response to “A join table for users and article scoring

  1. Pingback: Back from holiday « nuby on rails

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s