Implementare un contatore di visite in Rails

Ciao a tutti. Oggi vorrei mostrare come implementare un contatore di visualizzazioni generico per un qualsiasi modello della vostra applicazione Rails. Supponiamo di avere un model News e di volere tenere traccia di quante volte la singola news è stata visualizzata, in modo da implementare box del tipo "le piu' viste" ecc. Supponiamo di avere un model News creato in questo modo: 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
L'idea di base è quella di incrementare un contatore di visualizzazioni ogni volta che un utente visualizza la news. Alcune considerazioni: un'implementazione di questo tipo permetterebbe ad un utente di premere il tasto refresh ed incrementare continuamente il contatore. Quindi l'obiettivo è quello di avere un meccanismo intelligente che incrementi il contatore:
  • 1 sola volta per ogni singolo utente registrato
  • 1 sola volta per IP nel caso di guest
Si vorrebbe inoltre che questo meccanismo potesse essere applicato non solo al model News, ma a qualsiasi model della nostra applicazione. Creiamo allora la migrazione del nostro modello 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
e il modello corrispondente: 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
Come si può notare, il model Viewing non è strettamente legato al model News, ma, data la sua natura polimorphic, può essere associato ad un qualsiasi model. A questo punto, possiamo associare il model News a Viewing: 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
e aggiungiamo anche la colonna "counter cache" alla tabella 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
A questo punto, abbiamo tutto il necessario per applicare il meccanismo di viewing al nostro model News. Mostriamo come incrementare il contatore allo "show" della 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