sir_tracks_alot 0.2.0 → 0.4.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/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
|