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 ofin_orderabove). -
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