tracks_visits 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Jonas Grimfelt
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.textile ADDED
@@ -0,0 +1,145 @@
1
+ h1. TRACKS_VISITS
2
+
3
+ _Rails: Track unique and total visits/viewings of an ActiveRecord resource based on user/account or IP._
4
+
5
+ h2. Installation
6
+
7
+ <pre>sudo gem install grimen-tracks_visits</pre>
8
+
9
+ h2. Usage
10
+
11
+ 1. Generate migration:
12
+
13
+ <pre>
14
+ $ ./script/generate tracks_visits_migration
15
+ </pre>
16
+
17
+ Generates @db/migrations/{timestamp}_tracks_visits_migration@ with:
18
+
19
+ <pre>
20
+ class TracksVisitsMigration < ActiveRecord::Migration
21
+ def self.up
22
+ create_table :visits do |t|
23
+ t.references :visitable, :polymorphic => true
24
+
25
+ t.references :visitor, :polymorphic => true
26
+ t.string :ip, :limit => 24
27
+
28
+ t.integer :visits, :default => 0
29
+
30
+ # created_at <=> first_visited_at
31
+ # updated_at <=> last_visited_at
32
+ t.timestamps
33
+ end
34
+
35
+ add_index :visits, [:visitor_id, :visitor_type]
36
+ add_index :visits, [:visitable_id, :visitable_type]
37
+ end
38
+
39
+ def self.down
40
+ drop_table :visits
41
+ end
42
+ end
43
+ </pre>
44
+
45
+ 2. Make your model count visits:
46
+
47
+ <pre>
48
+ class Post < ActiveRecord::Base
49
+ tracks_visits
50
+ end
51
+ </pre>
52
+
53
+ or
54
+
55
+ <pre>
56
+ class Post < ActiveRecord::Base
57
+ tracks_visits :by => :user # Setup associations for the visitor class automatically
58
+ end
59
+ </pre>
60
+
61
+ *Note:* @:by@ is optional if you choose any of @User@ or @Account@ as visitor classes.
62
+
63
+ 3. ...and here we go:
64
+
65
+ <pre>
66
+ @post = Post.create
67
+
68
+ @post.visited? # => false
69
+ @post.unique_visits # => 0
70
+ @post.total_visits # => 0
71
+
72
+ @post.visit!(:ip => '128.0.0.0')
73
+ @post.visit!(:visitor => @user) # aliases: :user, :account
74
+
75
+ @post.visited? # => true
76
+ @post.unique_visits # => 2
77
+ @post.total_visits # => 2
78
+
79
+ @post.visit!(:ip => '128.0.0.0')
80
+ @post.visit!(:visitor => @user)
81
+ @post.visit!(:ip => '128.0.0.1')
82
+
83
+ @post.unique_visits # => 3
84
+ @post.total_visits # => 5
85
+
86
+ @post.visted_by?('128.0.0.0') # => true
87
+ @post.visted_by?(@user) # => true
88
+ @post.visted_by?('128.0.0.2') # => false
89
+ @post.visted_by?(@another_user) # => false
90
+
91
+ @post.reset_visits!
92
+ @post.unique_visits # => 0
93
+ @post.total_visits # => 0
94
+
95
+ # Note: See documentation for more info.
96
+
97
+ </pre>
98
+
99
+ h2. Caching
100
+
101
+ If the visitable class table - in the sample above @Post@ - contains a columns @total_visits_count@ and @unique_visits_count@, 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.
102
+
103
+ Additional caching fields:
104
+
105
+ <pre>
106
+ class AddTrackVisitsCachingToPostsMigration < ActiveRecord::Migration
107
+ def self.up
108
+ # Enable tracks_visits-caching.
109
+ add_column :posts, :unique_visits_count, :integer
110
+ add_column :posts, :total_visits_count, :integer
111
+ end
112
+
113
+ def self.down
114
+ remove_column :posts, :unique_visits_count
115
+ remove_column :posts, :total_visits_count
116
+ end
117
+ end
118
+ </pre>
119
+
120
+ h2. Dependencies
121
+
122
+ Basic usage:
123
+
124
+ * "rails":http://github.com/rails/rails (well...)
125
+
126
+ For running tests:
127
+
128
+ * sqlite3-ruby
129
+ * "thoughtbot-shoulda":http://github.com/thoughtbot/shoulda
130
+ * "nakajima-acts_as_fu":http://github.com/nakajima/acts_as_fu
131
+ * "jgre-monkeyspecdoc":http://github.com/jgre/monkeyspecdoc
132
+
133
+ h2. Notes
134
+
135
+ * Tested with Ruby 1.9+. =)
136
+ * Let me know if you find any bugs; not used in production yet so consider this a concept version.
137
+
138
+ h2. TODO
139
+
140
+ * More thorough tests for more complex scenarios.
141
+ * ActiveModel finder helpers.
142
+
143
+ h2. License
144
+
145
+ Copyright (c) Jonas Grimfelt, released under the MIT-license.
data/Rakefile ADDED
@@ -0,0 +1,51 @@
1
+ # coding: utf-8
2
+ require 'rubygems'
3
+ require 'rake'
4
+ require 'rake/testtask'
5
+ require 'rake/rdoctask'
6
+
7
+ NAME = "tracks_visits"
8
+ SUMMARY = %Q{Rails: Track unique and total visits/viewings of an ActiveRecord resource based on user/account or IP.}
9
+ HOMEPAGE = "http://github.com/grimen/#{NAME}/tree/master"
10
+ AUTHOR = "Jonas Grimfelt"
11
+ EMAIL = "grimen@gmail.com"
12
+ SUPPORT_FILES = %w(README.textile)
13
+
14
+ begin
15
+ gem 'technicalpickles-jeweler', '>= 1.2.1'
16
+ require 'jeweler'
17
+ Jeweler::Tasks.new do |gem|
18
+ gem.name = NAME
19
+ gem.summary = SUMMARY
20
+ gem.description = SUMMARY
21
+ gem.homepage = HOMEPAGE
22
+ gem.author = AUTHOR
23
+ gem.email = EMAIL
24
+
25
+ gem.require_paths = %w{lib}
26
+ gem.files = SUPPORT_FILES << %w(MIT-LICENSE Rakefile) << Dir.glob(File.join('{lib,test,rails}', '**', '*'))
27
+ gem.executables = %w()
28
+ gem.extra_rdoc_files = SUPPORT_FILES
29
+ end
30
+ rescue LoadError
31
+ puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
32
+ end
33
+
34
+ desc %Q{Run unit tests for "#{NAME}".}
35
+ task :default => :test
36
+
37
+ desc %Q{Run unit tests for "#{NAME}".}
38
+ Rake::TestTask.new(:test) do |test|
39
+ test.libs << %w(lib test)
40
+ test.pattern = File.join('test', '**', '*_test.rb')
41
+ test.verbose = true
42
+ end
43
+
44
+ desc %Q{Generate documentation for "#{NAME}".}
45
+ Rake::RDocTask.new(:rdoc) do |rdoc|
46
+ rdoc.rdoc_dir = 'rdoc'
47
+ rdoc.title = NAME
48
+ rdoc.options << '--line-numbers' << '--inline-source' << '--charset=UTF-8'
49
+ rdoc.rdoc_files.include(SUPPORT_FILES)
50
+ rdoc.rdoc_files.include(File.join('lib', '**', '*.rb'))
51
+ end
@@ -0,0 +1,16 @@
1
+ # coding: utf-8
2
+ require File.join(File.dirname(__FILE__), 'tracks_visits', 'tracks_visits_error')
3
+ require File.join(File.dirname(__FILE__), 'tracks_visits', 'visit')
4
+ require File.join(File.dirname(__FILE__), 'tracks_visits', 'visitor')
5
+ require File.join(File.dirname(__FILE__), 'tracks_visits', 'visitable')
6
+
7
+ module TracksVisits
8
+
9
+ def log(message, level = :info)
10
+ level = :info if level.blank?
11
+ RAILS_DEFAULT_LOGGER.send level.to_sym, message
12
+ end
13
+
14
+ extend self
15
+
16
+ end
@@ -0,0 +1,11 @@
1
+ # coding: utf-8
2
+
3
+ module TracksVisits #:nodoc:
4
+ module ActiveRecord #:nodoc:
5
+ class TracksVisitsError < RuntimeError
6
+
7
+ # Nothing
8
+
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,8 @@
1
+ # coding: utf-8
2
+
3
+ class Visit < ActiveRecord::Base
4
+
5
+ belongs_to :visitable, :polymorphic => true
6
+ belongs_to :visitor, :polymorphic => true
7
+
8
+ end
@@ -0,0 +1,284 @@
1
+ # coding: utf-8
2
+ require File.join(File.dirname(__FILE__), 'tracks_visits_error')
3
+ require File.join(File.dirname(__FILE__), 'visit')
4
+ require File.join(File.dirname(__FILE__), 'visitor')
5
+
6
+ module TracksVisits #:nodoc:
7
+ module ActiveRecord #:nodoc:
8
+ module Visitable
9
+
10
+ DEFAULT_VISIT_CLASS_NAME = :visit.freeze
11
+
12
+ def self.included(base) #:nodoc:
13
+ base.class_eval do
14
+ extend ClassMethods
15
+ end
16
+ end
17
+
18
+ module ClassMethods
19
+
20
+ # Make the model visitable, i.e. count/track visits by user/account of IP.
21
+ #
22
+ # * Adds a <tt>has_many :visits</tt> association to the model for easy retrieval of the detailed visits.
23
+ # * Adds a <tt>has_many :visitors</tt> association to the object.
24
+ # * Adds a <tt>has_many :visits</tt> associations to the visitor class.
25
+ #
26
+ # === Options
27
+ # * <tt>:options[:visit_class]</tt> - class of the model used for the visits. Defaults to Visit.
28
+ # This class will be dynamically created if not already defined. If the class is predefined,
29
+ # it must have in it the following definitions:
30
+ # <tt>belongs_to :visitable, :polymorphic => true</tt>
31
+ # <tt>belongs_to :visitor, :class_name => 'User', :foreign_key => :visitor_id</tt> replace user with
32
+ # the visitor class if needed.
33
+ # * <tt>:options[:visitor_class]</tt> - class of the model that creates the visit.
34
+ # Defaults to User or Account - auto-detected. This class will NOT be created, so it must be defined in the app.
35
+ # Use the IP address to prevent multiple visits from the same client.
36
+ #
37
+ def tracks_visits(options = {})
38
+ send :include, ::TracksVisits::ActiveRecord::Visitable::InstanceMethods
39
+
40
+ # Set default class names if not given.
41
+ options[:visitor_class_name] ||= options[:from] || Visitor::DEFAULT_CLASS_NAME
42
+ options[:visitor_class_name] = options[:visitor_class_name].to_s.classify
43
+
44
+ options[:visit_class_name] = DEFAULT_VISIT_CLASS_NAME.to_s.classify
45
+
46
+ options[:visitor_class] = options[:visitor_class_name].constantize rescue nil
47
+
48
+ # Assocations: Visit class (e.g. Visit).
49
+ options[:visit_class] = begin
50
+ options[:visit_class_name].constantize
51
+ rescue
52
+ # If note defined...define it!
53
+ Object.const_set(options[:visit_class_name].to_sym, Class.new(::ActiveRecord::Base)).class_eval do
54
+ belongs_to :visitable, :polymorphic => true
55
+ belongs_to :visitor, :polymorphic => true
56
+ end
57
+ options[:visit_class_name].constantize
58
+ end
59
+
60
+ # Save the initialized options for this class.
61
+ write_inheritable_attribute(:tracks_visits_options, options.slice(:visit_class, :visitor_class))
62
+ class_inheritable_reader :tracks_visits_options
63
+
64
+ # Assocations: Visitor class (e.g. User).
65
+ if Object.const_defined?(options[:visitor_class].name.to_sym)
66
+ options[:visitor_class].class_eval do
67
+ has_many :visits,
68
+ :foreign_key => :visitor_id,
69
+ :class_name => options[:visit_class].name
70
+ end
71
+ end
72
+
73
+ # Assocations: Visitable class (e.g. Page).
74
+ self.class_eval do
75
+ has_many options[:visit_class].name.tableize.to_sym,
76
+ :as => :visitable,
77
+ :dependent => :delete_all,
78
+ :class_name => options[:visit_class].name
79
+
80
+ has_many options[:visitor_class].name.tableize.to_sym,
81
+ :through => options[:visit_class].name.tableize,
82
+ :class_name => options[:visitor_class].name
83
+
84
+ # Hooks.
85
+ before_create :init_has_visits_fields
86
+ end
87
+
88
+ end
89
+
90
+ # Does this class count/track visits?
91
+ #
92
+ def visitable?
93
+ self.respond_do?(:tracks_visits_options)
94
+ end
95
+ alias :is_visitable? :visitable?
96
+
97
+ protected
98
+
99
+ def validate_visitor(identifiers)
100
+ raise TracksVisitsError, "Not initilized correctly" unless defined?(:tracks_visits_options)
101
+ raise TracksVisitsError, "Argument can't be nil: no IP and/or user provided" if identifiers.blank?
102
+
103
+ visitor = identifiers[:visitor] || identifiers[:user] || identifiers[:account]
104
+ ip = identifiers[:ip]
105
+
106
+ #tracks_visits_options[:visitor_class].present?
107
+ # raise TracksVisitsError, "Visitor is of wrong type: #{visitor.class}" unless (visitor.nil? || visitor.is_a?(tracks_visits_options[:visitor_class]))
108
+ #end
109
+ raise TracksVisitsError, "IP is of wrong type: #{ip.class}" unless (ip.nil? || ip.is_a?(String))
110
+ raise TracksVisitsError, "Arguments not supported: no ip and/or user provided" unless ((visitor && visitor.id) || ip)
111
+
112
+ [visitor, ip]
113
+ end
114
+
115
+ end
116
+
117
+ module InstanceMethods
118
+
119
+ # Does this object count/track visits?
120
+ #
121
+ def visitable?
122
+ self.class.visitable?
123
+ end
124
+ alias :is_visitable? :visitable?
125
+
126
+ # first_visit = created_at.
127
+ #
128
+ def first_visited_at
129
+ self.created_at if self.respond_to?(:created_at)
130
+ end
131
+
132
+ # last_visit = updated_at.
133
+ #
134
+ def last_visited_at
135
+ self.updated_at if self.respond_to?(:updated_at)
136
+ end
137
+
138
+ # Get the unique number of visits for this object based on the visits field,
139
+ # or with a SQL query if the visited objects doesn't have the visits field
140
+ #
141
+ def unique_visits
142
+ if self.has_cached_fields?
143
+ self.unique_visits || 0
144
+ else
145
+ self.visits.size
146
+ end
147
+ end
148
+ alias :number_of_visitors :unique_visits
149
+
150
+ # Get the total number of visits for this object.
151
+ #
152
+ def total_visits
153
+ if self.has_cached_fields?
154
+ self.total_visits || 0
155
+ else
156
+ tracks_visits_options[:visit_class].sum(:visits, :conditions => {:visitable_id => self.id})
157
+ end
158
+ end
159
+ alias :number_of_visits :total_visits
160
+
161
+ # Is this object visited by anyone?
162
+ #
163
+ def visited?
164
+ self.unique_visits > 0
165
+ end
166
+ alias :is_visited? :visited?
167
+
168
+ # Check if an item was already visited by the given visitor or ip.
169
+ #
170
+ # === Identifiers hash:
171
+ # * <tt>:ip</tt> - identify with IP
172
+ # * <tt>:visitor</tt> - identify with a visitor-model (e.g. User, ...)
173
+ # * <tt>:user</tt> - (same as above)
174
+ # * <tt>:account</tt> - (same as above)
175
+ #
176
+ def visited_by?(identifiers)
177
+ visitor, ip = self.validate_visitor(identifiers)
178
+
179
+ conditions = if visitor.present?
180
+ {:visitor => visitor}
181
+ else # ip
182
+ {:ip => (ip ? ip.to_s.strip : nil)}
183
+ end
184
+ self.visits.count(:conditions => conditions) > 0
185
+ end
186
+ alias :is_visited_by? :visited_by?
187
+
188
+ # Delete all tracked visits for this visitable object.
189
+ #
190
+ def reset_visits!
191
+ self.visits.delete_all
192
+ self.total_visits_count = self.unique_visits_count = 0 if self.has_cached_fields?
193
+ end
194
+
195
+ # View the object with and identifier (user or ip) - create new if new visitor.
196
+ #
197
+ # === Identifiers hash:
198
+ # * <tt>:ip</tt> - identify with IP
199
+ # * <tt>:visitor</tt> - identify with a visitor-model (e.g. User, ...)
200
+ # * <tt>:user</tt> - (same as above)
201
+ # * <tt>:account</tt> - (same as above)
202
+ #
203
+ def visit!(identifiers)
204
+ visitor, ip = self.validate_visitor(identifiers)
205
+
206
+ begin
207
+ # Count unique visits only, based on account or IP.
208
+ visit = self.visits.find_by_visitor_id(visitor.id) if visitor.present?
209
+ visit ||= self.visits.find_by_ip(ip) if ip.present?
210
+
211
+ # Try to get existing visit for the current visitor,
212
+ # or create a new otherwise and set new attributes.
213
+ if visit.present?
214
+ visit.visits += 1
215
+
216
+ unique_visit = false
217
+ else
218
+ visit = tracks_visits_options[:visit_class].new do |v|
219
+ #v.visitor = visitor
220
+ #v.visitable = self
221
+ v.visitable_id = self.id
222
+ v.visitable_type = self.class.name
223
+ v.visitor_id = visitor.id if visitor
224
+ v.visitor_type = visitor.class.name if visitor
225
+ v.ip = ip if ip.present?
226
+ v.visits = 1
227
+ end
228
+ self.visits << visit
229
+
230
+ unique_visit = true
231
+ end
232
+
233
+ visit.save
234
+
235
+ # Maintain cached value if cached field is available.
236
+ #
237
+ if self.has_cached_fields?
238
+ self.unique_visits += 1 if unique_visit
239
+ self.total_visits += 1
240
+ self.save_without_validation
241
+ end
242
+
243
+ true
244
+ rescue Exception => e
245
+ raise TracksVisitsError, "Database transaction failed: #{e}"
246
+ false
247
+ end
248
+ end
249
+
250
+ protected
251
+
252
+ # Is there a cached fields for this visitable class?
253
+ #
254
+ def cached_fields?
255
+ self.attributes.has_key?(:total_visits_count) && self.attributes.has_key?(:unique_visits_count)
256
+ end
257
+ alias :has_cached_fields? :cached_fields?
258
+
259
+ # Initialize cached fields - if any.
260
+ #
261
+ def init_has_visits_fields
262
+ self.total_visits = self.unique_visits = 0 if self.has_cached_fields?
263
+ end
264
+
265
+ def validate_visitor(identifiers)
266
+ self.class.send :validate_visitor, identifiers
267
+ end
268
+
269
+ end
270
+
271
+ module SingletonMethods
272
+
273
+ # TODO: Finders
274
+
275
+ end
276
+
277
+ end
278
+ end
279
+ end
280
+
281
+ # Extend ActiveRecord.
282
+ ::ActiveRecord::Base.class_eval do
283
+ include ::TracksVisits::ActiveRecord::Visitable
284
+ end
@@ -0,0 +1,40 @@
1
+ # coding: utf-8
2
+ require File.join(File.dirname(__FILE__), 'tracks_visits_error')
3
+
4
+ module TracksVisits #:nodoc:
5
+ module ActiveRecord #:nodoc:
6
+ module Visitor
7
+
8
+ DEFAULT_CLASS_NAME = begin
9
+ if defined?(Account)
10
+ :account
11
+ else
12
+ :user
13
+ end
14
+ rescue
15
+ :user
16
+ end
17
+ DEFAULT_CLASS_NAME.freeze
18
+
19
+ def self.included(base) #:nodoc:
20
+ base.class_eval do
21
+ include InstanceMethods
22
+ extend ClassMethods
23
+ end
24
+ end
25
+
26
+ module ClassMethods
27
+
28
+ # Nothing
29
+
30
+ end
31
+
32
+ module InstanceMethods
33
+
34
+ # Nothing
35
+
36
+ end
37
+
38
+ end
39
+ end
40
+ end
data/rails/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'tracks_visits'))
@@ -0,0 +1,52 @@
1
+ # coding: utf-8
2
+ require 'rubygems'
3
+
4
+ gem 'test-unit', '>= 2.0.0'
5
+ gem 'thoughtbot-shoulda', '>= 2.0.0'
6
+ gem 'sqlite3-ruby', '>= 1.2.0'
7
+ gem 'nakajima-acts_as_fu', '>= 0.0.5'
8
+ gem 'jgre-monkeyspecdoc', '>= 0.9.5'
9
+
10
+ require 'test/unit'
11
+ require 'shoulda'
12
+ require 'acts_as_fu'
13
+ require 'monkeyspecdoc'
14
+
15
+ require 'test_helper'
16
+
17
+ require 'tracks_visits'
18
+
19
+ # To get ZenTest to get it.
20
+ # require File.expand_path(File.join(File.dirname(__FILE__), 'tracks_visits_test.rb'))
21
+
22
+ build_model :visits do
23
+ references :visitable, :polymorphic => true
24
+
25
+ references :visitor, :polymorphic => true
26
+ string :ip, :limit => 24
27
+
28
+ integer :visits, :default => 0
29
+
30
+ timestamps
31
+ end
32
+
33
+ build_model :guests do
34
+ end
35
+
36
+ build_model :users do
37
+ string :username
38
+ end
39
+
40
+ build_model :untracked_posts do
41
+ end
42
+
43
+ build_model :tracked_posts do
44
+ tracks_visits :from => :users
45
+ end
46
+
47
+ build_model :cached_tracked_posts do
48
+ integer :unique_visits_count
49
+ integer :total_visits_count
50
+
51
+ tracks_visits :from => :users
52
+ end
@@ -0,0 +1,108 @@
1
+ # coding: utf-8
2
+ require 'test_helper'
3
+
4
+ class TracksVisitsTest < Test::Unit::TestCase
5
+
6
+ def setup
7
+ @user_1 = ::User.create
8
+ @user_2 = ::User.create
9
+ @tracked_post = ::TrackedPost.create
10
+ @regular_post = ::TrackedPost.create
11
+ end
12
+
13
+ context "initialization" do
14
+
15
+ should "extend ActiveRecord::Base" do
16
+ assert_respond_to ::ActiveRecord::Base, :tracks_visits
17
+ end
18
+
19
+ should "declare tracks_visits instance methods for visitable objects" do
20
+ methods = [
21
+ :first_visited_at,
22
+ :first_visited_at,
23
+ :last_visited_at,
24
+ :unique_visits,
25
+ :total_visits,
26
+ :visited_by?,
27
+ :reset_visits!,
28
+ :visitable?,
29
+ :visit!
30
+ ]
31
+
32
+ assert methods.all? { |m| @tracked_post.respond_to?(m) }
33
+ # assert !methods.any? { |m| @tracked_post.respond_to?(m) }
34
+ end
35
+
36
+ # Don't work for some reason... =S
37
+ # should "be enabled only for specified models" do
38
+ # assert @tracked_post.visitable?
39
+ # assert_not @untracked_post.visitable?
40
+ # end
41
+
42
+ end
43
+
44
+ context "visitable" do
45
+ should "have zero visits from the beginning" do
46
+ assert_equal(@tracked_post.visits.size, 0)
47
+ end
48
+
49
+ should "count visits based on IP correctly" do
50
+ number_of_unique_visits = @tracked_post.unique_visits
51
+ number_of_total_visits = @tracked_post.total_visits
52
+
53
+ @tracked_post.visit!(:ip => '128.0.0.0')
54
+ @tracked_post.visit!(:ip => '128.0.0.1')
55
+ @tracked_post.visit!(:ip => '128.0.0.1')
56
+
57
+ assert_equal @tracked_post.unique_visits, number_of_unique_visits + 2
58
+ assert_equal @tracked_post.total_visits, number_of_total_visits + 3
59
+ end
60
+
61
+ should "count visits based on visitor object (user/account) correctly" do
62
+ number_of_unique_visits = @tracked_post.unique_visits
63
+ number_of_total_visits = @tracked_post.total_visits
64
+
65
+ @tracked_post.visit!(:user => @user_1)
66
+ @tracked_post.visit!(:user => @user_2)
67
+ @tracked_post.visit!(:user => @user_2)
68
+
69
+ assert_equal @tracked_post.unique_visits, number_of_unique_visits + 2
70
+ assert_equal @tracked_post.total_visits, number_of_total_visits + 3
71
+ end
72
+
73
+ should "count visits based on both IP and visitor object (user/account) correctly" do
74
+ number_of_unique_visits = @tracked_post.unique_visits
75
+ number_of_total_visits = @tracked_post.total_visits
76
+
77
+ @tracked_post.visit!(:ip => '128.0.0.0')
78
+ @tracked_post.visit!(:ip => '128.0.0.0')
79
+ @tracked_post.visit!(:user => @user_1)
80
+ @tracked_post.visit!(:user => @user_2)
81
+ @tracked_post.visit!(:user => @user_2)
82
+
83
+ assert_equal @tracked_post.unique_visits, number_of_unique_visits + 3
84
+ assert_equal @tracked_post.total_visits, number_of_total_visits + 5
85
+ end
86
+
87
+ should "delete all visits upon reset" do
88
+ @tracked_post.visit!(:ip => '128.0.0.0')
89
+ @tracked_post.reset_visits!
90
+
91
+ assert_equal @tracked_post.unique_visits, 0
92
+ assert_equal @tracked_post.total_visits, 0
93
+ end
94
+ end
95
+
96
+ context "visitor" do
97
+
98
+ # Nothing
99
+
100
+ end
101
+
102
+ context "visit" do
103
+
104
+ # Nothing
105
+
106
+ end
107
+
108
+ end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tracks_visits
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - grimen@gmail.com
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-08-31 00:00:00 +02:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: "Rails: Track unique and total visits/viewings of an ActiveRecord resource based on user/account or IP."
17
+ email: Jonas Grimfelt
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - MIT-LICENSE
24
+ - README.textile
25
+ - Rakefile
26
+ - lib/tracks_visits.rb
27
+ - lib/tracks_visits/tracks_visits_error.rb
28
+ - lib/tracks_visits/visit.rb
29
+ - lib/tracks_visits/visitable.rb
30
+ - lib/tracks_visits/visitor.rb
31
+ - rails/init.rb
32
+ - test/test_helper.rb
33
+ - test/tracks_visits_test.rb
34
+ files:
35
+ - MIT-LICENSE
36
+ - README.textile
37
+ - Rakefile
38
+ - lib/tracks_visits.rb
39
+ - lib/tracks_visits/tracks_visits_error.rb
40
+ - lib/tracks_visits/visit.rb
41
+ - lib/tracks_visits/visitable.rb
42
+ - lib/tracks_visits/visitor.rb
43
+ - rails/init.rb
44
+ - test/test_helper.rb
45
+ - test/tracks_visits_test.rb
46
+ has_rdoc: true
47
+ homepage: http://github.com/grimen/tracks_visits/tree/master
48
+ licenses: []
49
+
50
+ post_install_message:
51
+ rdoc_options:
52
+ - --charset=UTF-8
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: "0"
60
+ version:
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: "0"
66
+ version:
67
+ requirements: []
68
+
69
+ rubyforge_project:
70
+ rubygems_version: 1.3.5
71
+ signing_key:
72
+ specification_version: 3
73
+ summary: "Rails: Track unique and total visits/viewings of an ActiveRecord resource based on user/account or IP."
74
+ test_files:
75
+ - test/test_helper.rb
76
+ - test/tracks_visits_test.rb