Skip to content
Rails: Track unique and total visits/viewings of an ActiveRecord resource based on user/account or IP.
Ruby
Find file
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.
generators/is_visitable_migration
lib
rails
test
.gitignore
MIT-LICENSE
README.textile
Rakefile
VERSION
is_visitable.gemspec

README.textile

IS_VISITABLE ~ concept version ~

Rails: Track unique and total visits/viewings of an ActiveRecord resource based on user/account or IP.

Installation

Gem:

sudo gem install is_visitable

and in config/environment.rb:

config.gem 'is_visitable'

Plugin:

./script/plugin install git://github.com/grimen/is_visitable.git

Usage

1. Generate migration:

$ ./script/generate is_visitable_migration

Generates db/migrations/{timestamp}_is_visitable_migration with:

class IsVisitableMigration < ActiveRecord::Migration
  def self.up
    create_table :visits do |t|
      t.references  :visitable,     :polymorphic => true
      
      t.references  :visitor,       :polymorphic => true
      t.string      :ip,            :limit => 24
      
      t.integer     :visits,        :default => 1
      
      # created_at <=> first_visited_at
      # updated_at <=> last_visited_at
      t.timestamps
    end
    
    add_index :visits, [:visitor_id, :visitor_type]
    add_index :visits, [:visitable_id, :visitable_type]
  end
  
  def self.down
    drop_table :visits
  end
end

2. Make your model count visits:

class Post < ActiveRecord::Base
  is_visitable
end

or, with explicit visitor (or visitors):

class Post < ActiveRecord::Base
  # Setup associations for the visitor class(es) automatically.
  is_visitable :by => [:users, :ducks]
end

3. …and here we go:

Examples:

@post = Post.create

@post.visited?          # => false
@post.unique_visits     # => 0
@post.total_visits      # => 0

@post.visit!(:by => '128.0.0.0')
@post.visit!(:by => @user)      # aliases: :user, :account

@post.visited?          # => true
@post.unique_visits     # => 2
@post.total_visits      # => 2

@post.visit!(:by => '128.0.0.0')
@post.visit!(:by => @user)
@post.visit!(:by => '128.0.0.1')

@post.unique_visits     # => 3
@post.total_visits      # => 5

@post.visited_by?('128.0.0.0')     # => true
@post.visited_by?(@user)           # => true
@post.visited_by?('128.0.0.2')     # => false
@post.visited_by?(@another_user)   # => false

@post.reset_visits!
@post.unique_visits     # => 0
@post.total_visits      # => 0

# Note: See documentation for more info.

Mixin Arguments

The is_visitable mixin takes some hash arguments for customization:

  • :by – the visitor model(s), e.g. User, Account, etc. (accepts either symbol or class, i.e. User <=> :user <=> :users, or an array of suchif there are more than one visitor model). The visitor model will be setup for you. Note: Polymorhic, so it accepts any model. Default: nil.
  • :accept_ip – accept anonymous users uniquely identified by IP (well…you handle the bots =D). See examples below how to use this as your visitor object. Default: false.

Aliases

To make the usage of IsVistable a bit more generic (similar to other plugins you may use), there are two useful aliases for this purpose:

  • Visit#owner <=> Visit#visitor
  • Visit#object <=> Visit#visitable

Example:

@post.visits.first.owner == post.visits.first.visitor      # => true
@post.visits.first.object == post.visits.first.visitable   # => true

Finders (Named Scopes)

IsVisitable has plenty of useful finders implemented using named scopes. Here they are:

Visit

Order:

  • in_order – most recent visits last (order by creation date).
  • most_recent – most recent visits first (opposite of in_order above).
  • least_visits – visits with least total visits first.
  • most_rating – visits with most total visits first.

