sir_tracks_alot 0.2.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +34 -0
- data/VERSION +1 -1
- data/benchmarks/activity_benchmark.rb +1 -1
- data/lib/sir_tracks_alot/activity.rb +38 -107
- data/lib/sir_tracks_alot/count.rb +80 -0
- data/lib/sir_tracks_alot/event_helper.rb +49 -0
- data/lib/sir_tracks_alot/filter_helper.rb +54 -0
- data/lib/sir_tracks_alot/persistable.rb +23 -4
- data/lib/sir_tracks_alot/queue/report_queue.rb +7 -8
- data/lib/sir_tracks_alot/reports/activity_report.rb +2 -2
- data/lib/sir_tracks_alot/reports/actor_report.rb +6 -23
- data/lib/sir_tracks_alot/reports/filter_report.rb +15 -8
- data/lib/sir_tracks_alot/reports/target_report.rb +5 -22
- data/lib/sir_tracks_alot/reports/trackable_report.rb +1 -3
- data/lib/sir_tracks_alot.rb +12 -2
- data/spec/activity_spec.rb +111 -56
- data/spec/count_spec.rb +88 -0
- data/spec/queue/report_queue_spec.rb +6 -6
- data/spec/reports/actor_report_spec.rb +6 -12
- data/spec/reports/filter_report_spec.rb +3 -5
- data/spec/reports/root_stem_report_spec.rb +1 -4
- data/spec/reports/target_report_spec.rb +6 -37
- data/spec/sir_tracks_alot_spec.rb +2 -1
- data/spec/spec_helper.rb +36 -2
- metadata +9 -4
data/README.rdoc
CHANGED
@@ -15,3 +15,37 @@ Description goes here.
|
|
15
15
|
== Copyright
|
16
16
|
|
17
17
|
Copyright (c) 2010 Peter T. Brown. See LICENSE for details.
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
# resolution can be: hourly = %Y/%m/%d %H, daily = %Y/%m/%d or any valid strftime string
|
22
|
+
# def self.count_by(resolution, options_for_find = {})
|
23
|
+
# groups = {}
|
24
|
+
#
|
25
|
+
# filter(options_for_find) do |activity|
|
26
|
+
# activity.views(resolution).each do |time, count|
|
27
|
+
# groups[time] ||= 0
|
28
|
+
# groups[time] += count
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# groups
|
33
|
+
# end
|
34
|
+
|
35
|
+
# def self.count(what, options_for_find)
|
36
|
+
# what = [what] unless what.kind_of?(Array)
|
37
|
+
# views, visits = 0, 0
|
38
|
+
# session_duration = options_for_find.delete(:session_duration)
|
39
|
+
# resolution = options_for_find.delete(:resolution)
|
40
|
+
#
|
41
|
+
# filter(options_for_find) do |activity|
|
42
|
+
# views += activity.views(resolution) if what.include?(:views)
|
43
|
+
# visits += activity.visits(session_duration) if what.include?(:visits)
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# return [views, visits] if what == [:views, :visits]
|
47
|
+
# return [visits, views] if what == [:visits, :views]
|
48
|
+
# return views if what == [:views]
|
49
|
+
# return visits if what == [:visits]
|
50
|
+
# raise ArgumentError("what must be one or more of :views, :visits")
|
51
|
+
# end
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.4.0
|
@@ -1,9 +1,11 @@
|
|
1
1
|
module SirTracksAlot
|
2
2
|
class Activity < Persistable
|
3
|
-
|
4
|
-
|
5
|
-
LIMIT = 1000
|
3
|
+
include EventHelper
|
4
|
+
extend FilterHelper
|
6
5
|
|
6
|
+
ACTIONS = [:create, :view, :login, :search, :update, :destroy]
|
7
|
+
LIMIT = 500
|
8
|
+
|
7
9
|
attribute :created_at
|
8
10
|
attribute :last_event # Clock.now
|
9
11
|
attribute :owner # 123123
|
@@ -12,128 +14,57 @@ module SirTracksAlot
|
|
12
14
|
attribute :category # **automatically set**
|
13
15
|
attribute :action # create, view, login, etc.
|
14
16
|
attribute :user_agent # IE/Safari Windows/Mac etc.
|
15
|
-
|
16
|
-
|
17
|
+
attribute :counted # true/false
|
18
|
+
list :events # Clock.now's
|
19
|
+
|
17
20
|
index :owner
|
18
21
|
index :actor
|
19
22
|
index :target
|
20
23
|
index :category
|
24
|
+
index :last_event
|
21
25
|
index :action
|
22
|
-
index :user_agent
|
23
|
-
|
24
|
-
list :events # Clock.now's
|
25
|
-
|
26
|
-
# Find activities that match attributes
|
27
|
-
# Strings are passed to Ohm
|
28
|
-
# Regular Expression filters match against retrieved attribute values
|
29
|
-
#
|
30
|
-
# filter(:category => 'category', :target => /\/targets\/\d+/)
|
31
|
-
def self.filter(options_for_find, &block)
|
32
|
-
activities = []
|
33
|
-
sort = options_for_find.delete(:sort) || {}
|
34
|
-
|
35
|
-
strings = {}
|
36
|
-
matchers = {}
|
37
|
-
|
38
|
-
options_for_find.each do |key, candidate|
|
39
|
-
matchers[key] = candidate if candidate.kind_of?(Regexp)
|
40
|
-
strings[key] = candidate if candidate.kind_of?(String)
|
41
|
-
end
|
42
|
-
|
43
|
-
all = SirTracksAlot::Activity.find(strings).sort(sort)
|
44
|
-
|
45
|
-
all.each do |activity|
|
46
|
-
pass = true
|
47
|
-
|
48
|
-
matchers.each do |key, matcher|
|
49
|
-
pass = false if !matcher.match(activity.send(key))
|
50
|
-
end
|
26
|
+
index :user_agent
|
27
|
+
index :counted
|
51
28
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
activities
|
60
|
-
end
|
61
|
-
|
62
|
-
# resolution can be: hourly = %Y/%m/%d %H, daily = %Y/%m/%d or any valid strftime string
|
63
|
-
def self.count_by(resolution, options_for_find = {})
|
64
|
-
groups = {}
|
65
|
-
|
66
|
-
filter(options_for_find) do |activity|
|
67
|
-
activity.views(resolution).each do |time, count|
|
68
|
-
groups[time] ||= 0
|
69
|
-
groups[time] += count
|
70
|
-
end
|
29
|
+
class <<self
|
30
|
+
alias_method :ohm_create, :create
|
31
|
+
|
32
|
+
# Create with defaults: counted = 0
|
33
|
+
def create(attrs)
|
34
|
+
ohm_create(attrs.merge(:counted => '0'))
|
71
35
|
end
|
72
|
-
|
73
|
-
groups
|
74
36
|
end
|
75
|
-
|
37
|
+
|
38
|
+
# find recent activities
|
76
39
|
def self.recent(options_for_find, options_for_sort = {:order => 'DESC', :limit => LIMIT})
|
77
|
-
|
78
|
-
end
|
79
|
-
|
80
|
-
def self.count(what, options_for_find)
|
81
|
-
what = [what] unless what.kind_of?(Array)
|
82
|
-
views, visits = 0, 0
|
83
|
-
session_duration = options_for_find.delete(:session_duration)
|
84
|
-
resolution = options_for_find.delete(:resolution)
|
85
|
-
|
86
|
-
filter(options_for_find) do |activity|
|
87
|
-
views += activity.views(resolution) if what.include?(:views)
|
88
|
-
visits += activity.visits(session_duration) if what.include?(:visits)
|
89
|
-
end
|
90
|
-
|
91
|
-
return [views, visits] if what == [:views, :visits]
|
92
|
-
return [visits, views] if what == [:visits, :views]
|
93
|
-
return views if what == [:views]
|
94
|
-
return visits if what == [:visits]
|
95
|
-
raise ArgumentError("what must be one or more of :views, :visits")
|
96
|
-
end
|
97
|
-
|
98
|
-
def views(resolution = nil)
|
99
|
-
return events.size if resolution.nil?
|
100
|
-
|
101
|
-
groups = {}
|
102
|
-
|
103
|
-
date_format = resolution.kind_of?(String) ? resolution : DATE_FORMATS[resolution]
|
40
|
+
find(options_for_find).sort_by(:last_event, options_for_sort)
|
41
|
+
end
|
104
42
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
end
|
110
|
-
|
111
|
-
return groups
|
43
|
+
# Delete counted activities, leaving a default of the 500 most recent for the provided search criteria
|
44
|
+
# If left blank, purges all but the most recent Limit
|
45
|
+
def self.purge!(options_for_find = {}, options_for_sort = {:order => 'DESC'})
|
46
|
+
recent(options_for_find.merge(:counted => 1), options_for_sort).each{|a| a.delete}
|
112
47
|
end
|
113
|
-
|
114
|
-
#
|
115
|
-
|
116
|
-
|
117
|
-
session_duration = sd || 1800
|
118
|
-
count = events.size > 0 ? 1 : 0
|
119
|
-
last_event = events.first
|
120
|
-
|
121
|
-
events.each do |event|
|
122
|
-
count += 1 if (event.to_i - last_event.to_i > session_duration)
|
123
|
-
last_event = event
|
124
|
-
end
|
125
|
-
|
126
|
-
count
|
48
|
+
|
49
|
+
# set this activity to counted
|
50
|
+
def counted!
|
51
|
+
update(:counted => '1')
|
127
52
|
end
|
128
|
-
|
53
|
+
|
54
|
+
# is this activity counted?
|
55
|
+
def counted?
|
56
|
+
counted == '1'
|
57
|
+
end
|
58
|
+
|
59
|
+
|
129
60
|
private
|
130
|
-
|
61
|
+
|
131
62
|
def validate
|
132
63
|
assert_present :owner
|
133
64
|
assert_present :target
|
134
65
|
assert_present :action
|
135
66
|
assert_unique([:owner, :target, :action, :category, :user_agent, :actor])
|
136
67
|
end
|
137
|
-
|
68
|
+
|
138
69
|
end
|
139
70
|
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module SirTracksAlot
|
2
|
+
class Count < Persistable
|
3
|
+
extend FilterHelper
|
4
|
+
|
5
|
+
ACTIONS = [:create, :view, :login, :search, :update, :destroy]
|
6
|
+
|
7
|
+
attribute :created_at
|
8
|
+
attribute :date # 10/01/2010
|
9
|
+
attribute :hour # 1 - 23
|
10
|
+
attribute :owner # 123123
|
11
|
+
attribute :actor # /users/peter-brown
|
12
|
+
attribute :target # /discussions/23423
|
13
|
+
attribute :category
|
14
|
+
attribute :views
|
15
|
+
attribute :visits
|
16
|
+
|
17
|
+
index :date
|
18
|
+
index :hour
|
19
|
+
index :owner
|
20
|
+
index :actor
|
21
|
+
index :target
|
22
|
+
index :category
|
23
|
+
|
24
|
+
def self.count(options)
|
25
|
+
options = OpenStruct.new(options) if options.kind_of?(Hash)
|
26
|
+
|
27
|
+
rollup(Activity.filter(:owner => options.owner,
|
28
|
+
:target => options.target,
|
29
|
+
:action => options.action||[],
|
30
|
+
:category => options.category || [],
|
31
|
+
:counted => '0'),
|
32
|
+
options.session_duration
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.rollup(activities, session_duration)
|
37
|
+
counts = []
|
38
|
+
|
39
|
+
activities.each do |activity|
|
40
|
+
activity.views(:hourly).each do |time, views|
|
41
|
+
date, hour = time.split(' ')
|
42
|
+
counts << create_by_activity({:owner => activity.owner, :category => activity.category, :target => activity.target, :date => date, :hour => hour}, views, 0)
|
43
|
+
counts << create_by_activity({:owner => activity.owner, :category => activity.category, :actor => activity.actor, :date => date, :hour => hour}, views, 0)
|
44
|
+
end
|
45
|
+
|
46
|
+
activity.visits(session_duration, :hourly).each do |time, visits|
|
47
|
+
date, hour = time.split(' ')
|
48
|
+
counts << create_by_activity({:owner => activity.owner, :category => activity.category, :target => activity.target, :date => date, :hour => hour}, 0, visits)
|
49
|
+
counts << create_by_activity({:owner => activity.owner, :category => activity.category, :actor => activity.actor, :date => date, :hour => hour}, 0, visits)
|
50
|
+
end
|
51
|
+
|
52
|
+
activity.counted!
|
53
|
+
end
|
54
|
+
|
55
|
+
counts
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.create_by_activity(attributes, views = 0, visits = 0)
|
59
|
+
count = Count.find_or_create(attributes)
|
60
|
+
|
61
|
+
count.views ||= 0; count.visits ||= 0
|
62
|
+
|
63
|
+
count.views = count.views.to_i + views
|
64
|
+
count.visits = count.visits.to_i + visits
|
65
|
+
|
66
|
+
count.save
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def validate
|
73
|
+
assert_present :owner
|
74
|
+
assert_present :views
|
75
|
+
assert_present :visits
|
76
|
+
assert_unique([:owner, :actor, :target, :category, :date, :hour])
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module SirTracksAlot
|
2
|
+
module EventHelper
|
3
|
+
DATE_FORMATS = {:hourly => '%Y/%m/%d %H', :daily => '%Y/%m/%d'}
|
4
|
+
|
5
|
+
def views(resolution = nil)
|
6
|
+
return events.size if resolution.nil?
|
7
|
+
|
8
|
+
groups = {}
|
9
|
+
|
10
|
+
date_format = resolution.kind_of?(String) ? resolution : DATE_FORMATS[resolution]
|
11
|
+
|
12
|
+
events.each do |event|
|
13
|
+
time = Time.at(event.to_i).utc.strftime(date_format) # lop of some detail
|
14
|
+
groups[time.to_s] ||= 0
|
15
|
+
groups[time.to_s] += 1
|
16
|
+
end
|
17
|
+
|
18
|
+
return groups
|
19
|
+
end
|
20
|
+
|
21
|
+
# Count one visit for all events every session_duration (in seconds)
|
22
|
+
# e.g. 3 visits within 15 minutes counts as one, 1 visit 2 days later counts as another
|
23
|
+
def visits(sd = 1800, resolution = nil)
|
24
|
+
session_duration = sd || 1800
|
25
|
+
last_event = events.first
|
26
|
+
|
27
|
+
count = events.size > 0 ? 1 : 0 # in case last_event == event (below)
|
28
|
+
groups = {}
|
29
|
+
|
30
|
+
events.each do |event|
|
31
|
+
boundary = (event.to_i - last_event.to_i > session_duration) # have we crossed a session boundary?
|
32
|
+
|
33
|
+
if resolution.nil? # i.e. don't group
|
34
|
+
count += 1 if boundary
|
35
|
+
else
|
36
|
+
date_format = resolution.kind_of?(String) ? resolution : DATE_FORMATS[resolution]
|
37
|
+
time = Time.at(event.to_i).utc.strftime(date_format) # lop of some detail
|
38
|
+
|
39
|
+
groups[time.to_s] ||= count
|
40
|
+
groups[time.to_s] += 1 if boundary
|
41
|
+
end
|
42
|
+
|
43
|
+
last_event = event # try the next bounary
|
44
|
+
end
|
45
|
+
|
46
|
+
resolution.nil? ? count : groups
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module SirTracksAlot
|
2
|
+
module FilterHelper
|
3
|
+
# Find activities that match attributes
|
4
|
+
# Strings are passed to Ohm
|
5
|
+
# Regular Expression filters match against retrieved attribute values
|
6
|
+
#
|
7
|
+
# filter(:actor => 'user1', :target => /\/targets\/\d+/, :action => ['view', 'create'], :category => ['/root', '/other_root'])
|
8
|
+
def filter(options_for_find, &block)
|
9
|
+
items = []
|
10
|
+
all = []
|
11
|
+
|
12
|
+
strings, matchers, arrays = extract_filter_options(options_for_find)
|
13
|
+
|
14
|
+
unless arrays.empty?
|
15
|
+
(arrays.values.inject{|a, b| a.product b}).each do |combo|
|
16
|
+
terms = {}; combo.each{|c| terms[find_key_from_value(arrays, c)] = c}
|
17
|
+
all += find(terms.merge(strings)).to_a
|
18
|
+
end
|
19
|
+
else
|
20
|
+
all = find(strings)
|
21
|
+
end
|
22
|
+
|
23
|
+
all.each do |item|
|
24
|
+
pass = true
|
25
|
+
|
26
|
+
matchers.each do |key, matcher|
|
27
|
+
pass = false if !matcher.match(item.send(key))
|
28
|
+
end
|
29
|
+
|
30
|
+
next unless pass
|
31
|
+
|
32
|
+
yield item if block_given?
|
33
|
+
|
34
|
+
items << item
|
35
|
+
end
|
36
|
+
|
37
|
+
items
|
38
|
+
end
|
39
|
+
|
40
|
+
def extract_filter_options(options)
|
41
|
+
strings = {}
|
42
|
+
matchers = {}
|
43
|
+
arrays = {}
|
44
|
+
|
45
|
+
options.each do |key, candidate|
|
46
|
+
matchers[key] = candidate if candidate.kind_of?(Regexp) && !candidate.blank?
|
47
|
+
strings[key] = candidate.to_s if (candidate.kind_of?(String)) && !candidate.blank?
|
48
|
+
arrays[key] = candidate if candidate.kind_of?(Array) && !candidate.blank?
|
49
|
+
end
|
50
|
+
|
51
|
+
[strings, matchers, arrays]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'ohm'
|
2
2
|
|
3
3
|
module SirTracksAlot
|
4
|
-
class Persistable < Ohm::Model
|
4
|
+
class Persistable < Ohm::Model
|
5
5
|
attribute :created_at
|
6
6
|
|
7
7
|
def self.find_or_create(attributes)
|
@@ -9,9 +9,28 @@ module SirTracksAlot
|
|
9
9
|
models.empty? ? self.create(attributes.merge(:created_at => Clock.now)) : models.first
|
10
10
|
end
|
11
11
|
|
12
|
-
|
12
|
+
def hash
|
13
|
+
self.id
|
14
|
+
end
|
15
|
+
|
16
|
+
# Simply delegate to == in this example.
|
17
|
+
def eql?(comparee)
|
18
|
+
self == comparee
|
19
|
+
end
|
20
|
+
|
21
|
+
# Objects are equal if they have the same
|
22
|
+
# unique custom identifier.
|
23
|
+
def ==(comparee)
|
24
|
+
self.id == comparee.id
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
protected
|
13
29
|
|
14
|
-
def
|
15
|
-
|
30
|
+
def self.find_key_from_value(hash, value)
|
31
|
+
hash.each do |k, v|
|
32
|
+
return k if v.include?(value)
|
33
|
+
end
|
34
|
+
end
|
16
35
|
end
|
17
36
|
end
|
@@ -10,8 +10,7 @@ module SirTracksAlot
|
|
10
10
|
|
11
11
|
def self.push(owner, report, options)
|
12
12
|
config = ReportConfig.find_or_create(:owner => owner.to_s, :report => report.to_s)
|
13
|
-
config.options = options.merge(:owner => owner.to_s) # reports require owner, should be sync'd with config owner
|
14
|
-
|
13
|
+
config.options = options.merge(:owner => owner.to_s) # reports require owner, should be sync'd with config owner
|
15
14
|
queue = self.find_or_create(:name => QUEUE_NAME)
|
16
15
|
queue.queue << config.id
|
17
16
|
end
|
@@ -33,19 +32,19 @@ module SirTracksAlot
|
|
33
32
|
|
34
33
|
def self.process(config)
|
35
34
|
return false if config.nil?
|
36
|
-
|
35
|
+
|
37
36
|
SirTracksAlot.log.info("building report: #{config.inspect}")
|
38
|
-
|
37
|
+
|
39
38
|
begin
|
40
|
-
report = QueueHelper.constantize(QueueHelper.camelize("SirTracksAlot::Reports::#{config.report.capitalize}"))
|
41
|
-
|
42
|
-
|
39
|
+
report = QueueHelper.constantize(QueueHelper.camelize("SirTracksAlot::Reports::#{config.report.capitalize}"))
|
40
|
+
counts = Count.filter(config.options)
|
41
|
+
html = report.render_html(:counts => counts)
|
43
42
|
cache = ReportCache.find_or_create(:owner => config.owner, :report => config.report)
|
44
43
|
cache.update(:html => html)
|
45
44
|
rescue Exception => e
|
46
45
|
SirTracksAlot.log.fatal("Error building report #{config.report} for #{config.owner}: #{e}")
|
47
46
|
end
|
48
|
-
|
47
|
+
|
49
48
|
true
|
50
49
|
end
|
51
50
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module SirTracksAlot
|
2
2
|
module Reports
|
3
|
-
class ActivityReport < SirTracksAlotReport
|
3
|
+
class ActivityReport < SirTracksAlotReport
|
4
4
|
COLUMN_NAMES = ['actor', 'action', 'target', 'event']
|
5
5
|
LIMIT = 10000
|
6
6
|
|
@@ -11,7 +11,7 @@ module SirTracksAlot
|
|
11
11
|
super
|
12
12
|
options.column_names ||= COLUMN_NAMES
|
13
13
|
|
14
|
-
activities = Activity.recent({:owner => options.owner}, :order => 'DESC', :limit => (options.limit || LIMIT))
|
14
|
+
activities = Activity.recent({:owner => options.owner}, :order => 'DESC', :limit => (options.limit || LIMIT))
|
15
15
|
events = activities.collect{|a| [a.actor, a.action, a.target, a.last_event]}.reject{|a| a[2].nil?}
|
16
16
|
|
17
17
|
table = Table(options.column_names, :data => events)
|
@@ -4,44 +4,27 @@ module SirTracksAlot
|
|
4
4
|
COLUMN_NAMES = ['actor', 'page views', 'visits']
|
5
5
|
|
6
6
|
stage :actor
|
7
|
-
required_option :owner
|
8
7
|
|
9
8
|
def setup
|
10
|
-
super
|
11
|
-
|
12
|
-
options.
|
13
|
-
options.actions ||= []
|
14
|
-
options.roots ||= []
|
9
|
+
super
|
10
|
+
column_names = options.column_names || COLUMN_NAMES
|
11
|
+
counts = options.counts || {}
|
15
12
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
activities.each do |activity|
|
21
|
-
counts[activity.actor] ||= [0,0]
|
22
|
-
counts[activity.actor][0] += activity.views
|
23
|
-
counts[activity.actor][1] += activity.visits(options.session_duration)
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
table = Table(options.column_names) do |t|
|
29
|
-
counts.each do |target, count|
|
30
|
-
t << [target, count[0], count[1]]
|
13
|
+
table = Table(column_names) do |t|
|
14
|
+
counts.each do |count|
|
15
|
+
t << [count.actor, count.visits, count.views]
|
31
16
|
end
|
32
17
|
end
|
33
18
|
|
34
19
|
table.sort_rows_by!('page views', :order => :descending)
|
35
20
|
|
36
21
|
self.data = table
|
37
|
-
|
38
22
|
end
|
39
23
|
|
40
24
|
module Helpers
|
41
25
|
include Report::Helpers
|
42
26
|
end
|
43
27
|
|
44
|
-
|
45
28
|
class HTML < Ruport::Formatter::HTML
|
46
29
|
renders :html, :for => ActorReport
|
47
30
|
|
@@ -22,18 +22,25 @@ module SirTracksAlot
|
|
22
22
|
def setup
|
23
23
|
super
|
24
24
|
|
25
|
-
counts =
|
25
|
+
counts = {}
|
26
26
|
options.filters ||= {}
|
27
27
|
options.column_names ||= COLUMN_NAMES
|
28
|
-
|
29
|
-
options.filters.each do |title, options_for_find|
|
30
|
-
views, visits = Activity.count([:views, :visits], options_for_find.merge(:owner => options.owner))
|
31
|
-
counts << [title, views, visits] if views > 0
|
32
|
-
end
|
33
28
|
|
34
29
|
|
35
|
-
|
36
|
-
|
30
|
+
options.filters.each do |title, options_for_find|
|
31
|
+
Count.filter(options_for_find.merge(:owner => options.owner)).each do |count|
|
32
|
+
counts[title] ||= [0,0]
|
33
|
+
counts[title][0] += count.visits.to_i
|
34
|
+
counts[title][1] += count.views.to_i
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
table = Table(options.column_names) do |t|
|
39
|
+
counts.each do |title, n|
|
40
|
+
t << [title, n[0], n[1]]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
37
44
|
table.sort_rows_by!('page views', :order => :descending)
|
38
45
|
|
39
46
|
self.data = table
|
@@ -4,31 +4,15 @@ module SirTracksAlot
|
|
4
4
|
COLUMN_NAMES = ['target', 'page views', 'visits']
|
5
5
|
|
6
6
|
stage :target
|
7
|
-
required_option :owner, :actions
|
8
7
|
|
9
8
|
def setup
|
10
9
|
super
|
11
|
-
|
12
|
-
options.
|
13
|
-
options.column_names ||= COLUMN_NAMES
|
14
|
-
options.actions ||= []
|
15
|
-
options.roots ||= []
|
10
|
+
column_names = options.column_names || COLUMN_NAMES
|
11
|
+
counts = options.counts || {}
|
16
12
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
activities.each do |activity|
|
22
|
-
counts[activity.target] ||= [0,0]
|
23
|
-
counts[activity.target][0] += activity.views
|
24
|
-
counts[activity.target][1] += activity.visits(options.session_duration)
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
table = Table(options.column_names) do |t|
|
30
|
-
counts.each do |target, count|
|
31
|
-
t << [target, count[0], count[1]]
|
13
|
+
table = Table(column_names) do |t|
|
14
|
+
counts.each do |count|
|
15
|
+
t << [count.target, count.visits, count.views]
|
32
16
|
end
|
33
17
|
end
|
34
18
|
|
@@ -41,7 +25,6 @@ module SirTracksAlot
|
|
41
25
|
include Report::Helpers
|
42
26
|
end
|
43
27
|
|
44
|
-
|
45
28
|
class HTML < Ruport::Formatter::HTML
|
46
29
|
renders :html, :for => TargetReport
|
47
30
|
|
data/lib/sir_tracks_alot.rb
CHANGED
@@ -6,12 +6,20 @@ require "bundler"
|
|
6
6
|
Bundler.setup
|
7
7
|
Bundler.require
|
8
8
|
|
9
|
+
# require 'logging'
|
10
|
+
# require 'twitter'
|
11
|
+
# require 'ruport'
|
12
|
+
# require 'ohm'
|
13
|
+
# require 'redis'
|
14
|
+
|
9
15
|
module SirTracksAlot
|
10
16
|
TRACKABLE_ROOT = "#{File.dirname(__FILE__)}/.."
|
11
17
|
|
12
18
|
autoload :Persistable, File.join(File.dirname(__FILE__), *%w[sir_tracks_alot persistable.rb])
|
13
|
-
autoload :Rollup, File.join(File.dirname(__FILE__), *%w[sir_tracks_alot rollup.rb])
|
14
19
|
autoload :Activity, File.join(File.dirname(__FILE__), *%w[sir_tracks_alot activity.rb])
|
20
|
+
autoload :EventHelper, File.join(File.dirname(__FILE__), *%w[sir_tracks_alot event_helper.rb])
|
21
|
+
autoload :FilterHelper, File.join(File.dirname(__FILE__), *%w[sir_tracks_alot filter_helper.rb])
|
22
|
+
autoload :Count, File.join(File.dirname(__FILE__), *%w[sir_tracks_alot count.rb])
|
15
23
|
autoload :Clock, File.join(File.dirname(__FILE__), *%w[sir_tracks_alot clock.rb])
|
16
24
|
|
17
25
|
class SirTracksAlotError < StandardError
|
@@ -49,7 +57,9 @@ module SirTracksAlot
|
|
49
57
|
|
50
58
|
raise RecordInvalidError.new("Activity not valid: #{activity.errors.inspect}") unless activity.valid?
|
51
59
|
|
52
|
-
|
60
|
+
raise "EVENT NIL" if event.nil?
|
61
|
+
|
62
|
+
activity.update(:last_event => event)
|
53
63
|
activity.events << event
|
54
64
|
activity
|
55
65
|
end
|
data/spec/activity_spec.rb
CHANGED
@@ -6,15 +6,12 @@ describe SirTracksAlot::Activity do
|
|
6
6
|
before do
|
7
7
|
RedisSpecHelper.reset
|
8
8
|
@now = SirTracksAlot::Clock.now
|
9
|
-
|
10
9
|
@activity = SirTracksAlot::Activity.create(@activities[0])
|
11
|
-
@activity.events << @now
|
10
|
+
@activity.events << @now
|
11
|
+
@activity.update(:last_event => @now)
|
12
12
|
end
|
13
13
|
|
14
14
|
context 'when creating' do
|
15
|
-
before do
|
16
|
-
end
|
17
|
-
|
18
15
|
it "should store valid activities" do
|
19
16
|
@activity.should be_valid
|
20
17
|
end
|
@@ -28,13 +25,44 @@ describe SirTracksAlot::Activity do
|
|
28
25
|
activity = SirTracksAlot::Activity.create(@activities[0]) # already created in before
|
29
26
|
activity.should_not be_valid
|
30
27
|
end
|
28
|
+
|
29
|
+
it "should set counted" do
|
30
|
+
@activity.counted!
|
31
|
+
@activity.should be_counted
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should default to uncounted" do
|
35
|
+
@activity.counted.should == '0'
|
36
|
+
end
|
31
37
|
end
|
32
38
|
|
33
|
-
context 'when
|
39
|
+
context 'when purging' do
|
40
|
+
before do
|
41
|
+
@activities.each{|a| SirTracksAlot.record(a)}
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should not purge uncounted" do
|
45
|
+
SirTracksAlot::Activity.purge!
|
46
|
+
SirTracksAlot::Activity.all.size.should == 8
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should purge counted" do
|
50
|
+
SirTracksAlot::Activity.all.each{|a| a.counted!}
|
51
|
+
SirTracksAlot::Activity.purge!
|
52
|
+
SirTracksAlot::Activity.all.size.should == 0
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should purge counted by range" do
|
56
|
+
SirTracksAlot::Activity.all.each{|a| a.counted!}
|
57
|
+
SirTracksAlot::Activity.purge!({}, :start => 5, :limit => 10)
|
58
|
+
SirTracksAlot::Activity.all.size.should == 5
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context 'when filtering (simple)' do
|
34
63
|
before do
|
35
64
|
@mock_activity = mock(SirTracksAlot::Activity, @activities[0])
|
36
|
-
|
37
|
-
SirTracksAlot::Activity.stub!(:find => @mock_activityz)
|
65
|
+
SirTracksAlot::Activity.stub!(:find => [@mock_activity])
|
38
66
|
end
|
39
67
|
|
40
68
|
it "should find activities matching attribute regex" do
|
@@ -55,61 +83,87 @@ describe SirTracksAlot::Activity do
|
|
55
83
|
|
56
84
|
it "should not match with negative regex" do
|
57
85
|
SirTracksAlot::Activity.filter(:owner => /^(?!(.*owner.*))/).size.should == 0
|
58
|
-
end
|
86
|
+
end
|
59
87
|
end
|
60
88
|
|
61
|
-
context 'when
|
62
|
-
it "should get recent" do
|
63
|
-
SirTracksAlot::Activity.recent(:owner => 'owner').should be_instance_of(Array)
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
context 'when counting' do
|
89
|
+
context 'when filtering (many)' do
|
68
90
|
before do
|
69
|
-
@
|
70
|
-
@mock_activityz = mock('SortedActivity', :sort => [@mock_activity])
|
71
|
-
SirTracksAlot::Activity.stub!(:find => @mock_activityz)
|
72
|
-
end
|
73
|
-
|
74
|
-
it "should look for activities using find options" do
|
75
|
-
SirTracksAlot::Activity.should_receive(:find).with(:owner => 'owner').and_return(@mock_activityz)
|
76
|
-
SirTracksAlot::Activity.count(:views, :owner => 'owner')
|
91
|
+
@activities.each{|a| SirTracksAlot.record(a)}
|
77
92
|
end
|
78
93
|
|
79
|
-
it "should
|
80
|
-
|
81
|
-
SirTracksAlot::Activity.count(:views, :owner => 'owner')
|
94
|
+
it "should filter by members of one array" do
|
95
|
+
SirTracksAlot::Activity.filter(:owner => 'owner', :target => ['/categories/item1', '/categories/item2']).size.should == 3
|
82
96
|
end
|
83
97
|
|
84
|
-
it "should
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
it "should return visits" do
|
94
|
-
SirTracksAlot::Activity.count([:visits], :owner => 'owner').should == 1
|
98
|
+
it "should filter by combinations of arrays" do
|
99
|
+
SirTracksAlot::Activity.filter(:owner => 'owner', :action => ['view', 'create'], :target => ['/categories/item1', '/categories/item2']).size.should == 3
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
context 'when getting recent' do
|
104
|
+
before do
|
105
|
+
@set_activities.each{|a| SirTracksAlot.record(a)}
|
95
106
|
end
|
96
107
|
|
97
|
-
it "should
|
98
|
-
SirTracksAlot::Activity.
|
108
|
+
it "should get recent" do
|
109
|
+
SirTracksAlot::Activity.recent(:owner => 'owner').should be_instance_of(Array)
|
99
110
|
end
|
100
111
|
|
101
|
-
it "should
|
102
|
-
|
103
|
-
|
104
|
-
|
112
|
+
it "should purge counted by range, sorting to most recent by last event" do
|
113
|
+
SirTracksAlot::Activity.recent(:owner => 'owner').first.last_event.should == @now.to_s
|
114
|
+
end
|
115
|
+
|
116
|
+
it "should purge counted by range, sorting to most recent by last event" do
|
117
|
+
SirTracksAlot::Activity.recent({:owner => 'owner'}, :start => 1).first.last_event.should == '1279709941'
|
118
|
+
end
|
105
119
|
|
106
|
-
it "should filter by category and match returning correct count" do
|
107
|
-
@mock_activity.stub!(:category => 'category')
|
108
|
-
SirTracksAlot::Activity.count([:visits], :owner => 'owner', :category => /category/).should == 1
|
109
|
-
end
|
110
120
|
end
|
111
121
|
|
112
|
-
context 'when counting
|
122
|
+
context 'when counting' do
|
123
|
+
before do
|
124
|
+
@mock_activity = mock(SirTracksAlot::Activity, :visits => 1, :views => 2)
|
125
|
+
SirTracksAlot::Activity.stub!(:find => [@mock_activity])
|
126
|
+
end
|
127
|
+
|
128
|
+
# it "should look for activities using find options" do
|
129
|
+
# SirTracksAlot::Activity.should_receive(:find).with(:owner => 'owner').and_return([@mock_activity])
|
130
|
+
# SirTracksAlot::Activity.count(:views, :owner => 'owner')
|
131
|
+
# end
|
132
|
+
#
|
133
|
+
# it "should look for views when counting views" do
|
134
|
+
# @mock_activity.should_receive(:views).once.and_return(1)
|
135
|
+
# SirTracksAlot::Activity.count(:views, :owner => 'owner')
|
136
|
+
# end
|
137
|
+
#
|
138
|
+
# it "should not look for views when counting visits" do
|
139
|
+
# @mock_activity.should_receive(:views).never
|
140
|
+
# SirTracksAlot::Activity.count(:visits, :owner => 'owner')
|
141
|
+
# end
|
142
|
+
#
|
143
|
+
# it "should return views and visits" do
|
144
|
+
# SirTracksAlot::Activity.count([:visits, :views], :owner => 'owner').should == [1,2]
|
145
|
+
# end
|
146
|
+
#
|
147
|
+
# it "should return visits" do
|
148
|
+
# SirTracksAlot::Activity.count([:visits], :owner => 'owner').should == 1
|
149
|
+
# end
|
150
|
+
#
|
151
|
+
# it "should ignore empty only" do
|
152
|
+
# SirTracksAlot::Activity.count([:visits], :owner => 'owner', :only => {}).should == 1
|
153
|
+
# end
|
154
|
+
#
|
155
|
+
# it "should filter by category" do
|
156
|
+
# @mock_activity.should_receive(:category).once.and_return('category')
|
157
|
+
# SirTracksAlot::Activity.count([:visits], :owner => 'owner', :category => /category/)
|
158
|
+
# end
|
159
|
+
#
|
160
|
+
# it "should filter by category and match returning correct count" do
|
161
|
+
# @mock_activity.stub!(:category => 'category')
|
162
|
+
# SirTracksAlot::Activity.count([:visits], :owner => 'owner', :category => /category/).should == 1
|
163
|
+
# end
|
164
|
+
end
|
165
|
+
|
166
|
+
context 'when counting views' do
|
113
167
|
before do
|
114
168
|
# nothing
|
115
169
|
end
|
@@ -136,12 +190,7 @@ describe SirTracksAlot::Activity do
|
|
136
190
|
it "should return count multiple for single group" do
|
137
191
|
SirTracksAlot::Activity.all.first.events << @now
|
138
192
|
@activity.views(:daily).values.should == [2]
|
139
|
-
end
|
140
|
-
|
141
|
-
it "should return count multiple for single group" do
|
142
|
-
# SirTracksAlot::Activity.count_by(:daily).should == ''
|
143
|
-
end
|
144
|
-
|
193
|
+
end
|
145
194
|
end
|
146
195
|
|
147
196
|
context 'when calculating visits' do
|
@@ -161,13 +210,19 @@ describe SirTracksAlot::Activity do
|
|
161
210
|
@activity.visits.should == 2
|
162
211
|
end
|
163
212
|
|
164
|
-
it "should count 2 visits separated by greater than default session duration as 2
|
213
|
+
it "should count 2 visits separated by greater than default session duration as 2, and a 3rd" do
|
165
214
|
5.times{@activity.events << SirTracksAlot::Clock.now}
|
166
215
|
@activity.events << SirTracksAlot::Clock.now + 2000
|
167
216
|
@activity.events << SirTracksAlot::Clock.now + 4000
|
168
217
|
@activity.visits.should == 3
|
169
218
|
end
|
170
219
|
|
220
|
+
it "should group separated visits hourly" do
|
221
|
+
5.times{@activity.events << 1279735846}
|
222
|
+
@activity.events << 1279709941
|
223
|
+
# @activity.visits(1800, :hourly).should == {"2010/07/27 20"=>1, "2010/07/27 21"=>2}
|
224
|
+
end
|
225
|
+
|
171
226
|
end
|
172
227
|
|
173
228
|
end
|
data/spec/count_spec.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
+
|
3
|
+
describe SirTracksAlot::Count do
|
4
|
+
include DataHelper
|
5
|
+
|
6
|
+
before do
|
7
|
+
RedisSpecHelper.reset
|
8
|
+
|
9
|
+
@set_activities.each{|a| SirTracksAlot.record(a)}
|
10
|
+
end
|
11
|
+
|
12
|
+
context 'when creating' do
|
13
|
+
before do
|
14
|
+
@count_attributes = [{:owner => 'owner', :actor => '/users/user1', :target => '/categories/item1', :views => 1, :visits => 2, :date => '07/01/2010', :hour => 5}]
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should create valid count' do
|
18
|
+
SirTracksAlot::Count.create(@count_attributes[0]).should be_valid
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should not validate duplicates' do
|
22
|
+
SirTracksAlot::Count.create(@count_attributes[0])
|
23
|
+
SirTracksAlot::Count.create(@count_attributes[0]).should_not be_valid
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context 'when filtering' do
|
28
|
+
before do
|
29
|
+
SirTracksAlot::Count.count(:owner => 'owner')
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should filter by string attribute' do
|
33
|
+
SirTracksAlot::Count.filter(:owner => 'owner').size.should == 13
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'should filter by array of strings' do
|
37
|
+
SirTracksAlot::Count.filter(:target => ['/categories', '/other_categories/item']).size.should == 2
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'should include all targets when filtering just by ower' do
|
41
|
+
SirTracksAlot::Count.filter(:owner => 'owner').collect{|c| c.target}.compact.sort.should ==
|
42
|
+
["/categories", "/categories/item1", "/categories/item1", "/categories/item1", "/categories/item2", "/other_categories", "/other_categories/item"].sort
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'should include only filtered targets' do
|
46
|
+
SirTracksAlot::Count.filter(:owner => 'owner', :target => ["/categories", "/categories/item1"]).collect{|c| c.target}.should_not
|
47
|
+
include("/categories/item2")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context 'when counting' do
|
52
|
+
before do
|
53
|
+
@counts = SirTracksAlot::Count.count(:owner => 'owner', :category => ['categories'], :action => ['view'])
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should count all activities for owner" do
|
57
|
+
SirTracksAlot::Count.count(:owner => 'owner')
|
58
|
+
SirTracksAlot::Count.find(:owner => 'owner').size.should == 13 # one for each unique target, time combo
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should set to counted all activities counted" do
|
62
|
+
SirTracksAlot::Count.count(:owner => 'owner')
|
63
|
+
SirTracksAlot::Activity.filter(:counted => "0").should be_empty
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should only count activities once" do
|
67
|
+
SirTracksAlot::Count.count(:owner => 'owner', :category => ['categories'], :action => ['view']).should be_empty
|
68
|
+
end
|
69
|
+
|
70
|
+
context 'views' do
|
71
|
+
it "should include counts by hour" do
|
72
|
+
@counts[0].hour.should == '18'
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should not include counts not requested" do
|
76
|
+
@counts.collect{|c| c.target}.should_not include("/other_categories")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context 'visits' do
|
81
|
+
it 'should include counts by hour' do
|
82
|
+
# @counts[1].should == {"/categories"=>{"2010/07/21 18"=>1},
|
83
|
+
# "/categories/item1"=>{"2010/07/21 18"=>2, "2010/07/21 10"=>1, "2010/07/21 13"=>1},
|
84
|
+
# "/categories/item2"=>{"2010/07/21 18"=>1}}
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -18,7 +18,7 @@ describe SirTracksAlot::Queue::ReportQueue do
|
|
18
18
|
@activities.each{|a| SirTracksAlot.record(a)}
|
19
19
|
SirTracksAlot::Queue::ReportQueue.push('owner', :actor_report, {})
|
20
20
|
end
|
21
|
-
|
21
|
+
|
22
22
|
it "should create a new queue" do
|
23
23
|
SirTracksAlot::Queue::ReportQueue.should_receive(:find_or_create).with(:name => SirTracksAlot::Queue::ReportQueue::QUEUE_NAME).and_return(@queue)
|
24
24
|
SirTracksAlot::Queue::ReportQueue.push('owner', 'report', {})
|
@@ -34,7 +34,7 @@ describe SirTracksAlot::Queue::ReportQueue do
|
|
34
34
|
before do
|
35
35
|
@cache = mock(SirTracksAlot::Queue::ReportCache, :update => true)
|
36
36
|
@activities.each{|a| SirTracksAlot.record(a)}
|
37
|
-
SirTracksAlot::Queue::ReportQueue.push('owner', :
|
37
|
+
SirTracksAlot::Queue::ReportQueue.push('owner', :target_report, {:category => ['categories', 'other_categories']})
|
38
38
|
end
|
39
39
|
|
40
40
|
it "should find existing queue" do
|
@@ -43,19 +43,19 @@ describe SirTracksAlot::Queue::ReportQueue do
|
|
43
43
|
end
|
44
44
|
|
45
45
|
it "should create report cache" do
|
46
|
-
SirTracksAlot::Queue::ReportCache.should_receive(:find_or_create).with(:owner => 'owner', :report => '
|
46
|
+
SirTracksAlot::Queue::ReportCache.should_receive(:find_or_create).with(:owner => 'owner', :report => 'target_report').and_return(@cache)
|
47
47
|
SirTracksAlot::Queue::ReportQueue.pop
|
48
48
|
end
|
49
49
|
|
50
50
|
it "should constantize report by name" do
|
51
|
-
SirTracksAlot::Queue::QueueHelper.should_receive(:constantize).with("SirTracksAlot::Reports::
|
51
|
+
SirTracksAlot::Queue::QueueHelper.should_receive(:constantize).with("SirTracksAlot::Reports::TargetReport").and_return(SirTracksAlot::Reports::TargetReport)
|
52
52
|
SirTracksAlot::Queue::ReportQueue.pop
|
53
53
|
end
|
54
54
|
|
55
55
|
it "should build report HTML" do
|
56
|
-
SirTracksAlot::Queue::ReportCache.should_receive(:find_or_create).with(:owner => 'owner', :report => '
|
56
|
+
SirTracksAlot::Queue::ReportCache.should_receive(:find_or_create).with(:owner => 'owner', :report => 'target_report').and_return(@cache)
|
57
57
|
@cache.should_receive(:update) do |options|
|
58
|
-
options[:html].should have_tag('td.
|
58
|
+
options[:html].should have_tag('td.target', /\/categories\/item1/)
|
59
59
|
end
|
60
60
|
SirTracksAlot::Queue::ReportQueue.pop
|
61
61
|
end
|
@@ -6,22 +6,12 @@ describe SirTracksAlot::Reports::ActorReport do
|
|
6
6
|
before do
|
7
7
|
RedisSpecHelper.reset
|
8
8
|
@activities.each{|a| SirTracksAlot.record(a)}
|
9
|
-
@mock_finder = mock('Finder', :find => [], :collect => [], :each => [])
|
10
9
|
end
|
11
10
|
|
12
|
-
it "should raise error on mission options" do
|
13
|
-
lambda{SirTracksAlot::Reports::ActorReport.render_html({})}.should raise_error(Ruport::Controller::RequiredOptionNotSet)
|
14
|
-
end
|
15
|
-
|
16
|
-
it "should find activities when building data" do
|
17
|
-
SirTracksAlot::Activity.should_receive(:find).exactly(1).times.and_return(@mock_finder)
|
18
|
-
SirTracksAlot::Reports::ActorReport.render_html(@report_attributes)
|
19
|
-
end
|
20
|
-
|
21
11
|
context 'building HTML' do
|
22
12
|
before do
|
23
|
-
SirTracksAlot.
|
24
|
-
@html = SirTracksAlot::Reports::ActorReport.render_html(@
|
13
|
+
@counts = SirTracksAlot::Count.count(OpenStruct.new(:owner => 'owner', :roots => ['categories']))
|
14
|
+
@html = SirTracksAlot::Reports::ActorReport.render_html(:counts => @counts)
|
25
15
|
end
|
26
16
|
|
27
17
|
it "include target row" do
|
@@ -31,6 +21,10 @@ describe SirTracksAlot::Reports::ActorReport do
|
|
31
21
|
it "include count row" do
|
32
22
|
@html.should have_tag('td.count', /1/)
|
33
23
|
end
|
24
|
+
|
25
|
+
it "include page views" do
|
26
|
+
@html.should have_tag('td.page_views', /2/)
|
27
|
+
end
|
34
28
|
|
35
29
|
it "should ignore other owners" do
|
36
30
|
@html.should_not have_tag('td.actor', '/users/user')
|
@@ -1,16 +1,13 @@
|
|
1
1
|
require File.dirname(__FILE__) + '/../spec_helper.rb'
|
2
2
|
|
3
|
-
describe SirTracksAlot::Reports::
|
3
|
+
describe SirTracksAlot::Reports::FilterReport do
|
4
4
|
include DataHelper
|
5
5
|
|
6
6
|
before do
|
7
7
|
RedisSpecHelper.reset
|
8
8
|
@report_options = {:owner => 'owner'}
|
9
9
|
@activities.each{|a| SirTracksAlot.record(a)}
|
10
|
-
|
11
|
-
|
12
|
-
it "should render empty" do
|
13
|
-
SirTracksAlot::Reports::FilterReport.render_html(@report_options)
|
10
|
+
SirTracksAlot::Count.count(OpenStruct.new(:owner => 'owner', :roots => ['categories']))
|
14
11
|
end
|
15
12
|
|
16
13
|
context 'filtering things with only' do
|
@@ -23,6 +20,7 @@ describe SirTracksAlot::Reports::ActivityReport do
|
|
23
20
|
'blank' => {:target => /blanks/}
|
24
21
|
}
|
25
22
|
}
|
23
|
+
|
26
24
|
@html = SirTracksAlot::Reports::FilterReport.render_html(@report_options)
|
27
25
|
end
|
28
26
|
|
@@ -6,10 +6,7 @@ describe SirTracksAlot::Reports::RootStemReport do
|
|
6
6
|
before do
|
7
7
|
RedisSpecHelper.reset
|
8
8
|
@activities.each{|a| SirTracksAlot.record(a)}
|
9
|
-
|
10
|
-
|
11
|
-
it "should render empty" do
|
12
|
-
SirTracksAlot::Reports::FilterReport.render_html(:owner => 'owner')
|
9
|
+
SirTracksAlot::Count.count(OpenStruct.new(:owner => 'owner', :roots => ['categories']))
|
13
10
|
end
|
14
11
|
|
15
12
|
context 'building HTML with categories' do
|
@@ -6,57 +6,26 @@ describe SirTracksAlot::Reports::TargetReport do
|
|
6
6
|
before do
|
7
7
|
RedisSpecHelper.reset
|
8
8
|
@activities.each{|a| SirTracksAlot.record(a)}
|
9
|
-
@mock_finder = mock('Finder', :find => [], :collect => [])
|
10
|
-
@mock_filter = mock('Filter', :each => [])
|
11
9
|
end
|
12
10
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
it "should find activities when building data" do
|
18
|
-
SirTracksAlot::Activity.should_receive(:filter).with(
|
19
|
-
:action => @activities[0][:action], :owner => @activities[0][:owner], :target => nil, :category=>"categories").exactly(1).times.and_return(@mock_filter)
|
20
|
-
SirTracksAlot::Reports::TargetReport.render_html(@report_attributes)
|
21
|
-
end
|
22
|
-
|
23
|
-
context 'building HTML without filters' do
|
24
|
-
before do
|
25
|
-
SirTracksAlot.record(:owner => 'other_owner', :category => 'other_category', :target => '/other_categories/item', :actor => '/users/user', :action => 'view')
|
26
|
-
@html = SirTracksAlot::Reports::TargetReport.render_html(@report_attributes)
|
11
|
+
context 'building HTML' do
|
12
|
+
before do
|
13
|
+
@counts = SirTracksAlot::Count.count(OpenStruct.new(:owner => 'owner', :roots => ['categories']))
|
14
|
+
@html = SirTracksAlot::Reports::TargetReport.render_html(:counts => @counts)
|
27
15
|
end
|
28
16
|
|
29
17
|
it_should_behave_like 'all reports'
|
30
18
|
|
31
|
-
it "include target row" do
|
19
|
+
it "include target row" do
|
32
20
|
@html.should have_tag('td.target', /\/categories\/item/)
|
33
21
|
end
|
34
22
|
|
35
23
|
it "include count row" do
|
36
24
|
@html.should have_tag('td.count', /1/)
|
37
25
|
end
|
38
|
-
|
26
|
+
|
39
27
|
it "should ignore other owners" do
|
40
28
|
@html.should_not have_tag('td.target', '/other_categories/item')
|
41
29
|
end
|
42
30
|
end
|
43
|
-
|
44
|
-
context 'building HTML with filters' do
|
45
|
-
before do
|
46
|
-
@html = SirTracksAlot::Reports::TargetReport.render_html(
|
47
|
-
@report_attributes.merge(
|
48
|
-
:filters => {:except => [{:target => /^\/categories$/}]}
|
49
|
-
)
|
50
|
-
)
|
51
|
-
end
|
52
|
-
|
53
|
-
it "should include non excepted target row" do
|
54
|
-
@html.should have_tag('td.target', /\/categories\/item/)
|
55
|
-
end
|
56
|
-
|
57
|
-
it "should not include excepted target row" do
|
58
|
-
@html.should_not have_tag('td.target', /^\/categories$/)
|
59
|
-
end
|
60
|
-
|
61
|
-
end
|
62
31
|
end
|
@@ -6,6 +6,7 @@ describe SirTracksAlot, 'when recording' do
|
|
6
6
|
before do
|
7
7
|
RedisSpecHelper.reset
|
8
8
|
@record_attributes = @activities[0]
|
9
|
+
@now = SirTracksAlot::Clock.now
|
9
10
|
end
|
10
11
|
|
11
12
|
it "should record activity" do
|
@@ -54,7 +55,7 @@ describe SirTracksAlot, 'when recording' do
|
|
54
55
|
end
|
55
56
|
|
56
57
|
it "should add date for now" do
|
57
|
-
SirTracksAlot::Clock.should_receive(:now).twice # once for activity create, once for events date
|
58
|
+
SirTracksAlot::Clock.should_receive(:now).twice.and_return(@now) # once for activity create, once for events date
|
58
59
|
SirTracksAlot.record(@record_attributes)
|
59
60
|
end
|
60
61
|
|
data/spec/spec_helper.rb
CHANGED
@@ -31,6 +31,38 @@ module DataHelper
|
|
31
31
|
def initialize(*attrs)
|
32
32
|
super
|
33
33
|
|
34
|
+
@set_activities = [
|
35
|
+
{:owner => 'owner', :target => '/categories/item1', :actor => '/users/user1',
|
36
|
+
:action => 'view', :user_agent => 'agent1', :event => 1279735846}, # 1.day.ago
|
37
|
+
|
38
|
+
{:owner => 'owner', :target => '/categories/item1', :actor => '/users/user1',
|
39
|
+
:action => 'view', :user_agent => 'agent1', :event => 1279735846}, # 1.day.ago
|
40
|
+
|
41
|
+
{:owner => 'owner', :target => '/categories/item1', :actor => '/users/user1',
|
42
|
+
:action => 'view', :user_agent => 'agent1', :event => 1279718578}, # 1.2.days.ago
|
43
|
+
|
44
|
+
{:owner => 'owner', :target => '/categories/item1', :actor => '/users/user1',
|
45
|
+
:action => 'view', :user_agent => 'agent1', :event => 1279709941}, # 1.3.days.ago
|
46
|
+
|
47
|
+
{:owner => 'owner', :target => '/categories/item2', :actor => '/users/user2',
|
48
|
+
:action => 'view', :user_agent => 'agent2', :event => 1279735846}, # 1.day.ago
|
49
|
+
|
50
|
+
{:owner => 'owner', :target => '/categories', :actor => '/users/user2',
|
51
|
+
:action => 'view', :user_agent => 'agent2', :event => 1279735846}, # 1.day.ago
|
52
|
+
|
53
|
+
{:owner => 'owner', :target => '/other_categories/item', :actor => '/users/user1',
|
54
|
+
:action => 'view', :user_agent => 'agent1', :event => 1279735846}, # 1.day.ago
|
55
|
+
|
56
|
+
{:owner => 'owner', :target => '/other_categories/item', :actor => '/users/user2',
|
57
|
+
:action => 'view', :user_agent => 'agent2', :event => 1279735846}, # 1.day.ago
|
58
|
+
|
59
|
+
{:owner => 'owner', :target => '/other_categories', :actor => '/users/user1',
|
60
|
+
:action => 'view', :user_agent => 'agent1', :event => 1279735846}, # 1.day.ago
|
61
|
+
|
62
|
+
{:owner => 'owner', :target => '/other_categories', :actor => '/users/user2',
|
63
|
+
:action => 'view', :user_agent => 'agent2', :event => 1279735846} # 1.day.ago
|
64
|
+
]
|
65
|
+
|
34
66
|
@activities = [
|
35
67
|
{:owner => 'owner', :target => '/categories/item1', :actor => '/users/user1', :action => 'view', :user_agent => 'agent1'},
|
36
68
|
{:owner => 'owner', :target => '/categories/item2', :actor => '/users/user2', :action => 'view', :user_agent => 'agent2'},
|
@@ -40,8 +72,10 @@ module DataHelper
|
|
40
72
|
{:owner => 'owner', :target => '/other_categories', :actor => '/users/user1', :action => 'view', :user_agent => 'agent1'},
|
41
73
|
{:owner => 'owner', :target => '/other_categories', :actor => '/users/user2', :action => 'view', :user_agent => 'agent2'}
|
42
74
|
]
|
43
|
-
|
44
|
-
@
|
75
|
+
|
76
|
+
@counts = {"/categories" => [1, 1], "/categories/item1" => [1, 1], "/categories/item2" => [1, 1]}
|
77
|
+
|
78
|
+
@report_attributes = {:owner => 'owner', :roots => ['categories'], :actions => ['view'], :counts => @counts}
|
45
79
|
end
|
46
80
|
end
|
47
81
|
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sir_tracks_alot
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 15
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
8
|
+
- 4
|
9
9
|
- 0
|
10
|
-
version: 0.
|
10
|
+
version: 0.4.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Peter T. Brown
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2010-
|
18
|
+
date: 2010-08-05 00:00:00 -07:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -150,6 +150,9 @@ files:
|
|
150
150
|
- lib/sir_tracks_alot.rb
|
151
151
|
- lib/sir_tracks_alot/activity.rb
|
152
152
|
- lib/sir_tracks_alot/clock.rb
|
153
|
+
- lib/sir_tracks_alot/count.rb
|
154
|
+
- lib/sir_tracks_alot/event_helper.rb
|
155
|
+
- lib/sir_tracks_alot/filter_helper.rb
|
153
156
|
- lib/sir_tracks_alot/persistable.rb
|
154
157
|
- lib/sir_tracks_alot/queue/queue_helper.rb
|
155
158
|
- lib/sir_tracks_alot/queue/report_cache.rb
|
@@ -165,6 +168,7 @@ files:
|
|
165
168
|
- lib/sir_tracks_alot/reports/target_report.rb
|
166
169
|
- lib/sir_tracks_alot/reports/trackable_report.rb
|
167
170
|
- spec/activity_spec.rb
|
171
|
+
- spec/count_spec.rb
|
168
172
|
- spec/queue/report_config_spec.rb
|
169
173
|
- spec/queue/report_queue_spec.rb
|
170
174
|
- spec/reports/activity_report_spec.rb
|
@@ -218,6 +222,7 @@ specification_version: 3
|
|
218
222
|
summary: A high speed general purpose tracking and reporting tool which uses Redis.
|
219
223
|
test_files:
|
220
224
|
- spec/activity_spec.rb
|
225
|
+
- spec/count_spec.rb
|
221
226
|
- spec/queue/report_config_spec.rb
|
222
227
|
- spec/queue/report_queue_spec.rb
|
223
228
|
- spec/reports/activity_report_spec.rb
|