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_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