Filter:

  • limit(<number_of_items>) – maximum <number_of_items> visits.
  • since(<created_at_datetime>) – visits since <created_at_datetime>.
  • recent(<datetime_or_size>) – if DateTime: visits since <datetime_or_size>, else if Fixnum: pick last <datetime_or_size> number of visits.
  • between_dates(<from_date>, to_date) – visits between two datetimes.
  • with_visits(<visits_value_or_range>) – visits with(in) visits value (or range) <visits_value_or_range>.
  • of_visitable_type(<visitable_type>) – visits of <visitable_type> type of visitable models.
  • by_visitor_type(<visitor_type>) – visits of <visitor_type> type of visitor models.
  • on(<visitable_object>) – visits on the visitable object <visitable_object> .
  • by(<visitor_object>) – visits by the <visitor_object> type of visitor models.

Visitable

TODO: Documentation on named scopes for Visitable.

Visitor

TODO: Documentation on named scopes for Visitor.

Examples using finders:

@user = User.first
@post = Post.first

@post.visits.recent(10)           # => [10 most recent visits]
@post.visits.recent(1.week.ago)   # => [visits since 1 week ago]

@post.visits.with_visits(100..500)    # => [all visits on @post with total visits between 100 and 500]

@post.visits.by_visitor_type(:user)   # => [all visits on @post by User-objects]
# ...or:
@post.visits.by_visitor_type(:users)  # => [all visits on @post by User-objects]
# ...or:
@post.visits.by_visitor_type(User)    # => [all visits on @post by User-objects]

@user.visits.on(@post)  # => [all visits by @user on @post]
@post.visits.by(@user)  # => [all visits by @user on @post] (equivalent with above)

Visit.on(@post)  # => [all visits on @user] <=> @post.visits
Visit.by(@user)  # => [all visits by @user] <=> @user.visits

# etc, etc. It's all named scopes, so it's really no new hokus-pokus you have to learn.

Additional Methods

Note: See documentation (RDoc).

Extend the Visit model

This is optional, but if you wanna be in control of your models (in this case Visit) you can take control like this:

class Visit < IsVisitable::Visit
  
  # Do what you do best here... (stating the obvious: core IsVisitable associations, named scopes, etc. will be inherited)
  
end

Caching

If the visitable class table – in the sample above Post – contains a columns cached_total_visits_count and cached_unique_visits, then a cached value will be maintained within it for the number of unique and total visits the object have got. This will save a database query for counting the number of visits, which is a common task.

Additional caching fields:

class AddTrackVisitsCachingToPostsMigration < ActiveRecord::Migration
  def self.up
    # Enable is_visitable-caching.
    add_column :posts, :cached_unique_visits, :integer
    add_column :posts, :cached_total_visits, :integer
  end
  
  def self.down
    remove_column :posts, :cached_unique_visits
    remove_column :posts, :cached_total_visits
  end
end

Example

In your “visitable resource” controller:

Example: app/controllers/posts_controller.rb:

class PostsController < ApplicationController
  
  def show
    ...
    @post.visit!(:by => (current_user.present? ? current_user : request.try(:remote_ip)))
    ...
  end
  
end

Dependencies

For testing: shoulda, redgreen, acts_as_fu, and sqlite3-ruby.

Notes

  • Tested with Ruby 1.8.6 – 1.9.1 and Rails 2.3.2 – 2.3.4.
  • Let me know if you find any bugs; not used in production yet so consider this a concept version.

TODO

  • documentation: A few more README-examples.
  • helper: Controller helper taking arguments for DRYer controller code. Example (in controller): handle_visits :by => current_user
  • feature: Useful finders for Visitable.
  • feature: Useful finders for Visitor.
  • testing: More thorough tests.
  • refactor: Refactor generic stuff to new gem, is_base, and add as gem dependency. Reason: Share the same patterns for my very similar ActiveRecord plugins: is_reviewable, is_visitable, is_commentable, and future additions.

License

Released under the MIT license.
Copyright © Jonas Grimfelt

Something went wrong with that request. Please try again.