tracks_visits 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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