How to implement a viewing system in Rails

Hello everyone. Today I will show how to implement a viewing system for any model of your Rails application. Suppose we have a model News and we would like to keep track of how many times a single news has been displayed, in order to implement box like "the most 'seen'" etc.. Suppose we have a model News created in this way: myapp/db/migrate/001_create_news.rb:

class CreateNews < ActiveRecord::Migration
  def self.up
    create_table :news do |t|
      t.string  :title
      t.text    :content
      t.date    :online_date_start, :null => true
      t.date    :online_date_end,   :null => true
      t.boolean :online,            :null => false, :default => true
      t.timestamps
    end
  end

  def self.down
    drop_table :news
  end
end
myapp/app/models/news.rb:

class News < ActiveRecord::Base
  validates_length_of :title, :within => 2..255
  validates_presence_of :title, :content, :online_date_start
end
The basic idea is to increment a counter displayed every time a user views the news. Some considerations: an implementation of this type would allow a user to press the refresh button and constantly increase the counter. So the goal is to have a clever mechanism that increases the counter:
  • 1 once for each registered user
  • 1 only once per IP in the case of guest
We would also like that this mechanism could be applied not only to model News, but to any model of our application. Then we create the migration of our model Viewing: myapp/db/migrate/002_create_viewings.rb:

class CreateViewings < ActiveRecord::Migration
  def self.up
    create_table   :viewings do |t|
      t.string     :ip
      t.string     :viewable_type
      t.integer    :viewable_id
      t.references :person
      t.datetime   :created_at
    end
  end

  def self.down
    drop_table :viewings
  end
end
and the corresponding model: myapp/app/models/viewing.rb:

class Viewing < ActiveRecord::Base
  # RELATIONSHIPS
  belongs_to :viewable, :polymorphic => true, :counter_cache => :popularity
  belongs_to :viewer, :class_name => "Person", :foreign_key => "person_id"

  # VALIDATIONS
  validates_uniqueness_of :ip, :scope => [:viewable_id, :viewable_type, :person_id]
  validates_uniqueness_of :person_id, :allow_nil => true

  # OTHER
  def viewable_type=(sType)
    super(sType.to_s.classify.constantize.base_class.to_s)
  end
end
As you can see, the model Viewing is not closely tied to model News, but given its polimorphic nature, it may be associated with any model. At this point, we can associate the model Viewing to the News : myapp/app/models/news.rb:


class News < ActiveRecord::Base
  validates_length_of :title, :within => 2..255
  validates_presence_of :title, :content, :online_date_start
  has_many :viewings, :as => :viewable
end
and add the column "counter cache to the table News: myapp/db/migrate/003_add_popularity_to_news.rb:

class AddPopularityToNews < ActiveRecord::Migration
  def self.up
    add_column :news, :popularity, :integer, :default => 0
  end

  def self.down
    remove_column :news, :popularity
  end
end
Now we have everything necessary to implement the mechanism for viewing in our model News. Let's show how to increment the counter to "show" of the news. myapp/app/controllers/news_controller.rb:

class NewsController < ApplicationController
  after_filter  :record_view, :only => :show

  def show
    @news = News.find(params[ :id ])
  end

  private

  def record_view
    @news.viewings.create(:ip => request.remote_ip, :viewer => current_user) unless @news.nil?
  end
end