stats_combiner 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +8 -0
- data/README.md +85 -0
- data/Rakefile +25 -0
- data/VERSION +1 -0
- data/combiner_sample.rb +26 -0
- data/lib/stats_combiner.rb +182 -0
- data/lib/stats_combiner/filterer.rb +111 -0
- data/spec/stats_combiner_spec.rb +294 -0
- data/spec/test_data.rb +9 -0
- data/stats_combiner.gemspec +68 -0
- metadata +143 -0
data/.gitignore
ADDED
data/README.md
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
# StatsCombiner
|
2
|
+
|
3
|
+
StatsCombiner is a Ruby gem for generating most-viewed widgets from the [Chartbeat API](http://chartbeat.pbworks.com/). Unlike most analytics systems, Chartbeat doesn't give you cumulative visitor counts. Rather, it takes live snapshots of people sitting on pages at a given time. StatsCombiner asks Chartbeat what these numbers are every `n` minutes during a given `ttl` and combines visitor counts where it finds matching `<title>`s, to allow popular stories to bubble up the list. When `ttl` expires, it will publish out a static HTML file with your top ten list to a location of your choosing, trash its database and start collecting again.
|
4
|
+
|
5
|
+
This gem is a rewrite from the PHP implementation I wrote about [here](http://blog.chartbeat.com/2009/08/04/guest-post-how-talking-points-memo-uses-chartbeat/).
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
`gem install stats_combiner`
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
|
13
|
+
Write a short combiner script that tells StatsCombiner your Chartbeat API parameters, how long it should combine for and where to put the flat file. Add filters to manipulate the data it will publish.
|
14
|
+
|
15
|
+
Here's an example:
|
16
|
+
|
17
|
+
require 'rubygems'
|
18
|
+
require 'stats_combiner'
|
19
|
+
|
20
|
+
# initialize the script
|
21
|
+
s = StatsCombiner::Combiner.new({
|
22
|
+
:ttl => 3600, #1 hour from first run
|
23
|
+
:host => 'yourdomain.com',
|
24
|
+
:api_key => 'YOURKEY',
|
25
|
+
:flat_file => '/path/to/staticfile/top_ten.html'
|
26
|
+
})
|
27
|
+
|
28
|
+
# create a filters object and add some filters
|
29
|
+
e = StatsCombiner::Filterer.new
|
30
|
+
e.add :path_regex => /(\/$|\/index.php$)/, :exclude => true
|
31
|
+
|
32
|
+
# run it!
|
33
|
+
s.run({
|
34
|
+
:filters => e.filters,
|
35
|
+
:verbose => true #this option reports combining status and TTL when true
|
36
|
+
})
|
37
|
+
|
38
|
+
Then add this script to your crontab. I recommend running it every 5 minutes. Just be a good API-consumer when setting your cron:
|
39
|
+
|
40
|
+
*/5 * * * * cd /path/to/my_combiner && ruby my_combiner.rb
|
41
|
+
|
42
|
+
### Filters
|
43
|
+
|
44
|
+
http://{prefix}.{host}/{path}{suffix}
|
45
|
+
|
46
|
+
search by..
|
47
|
+
|
48
|
+
:title_regex => regexp # Filter on a title pattern
|
49
|
+
:path_regex=> regexp # Filter on a path pattern
|
50
|
+
|
51
|
+
..to add a:
|
52
|
+
|
53
|
+
:suffix => string or regexp # a path modification
|
54
|
+
:prefix => string # a subdomain
|
55
|
+
:modify_title => bool or regexp # Modify the title inline
|
56
|
+
# (true replaces title match with '')
|
57
|
+
|
58
|
+
..or, to ignore the entry:
|
59
|
+
|
60
|
+
:exclude => bool # Exclude matching URLs from the top ten list
|
61
|
+
|
62
|
+
Some examples from TPM:
|
63
|
+
|
64
|
+
e.add :prefix => 'tpmdc', :title_regex => /\| TPMDC/, :modify_title => true
|
65
|
+
e.add :path_regex => /((\?|&)ref=.*)/, :suffix => ''
|
66
|
+
e.add :path_regex => /(\?(page|img)=(.*)($|&))/, :suffix => '?\2=1'
|
67
|
+
e.add :path_regex => /(\/$|\/index.php$)/, :exclude => true
|
68
|
+
|
69
|
+
### A note on testing
|
70
|
+
|
71
|
+
You may need to `sudo spec` if your user doesn't have write access to the gems directory.
|
72
|
+
|
73
|
+
## Author
|
74
|
+
|
75
|
+
Al Shaw (al@talkingpointsmemo.com)
|
76
|
+
|
77
|
+
## License (MIT)
|
78
|
+
|
79
|
+
Copyright (c) 2010 TPM Media LLC
|
80
|
+
|
81
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
82
|
+
|
83
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
84
|
+
|
85
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "stats_combiner"
|
8
|
+
gem.summary = %Q{StatsCombiner creates most-viewed story widgets from the Chartbeat API}
|
9
|
+
gem.description = %Q{A tool to create most-viewed story widgets from the Chartbeat API.}
|
10
|
+
gem.email = "almshaw@gmail.com"
|
11
|
+
gem.homepage = "http://github.com/tpm/stats_combiner"
|
12
|
+
gem.authors = ["Al Shaw"]
|
13
|
+
gem.add_dependency 'crack'
|
14
|
+
gem.add_dependency 'sequel'
|
15
|
+
gem.add_development_dependency "rspec"
|
16
|
+
gem.add_development_dependency "fakeweb"
|
17
|
+
gem.add_development_dependency "timecop"
|
18
|
+
gem.add_development_dependency "hpricot"
|
19
|
+
|
20
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
21
|
+
end
|
22
|
+
Jeweler::GemcutterTasks.new
|
23
|
+
rescue LoadError
|
24
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
25
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
data/combiner_sample.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'stats_combiner'
|
3
|
+
|
4
|
+
s = StatsCombiner::Combiner.new({
|
5
|
+
:ttl => 3600,
|
6
|
+
:host => 'yourhost.com',
|
7
|
+
:api_key => 'YOURKEY',
|
8
|
+
:flat_file => '/path/to/top_ten.html'
|
9
|
+
})
|
10
|
+
|
11
|
+
e = StatsCombiner::Filterer.new
|
12
|
+
e.add :prefix => 'tpmdc', :title_regex => /\| TPMDC/, :modify_title => true
|
13
|
+
e.add :prefix => 'tpmmuckraker', :title_regex => /\| TPMMuckraker/, :modify_title => true
|
14
|
+
e.add :prefix => 'tpmtv', :title_regex => /\| TPMTV/, :modify_title => true
|
15
|
+
e.add :prefix => 'tpmcafe', :title_regex => /\| TPMCafe/, :modify_title => true
|
16
|
+
e.add :prefix => 'tpmlivewire', :title_regex => /\| TPM LiveWire/, :modify_title => true
|
17
|
+
e.add :prefix => 'tpmpolltracker', :title_regex => /\| TPM PollTracker/, :modify_title => true
|
18
|
+
e.add :prefix => 'www', :title_regex => /\|.*$/, :modify_title => true
|
19
|
+
|
20
|
+
#put excluders last
|
21
|
+
e.add :path_regex => /(\/$|\/index.php$)/, :exclude => true
|
22
|
+
|
23
|
+
s.run({
|
24
|
+
:filters => e.filters,
|
25
|
+
:verbose => true
|
26
|
+
})
|
@@ -0,0 +1,182 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'crack/json'
|
4
|
+
require 'sequel'
|
5
|
+
require 'stats_combiner/filterer'
|
6
|
+
|
7
|
+
module StatsCombiner
|
8
|
+
|
9
|
+
class Combiner
|
10
|
+
|
11
|
+
# Usage:
|
12
|
+
#
|
13
|
+
# s = StatsCombiner::Combiner.new({
|
14
|
+
# :api_key => 'your_key',
|
15
|
+
# :host => 'talkingpointsmemo.com',
|
16
|
+
# :ttl => 3600,
|
17
|
+
# :flat_file => '/var/www/html/topten.html'
|
18
|
+
# })
|
19
|
+
def initialize(opts = {})
|
20
|
+
@init_options = {
|
21
|
+
:ttl => 3600, #one hour by default
|
22
|
+
:story_count => 10,
|
23
|
+
:flat_file => 'topten.html',
|
24
|
+
}.merge!(opts)
|
25
|
+
@db_file = "#{@init_options[:host]}_stats_db.sqlite3"
|
26
|
+
end
|
27
|
+
|
28
|
+
# check where we are in the cycle
|
29
|
+
# and run the necessary functions
|
30
|
+
# call this after initializing StatsCombiner
|
31
|
+
#
|
32
|
+
# Usage:
|
33
|
+
# s.run({
|
34
|
+
# :filters => e.filters (result of a StatsCombiner::Filterer filters hash)
|
35
|
+
# :verbose => true
|
36
|
+
# })
|
37
|
+
def run(opts = {})
|
38
|
+
{ :filters => nil,
|
39
|
+
:verbose => false
|
40
|
+
}.merge!(opts)
|
41
|
+
|
42
|
+
@filters = opts[:filters]
|
43
|
+
|
44
|
+
now = Time.now
|
45
|
+
if File::exists?(@db_file)
|
46
|
+
@db = Sequel.sqlite(@db_file)
|
47
|
+
@table_create_time = self.table_create_time
|
48
|
+
@table_destroy_time = @table_create_time + @init_options[:ttl]
|
49
|
+
@ttl = @table_destroy_time.to_i - Time.now.to_i
|
50
|
+
|
51
|
+
if now.to_i < @table_destroy_time.to_i
|
52
|
+
self.combine
|
53
|
+
if opts[:verbose]
|
54
|
+
puts "Combining. DB has #{@ttl} seconds to live"
|
55
|
+
end
|
56
|
+
else
|
57
|
+
self.report_and_cleanup
|
58
|
+
if opts[:verbose]
|
59
|
+
puts "ttl expired. reporting and cleaning up"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
else
|
63
|
+
self.setup
|
64
|
+
if opts[:verbose]
|
65
|
+
puts "No DB detected. I set one up"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
protected
|
72
|
+
|
73
|
+
# Set up the database.
|
74
|
+
# This is done once every timeout cycle
|
75
|
+
def setup
|
76
|
+
@db = Sequel.sqlite(@db_file)
|
77
|
+
@db.create_table :stories do
|
78
|
+
primary_key :id, :type => Integer
|
79
|
+
String :title, :text => true
|
80
|
+
String :path, :text => true
|
81
|
+
Fixnum :visitors
|
82
|
+
DateTime :created_at
|
83
|
+
end
|
84
|
+
|
85
|
+
@db.create_table :create_time do
|
86
|
+
primary_key :id, :type => Integer
|
87
|
+
DateTime :timestamp
|
88
|
+
end
|
89
|
+
|
90
|
+
now = Time.now
|
91
|
+
time_table = @db[:create_time]
|
92
|
+
time_table.insert(:timestamp => now)
|
93
|
+
end
|
94
|
+
|
95
|
+
def table_create_time
|
96
|
+
@db[:create_time].select(:timestamp).first[:timestamp]
|
97
|
+
end
|
98
|
+
|
99
|
+
def destroy
|
100
|
+
FileUtils.rm_rf(@db_file)
|
101
|
+
end
|
102
|
+
|
103
|
+
# grab the data, and parse it into something we can use
|
104
|
+
# and combine it
|
105
|
+
def combine
|
106
|
+
host = @init_options[:host]
|
107
|
+
api_key = @init_options[:api_key]
|
108
|
+
@url = "http://api.chartbeat.com/toppages/?host=#{host}&limit=50&apikey=#{api_key}"
|
109
|
+
|
110
|
+
@data = open(@url).read
|
111
|
+
@data = Crack::JSON.parse(@data)
|
112
|
+
|
113
|
+
@data.each do |datum|
|
114
|
+
visitors = datum['visitors']
|
115
|
+
path = datum['path']
|
116
|
+
title = datum['i']
|
117
|
+
|
118
|
+
# if the story is already in the db, combine visitor count
|
119
|
+
# otherwise insert a new row
|
120
|
+
existing_story = @db[:stories].where(:title => title).first || ''
|
121
|
+
|
122
|
+
if not existing_story.empty?
|
123
|
+
existing_visitors = existing_story[:visitors]
|
124
|
+
@db[:stories].where(:title => title).update :visitors => existing_visitors + visitors
|
125
|
+
else
|
126
|
+
@db[:stories].insert({
|
127
|
+
:title => title,
|
128
|
+
:visitors => visitors,
|
129
|
+
:path => path
|
130
|
+
})
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Pull data out of the db, apply filters and write the flat file and dump the db.
|
137
|
+
# This is done once every timeout cycle.
|
138
|
+
def report_and_cleanup
|
139
|
+
stories = @db[:stories].order(:visitors.desc).all
|
140
|
+
|
141
|
+
#filter the array if applicable, then narrow down to top ten
|
142
|
+
if @filters
|
143
|
+
stories.each do |story|
|
144
|
+
StatsCombiner::Filterer.apply_filters!(@filters,story)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
#sweep away the excludes
|
149
|
+
stories.reject! {|story| story if story[:title].nil? || story[:path].nil? }
|
150
|
+
|
151
|
+
top_ten = stories[0..9]
|
152
|
+
now = Time.now
|
153
|
+
flat_file = @init_options[:flat_file]
|
154
|
+
host = @init_options[:host]
|
155
|
+
|
156
|
+
#write it out
|
157
|
+
html = '<ol>'
|
158
|
+
|
159
|
+
top_ten.each do |story|
|
160
|
+
title = story[:title]
|
161
|
+
path = story[:path]
|
162
|
+
visitors = story[:visitors]
|
163
|
+
|
164
|
+
if not story[:prefix].nil?
|
165
|
+
prefix = story[:prefix] + '.'
|
166
|
+
end
|
167
|
+
|
168
|
+
html << "<li><a href=\"http://#{prefix}#{host}#{path}\">#{title}</a></li> <!-- #{visitors} -->"
|
169
|
+
end
|
170
|
+
|
171
|
+
html << '</ol>'
|
172
|
+
html << "<!-- This report was generated at #{now} -->"
|
173
|
+
|
174
|
+
flat_file = File.new(flat_file, "w+")
|
175
|
+
flat_file.write(html)
|
176
|
+
flat_file.close
|
177
|
+
|
178
|
+
#Destroy the DB and start the journey over again.
|
179
|
+
self.destroy
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module StatsCombiner
|
2
|
+
|
3
|
+
class Filterer
|
4
|
+
|
5
|
+
attr_accessor :filters
|
6
|
+
|
7
|
+
# Initialize a filters object:
|
8
|
+
# e = StatsCombiner::Filterer.new
|
9
|
+
#
|
10
|
+
def initialize()
|
11
|
+
@filters ||= []
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
# Add a filter that StatsCombiner can use to manipulate paths and titles it
|
16
|
+
# gets from Chartbeat.
|
17
|
+
#
|
18
|
+
# Options: Pattern: <tt>http://{prefix}.{host}/{path}{suffix}</tt>
|
19
|
+
#
|
20
|
+
# search by..
|
21
|
+
# title_regex => nil Filter on a title pattern
|
22
|
+
# path_regex=> nil Filter on a path pattern
|
23
|
+
#
|
24
|
+
# ..to add a:
|
25
|
+
# suffix => nil a path modification
|
26
|
+
# prefix => nil a subdomain
|
27
|
+
# modify_title => bool or regexp Modify the title inline
|
28
|
+
#
|
29
|
+
# Or, to ignore the entry:
|
30
|
+
# exclude => true Exclude this pattern from the top ten list
|
31
|
+
#
|
32
|
+
# Some examples from TPM:
|
33
|
+
# e.add :prefix => 'tpmdc', :title_regex => /\| TPMDC/, :modify_title => true
|
34
|
+
# e.add :path_regex => /(\?ref=.*$|\&ref=.*$|)/, :suffix => '', :modify_path => true
|
35
|
+
# e.add :path_regex => /(\?(page|img)=(.*)($|&))/, :suffix => '?\2=1'
|
36
|
+
# e.add :path_regex => /(\/$|\/index.php$)/, :exclude => true
|
37
|
+
def add(options={})
|
38
|
+
{ :prefix => nil,
|
39
|
+
:suffix => nil,
|
40
|
+
:title_regex => nil,
|
41
|
+
:path_regex => nil,
|
42
|
+
:modify_title => false,
|
43
|
+
:exclude => false,
|
44
|
+
}.merge!(options)
|
45
|
+
|
46
|
+
filter = {}
|
47
|
+
filter[:rule] = {}.merge!(options)
|
48
|
+
|
49
|
+
@filters << filter
|
50
|
+
end
|
51
|
+
|
52
|
+
# sanity check
|
53
|
+
def list_filters
|
54
|
+
@filters.each do |filter|
|
55
|
+
p filter[:rule]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# a datum comes in from chartbeat data, and is manipulated
|
60
|
+
# with the apply_filters method, and sent back to Combiner to write out
|
61
|
+
#
|
62
|
+
# Grab filters with <tt>e.filters</tt>
|
63
|
+
def self.apply_filters!(filters,datum={})
|
64
|
+
{ :title => nil,
|
65
|
+
:path => nil,
|
66
|
+
:prefix => nil
|
67
|
+
}.merge!(datum)
|
68
|
+
|
69
|
+
filters.each do |filter|
|
70
|
+
|
71
|
+
# set prefixes where they match title regexes
|
72
|
+
# /\| TPMDC/ => http://tpmdc
|
73
|
+
if (filter[:rule][:prefix] && filter[:rule][:title_regex]) && datum[:title].match(filter[:rule][:title_regex])
|
74
|
+
datum[:prefix] = filter[:rule][:prefix]
|
75
|
+
end
|
76
|
+
|
77
|
+
# modify path => '?q=new_suffix'
|
78
|
+
# append to path with regex replacement variables => '\1&new_suffix'
|
79
|
+
if (filter[:rule][:suffix] && filter[:rule][:path_regex]) && datum[:path].match(filter[:rule][:path_regex])
|
80
|
+
datum[:path].gsub!(filter[:rule][:path_regex],filter[:rule][:suffix])
|
81
|
+
end
|
82
|
+
|
83
|
+
# apply title mods
|
84
|
+
# modify_title => true /==>
|
85
|
+
# modify_title => "DC Central"
|
86
|
+
# title_regex => /| TPMDC/, modify_title => '\1 Central' ==> TPMDC Central
|
87
|
+
if filter[:rule][:modify_title]
|
88
|
+
filter[:rule][:modify_title] = '' unless filter[:rule][:modify_title].is_a?(String)
|
89
|
+
datum[:title].gsub!(filter[:rule][:title_regex], filter[:rule][:modify_title])
|
90
|
+
datum[:title].strip!
|
91
|
+
end
|
92
|
+
|
93
|
+
# apply excludes.
|
94
|
+
# this should take out the whole record if it matches a path or title regex
|
95
|
+
if filter[:rule][:exclude] && ((filter[:rule][:path_regex].is_a?(Regexp) && datum[:path].match(filter[:rule][:path_regex])) || (filter[:rule][:title_regex].is_a?(Regexp) && datum[:title].match(filter[:rule][:title_regex])))
|
96
|
+
# nil out datum.
|
97
|
+
# StatsCombiner::Combiner will sweep away the nils later
|
98
|
+
datum[:title] = datum[:path] = datum[:prefix] = nil
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
datum
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
@@ -0,0 +1,294 @@
|
|
1
|
+
require '../lib/stats_combiner'
|
2
|
+
require 'test_data'
|
3
|
+
|
4
|
+
require 'spec'
|
5
|
+
require 'timecop'
|
6
|
+
require 'hpricot'
|
7
|
+
|
8
|
+
HOST = 'fake.com'
|
9
|
+
KEY = 'fake_key'
|
10
|
+
|
11
|
+
describe "an unfiltered StatsCombiner cycle" do
|
12
|
+
|
13
|
+
before :each do
|
14
|
+
@flat_file = File.dirname(__FILE__) + '/test_flat_file.html'
|
15
|
+
@ttl = 3600
|
16
|
+
@s = StatsCombiner::Combiner.new({
|
17
|
+
:ttl => @ttl,
|
18
|
+
:host=> HOST,
|
19
|
+
:api_key=> KEY,
|
20
|
+
:flat_file => @flat_file,
|
21
|
+
})
|
22
|
+
@db_file = "#{HOST}_stats_db.sqlite3"
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should do a first-time run, setting up the db' do
|
26
|
+
@s.run()
|
27
|
+
|
28
|
+
|
29
|
+
File.exist?(@db_file).should == true
|
30
|
+
|
31
|
+
@db = Sequel.sqlite(@db_file)
|
32
|
+
@db[:stories].all.should be_a(Array)
|
33
|
+
@db[:create_time].all.should be_a(Array)
|
34
|
+
|
35
|
+
#allow for a 2 second variation in timestamp
|
36
|
+
@db[:create_time].select(:timestamp).first[:timestamp].to_i.should be_close((Time.now.to_i - 2),(Time.now.to_i + 2))
|
37
|
+
|
38
|
+
@first_run_time = Time.now
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'should do a second-time run, capturing data' do
|
42
|
+
#set Time.now to 5 seconds from now
|
43
|
+
t = Time.now
|
44
|
+
Timecop.travel(t + 5)
|
45
|
+
|
46
|
+
@s.run()
|
47
|
+
|
48
|
+
File.exist?(@db_file).should == true
|
49
|
+
|
50
|
+
@db = Sequel.sqlite(@db_file)
|
51
|
+
@db[:stories].all.length.should be >= 1
|
52
|
+
first_story = @db[:stories].first
|
53
|
+
first_story[:visitors].should be_a(Fixnum)
|
54
|
+
first_story[:title].should be_a(String)
|
55
|
+
first_story[:path].should be_a(String)
|
56
|
+
|
57
|
+
#save first_story title & first_story visitors for use in the combiner test
|
58
|
+
FIRST_STORY_TITLE = first_story[:title]
|
59
|
+
FIRST_STORY_VISITORS = first_story[:visitors]
|
60
|
+
Timecop.return
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'should do a combining data capture' do
|
64
|
+
@s.run()
|
65
|
+
|
66
|
+
File.exist?(@db_file).should == true
|
67
|
+
@db = Sequel.sqlite(@db_file)
|
68
|
+
|
69
|
+
# let's check to see that the titles array is unique
|
70
|
+
# i.e., that dupes have been added together
|
71
|
+
test_titles = []
|
72
|
+
stories = @db[:stories].all
|
73
|
+
stories.each do |story|
|
74
|
+
test_titles << story[:title]
|
75
|
+
end
|
76
|
+
test_titles.uniq.size.should eql(stories.size)
|
77
|
+
|
78
|
+
# now let's check that visit counts have been combined
|
79
|
+
# for this, we'll *assume* that the first story in the array
|
80
|
+
# (which has the higest visitor ct) will be combined.
|
81
|
+
# We'll use @first_story_title we got in the second-time-run test
|
82
|
+
@combined_story = @db[:stories].where(:title => FIRST_STORY_TITLE).first
|
83
|
+
@combined_story[:title].should be_a(String)
|
84
|
+
@combined_story[:visitors].should > FIRST_STORY_VISITORS
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'should report data and dump db' do
|
88
|
+
# set Time.now to 5 seconds past ttl
|
89
|
+
t = Time.now
|
90
|
+
Timecop.travel(t + @ttl + 5)
|
91
|
+
|
92
|
+
@s.run()
|
93
|
+
|
94
|
+
File.exist?(@flat_file).should == true
|
95
|
+
File.exist?(@db_file).should == false
|
96
|
+
|
97
|
+
Timecop.return
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
after :all do
|
102
|
+
FileUtils.rm_rf(@db_file)
|
103
|
+
FileUtils.rm_rf(@flat_file)
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
describe "basic Filterer filtering" do
|
109
|
+
|
110
|
+
before :each do
|
111
|
+
@f = StatsCombiner::Filterer.new
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'should add a rule to the filters array' do
|
115
|
+
@f.add :prefix => 'tpmdc', :title_regex => /\| TPMDC/, :modify_title => true
|
116
|
+
|
117
|
+
@f.filters.size.should eql(1)
|
118
|
+
end
|
119
|
+
|
120
|
+
# various filter cases
|
121
|
+
it 'should set a prefix according to a title_regex' do
|
122
|
+
@f.add :prefix => 'tpmdc', :title_regex => /\| TPMDC/
|
123
|
+
datum = {:visitors=>366, :created_at=>nil, :path=>"/2010/05/with-specter-suffering-white-house-and-gop-looking-at-surging-sestak.php", :id=>3, :title=>"With Specter Suffering, White House And GOP Looking At Surging Sestak | TPMDC"}
|
124
|
+
|
125
|
+
result = StatsCombiner::Filterer.apply_filters!(@f.filters,datum)
|
126
|
+
result[:prefix].should eql("tpmdc")
|
127
|
+
end
|
128
|
+
|
129
|
+
it 'should modify a title based on a title_regex and a modify_title boolean' do
|
130
|
+
@f.add :prefix => 'tpmdc', :title_regex => /\| TPMDC/, :modify_title => true
|
131
|
+
|
132
|
+
datum = {:visitors=>366, :created_at=>nil, :path=>"/2010/05/with-specter-suffering-white-house-and-gop-looking-at-surging-sestak.php", :id=>3, :title=>"With Specter Suffering, White House And GOP Looking At Surging Sestak | TPMDC"}
|
133
|
+
|
134
|
+
result = StatsCombiner::Filterer.apply_filters!(@f.filters,datum)
|
135
|
+
result[:title].should eql("With Specter Suffering, White House And GOP Looking At Surging Sestak")
|
136
|
+
end
|
137
|
+
|
138
|
+
it 'should modify a title based on a title_regex and a modify_title regex' do
|
139
|
+
@f.add :prefix => 'tpmdc', :title_regex => /(\| TPMDC)$/, :modify_title => '\1 Central'
|
140
|
+
|
141
|
+
datum = {:visitors=>366, :created_at=>nil, :path=>"/2010/05/with-specter-suffering-white-house-and-gop-looking-at-surging-sestak.php", :id=>3, :title=>"With Specter Suffering, White House And GOP Looking At Surging Sestak | TPMDC"}
|
142
|
+
|
143
|
+
result = StatsCombiner::Filterer.apply_filters!(@f.filters,datum)
|
144
|
+
result[:title].should eql("With Specter Suffering, White House And GOP Looking At Surging Sestak | TPMDC Central")
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'should set a suffix where it matches a path_regex and modify a path according to a suffix regex' do
|
148
|
+
@f.add :path_regex => /(\?ref=.*)$/, :suffix => '\1&foo=bar', :modify_path => true
|
149
|
+
|
150
|
+
datum = {:visitors=>366, :created_at=>nil, :path=>"/2010/05/with-specter-suffering-white-house-and-gop-looking-at-surging-sestak.php?ref=fpa", :id=>3, :title=>"With Specter Suffering, White House And GOP Looking At Surging Sestak | TPMDC"}
|
151
|
+
|
152
|
+
result = StatsCombiner::Filterer.apply_filters!(@f.filters,datum)
|
153
|
+
result[:path].should eql("/2010/05/with-specter-suffering-white-house-and-gop-looking-at-surging-sestak.php?ref=fpa&foo=bar")
|
154
|
+
|
155
|
+
@f.filters.clear
|
156
|
+
|
157
|
+
#another example to kill unwanted query strings
|
158
|
+
@f.add :path_regex => /((\?|&)ref=.*)/, :suffix => '', :modify_path => true
|
159
|
+
|
160
|
+
datum = {:visitors=>366, :created_at=>nil, :path=>"/2010/05/with-specter-suffering-white-house-and-gop-looking-at-surging-sestak.php?id=keepme&ref=killme", :id=>3, :title=>"With Specter Suffering, White House And GOP Looking At Surging Sestak | TPMDC"}
|
161
|
+
|
162
|
+
result = StatsCombiner::Filterer.apply_filters!(@f.filters,datum)
|
163
|
+
result[:path].should eql("/2010/05/with-specter-suffering-white-house-and-gop-looking-at-surging-sestak.php?id=keepme")
|
164
|
+
end
|
165
|
+
|
166
|
+
|
167
|
+
it 'should nil out data where exclude is true and path or title regexes are matched' do
|
168
|
+
#first, two examples of a matching regex
|
169
|
+
|
170
|
+
@f.add :path_regex => /(\/$|\/index.php$)/, :exclude => true
|
171
|
+
|
172
|
+
datum = {:visitors=>3090, :created_at=>nil, :path=>"/", :id=>1, :title=>"Talking Points Memo | Breaking News and Analysis"}
|
173
|
+
|
174
|
+
result = StatsCombiner::Filterer.apply_filters!(@f.filters,datum)
|
175
|
+
result[:title].should be_nil
|
176
|
+
result[:path].should be_nil
|
177
|
+
|
178
|
+
@f.filters.clear
|
179
|
+
|
180
|
+
@f.add :path_regex => /talk\/blogs/, :exclude => true
|
181
|
+
|
182
|
+
datum = {:visitors=>6, :created_at=>nil, :path=>"/talk/blogs/a/m/americandad/2010/03/an-open-letter-to-conservative.php/", :id=>31, :title=>"An open letter to conservatives | AmericanDad's Blog"}
|
183
|
+
|
184
|
+
result = StatsCombiner::Filterer.apply_filters!(@f.filters,datum)
|
185
|
+
result[:title].should be_nil
|
186
|
+
result[:path].should be_nil
|
187
|
+
|
188
|
+
@f.filters.clear
|
189
|
+
|
190
|
+
#now, let's look for invalid data. run a regex against a nonmatch
|
191
|
+
@f.add :path_regex => /(\/$|\/index.php$)/, :exclude => true
|
192
|
+
|
193
|
+
datum = {:visitors=>6, :created_at=>nil, :path=>"/talk/blogs/a/m/americandad/2010/03/an-open-letter-to-conservative.php", :id=>31, :title=>"An open letter to conservatives | AmericanDad's Blog"}
|
194
|
+
|
195
|
+
result = StatsCombiner::Filterer.apply_filters!(@f.filters,datum)
|
196
|
+
result[:title].should_not be_nil
|
197
|
+
result[:path].should_not be_nil
|
198
|
+
|
199
|
+
@f.filters.clear
|
200
|
+
|
201
|
+
#try a title match
|
202
|
+
@f.add :title_regex => /(Breaking News and Analysis)/, :exclude => true
|
203
|
+
|
204
|
+
datum = {:visitors=>3090, :created_at=>nil, :path=>"/", :id=>1, :title=>"Talking Points Memo | Breaking News and Analysis"}
|
205
|
+
|
206
|
+
result = StatsCombiner::Filterer.apply_filters!(@f.filters,datum)
|
207
|
+
result[:title].should be_nil
|
208
|
+
result[:path].should be_nil
|
209
|
+
|
210
|
+
end
|
211
|
+
|
212
|
+
end
|
213
|
+
|
214
|
+
# best way to do this (without prying open the Class) might be
|
215
|
+
# to define a set of filters, timetravel our way to the flat file stage,
|
216
|
+
# open it and parse the HTML with hpricot for rules.
|
217
|
+
# Caveats - filters will be pretty specific
|
218
|
+
# to whatever account is running this suite. So, proceed with caution.
|
219
|
+
describe "filtered StatsCombining" do
|
220
|
+
|
221
|
+
before :each do
|
222
|
+
@flat_file = File.dirname(__FILE__) + '/test_flat_file.html'
|
223
|
+
@ttl = 3600
|
224
|
+
@s = StatsCombiner::Combiner.new({
|
225
|
+
:ttl => @ttl,
|
226
|
+
:host=> HOST,
|
227
|
+
:api_key=> KEY,
|
228
|
+
:flat_file => @flat_file,
|
229
|
+
})
|
230
|
+
@db_file = "#{HOST}_stats_db.sqlite3"
|
231
|
+
end
|
232
|
+
|
233
|
+
it 'should run its way through the cycle and publish out a top ten list according to filter rules' do
|
234
|
+
|
235
|
+
# first, let's set the filters we want to apply
|
236
|
+
e = StatsCombiner::Filterer.new
|
237
|
+
e.add :prefix => 'tpmdc', :title_regex => /\| TPMDC/, :modify_title => true
|
238
|
+
e.add :prefix => 'tpmmuckraker', :title_regex => /\| TPMMuckraker/, :modify_title => true
|
239
|
+
e.add :prefix => 'tpmtv', :title_regex => /\| TPMTV/, :modify_title => true
|
240
|
+
e.add :prefix => 'tpmcafe', :title_regex => /\| TPMCafe/, :modify_title => true
|
241
|
+
e.add :prefix => 'tpmlivewire', :title_regex => /\| TPM LiveWire/, :modify_title => true
|
242
|
+
e.add :prefix => 'polltracker', :title_regex => /\| TPM PollTracker/, :modify_title => true
|
243
|
+
e.add :prefix => 'www', :title_regex => /\|.*$/, :modify_title => true
|
244
|
+
e.add :path_regex => /(\/$|\/index.php$)/, :exclude => true
|
245
|
+
|
246
|
+
# now, let's go through the rigamarole to get this thing pubbed.
|
247
|
+
# run to setup db
|
248
|
+
@s.run :filters => e.filters
|
249
|
+
# run again to start publishing
|
250
|
+
@s.run :filters => e.filters
|
251
|
+
# timetravel to pub time and do it.
|
252
|
+
# * set Time.now to 5 seconds past ttl
|
253
|
+
t = Time.now
|
254
|
+
Timecop.travel(t + @ttl + 5)
|
255
|
+
|
256
|
+
# add filters
|
257
|
+
@s.run :filters => e.filters
|
258
|
+
|
259
|
+
# sanity check
|
260
|
+
File.exist?(@flat_file).should == true
|
261
|
+
|
262
|
+
# open the file we just made
|
263
|
+
list = File.open(@flat_file).read
|
264
|
+
list = Hpricot(list)
|
265
|
+
|
266
|
+
# collect urls and titles
|
267
|
+
urls = []
|
268
|
+
titles = []
|
269
|
+
list.search("a").each do |a|
|
270
|
+
urls << a.attributes['href']
|
271
|
+
titles << a.inner_html
|
272
|
+
end
|
273
|
+
|
274
|
+
# let's make sure we have 10 stories
|
275
|
+
urls.size.should eql(10)
|
276
|
+
|
277
|
+
# pull prefixes from rules array
|
278
|
+
prefixes = e.filters.collect { |filter| filter[:rule][:prefix] }
|
279
|
+
|
280
|
+
# test prefixes against subdomains
|
281
|
+
urls.each do |url|
|
282
|
+
subdomain = URI.parse(url).host.split('.')[0]
|
283
|
+
prefixes.include?(subdomain).should == true
|
284
|
+
end
|
285
|
+
|
286
|
+
Timecop.return
|
287
|
+
end
|
288
|
+
|
289
|
+
after :all do
|
290
|
+
FileUtils.rm_rf(@db_file)
|
291
|
+
FileUtils.rm_rf(@flat_file)
|
292
|
+
end
|
293
|
+
|
294
|
+
end
|
data/spec/test_data.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'fakeweb'
|
2
|
+
|
3
|
+
FakeWeb.allow_net_connect = false
|
4
|
+
|
5
|
+
TEST_DATA = <<-DOCUMENT
|
6
|
+
[{"i": "Talking Points Memo | Breaking News and Analysis", "path": "\/", "visitors": 529}, {"i": "Theories of the Fall | Talking Points Memo", "path": "\/archives\/2010\/05\/theories_of_the_fall.php", "visitors": 66}, {"i": "What A Week: Rand Paul Takes The National Stage | TPMDC", "path": "\/2010\/05\/what-a-week-rand-paul-takes-the-national-stage.php?ref=fpa", "visitors": 37}, {"i": "Texas Board Of Ed Approves Right-Wing History Textbook Standards (VIDEO) | TPMMuckraker", "path": "\/2010\/05\/texas_history_textbooks_final_vote.php", "visitors": 15}, {"i": "Ron Paul Appeared On Meet The Press In '07 And Spoke Out Against Civil Rights Act (VIDEO) | TPMDC", "path": "\/2010\/05\/ron-paul-appeared-on-meet-the-press-in-07-and-spoke-out-against-civil-rights-act-video.php?ref=fpb", "visitors": 12}, {"i": "Texas Board Of Ed Approves Right-Wing History Textbook Standards (VIDEO) | TPMMuckraker", "path": "\/2010\/05\/texas_history_textbooks_final_vote.php?ref=fpb", "visitors": 11}, {"i": "CNN Headline: 'Miss USA: Muslim Trailblazer Or Hezbollah Spy?' | TPM LiveWire", "path": "\/2010\/05\/cnn-headline-miss-usa-muslim-trailblazer-or-hezbollah-spy.php?ref=fpb", "visitors": 8}, {"i": "GOP Kills Science Jobs Bill By Forcing Dems To Vote For Porn | TPMDC", "path": "\/2010\/05\/gop-kills-science-jobs-bill-by-forcing-dems-to-vote-for-porn.php", "visitors": 7}, {"i": "Texas Board Of Ed Approves Right-Wing History Textbook Standards (VIDEO) | TPMMuckraker", "path": "\/2010\/05\/texas_history_textbooks_final_vote.php?ref=fpa", "visitors": 7}, {"i": "Will Wall Street Reform Negotiators Publicly Weaken Derivative-Trading Regulations? (VIDEO) | TPMDC", "path": "\/2010\/05\/will-wall-street-reform-negotiators-publicly-weaken-derivative-trading-regulations-video.php?ref=fpb", "visitors": 6}, {"i": "What A Week: Rand Paul Takes The National Stage | TPMDC", "path": "\/2010\/05\/what-a-week-rand-paul-takes-the-national-stage.php?ref=tn", "visitors": 6}, {"i": "Unity In Kentucky As GOPers Rally 'Round Rand | TPMDC", "path": "\/2010\/05\/this-is-what-kentucky-unity-looks-like-gopers-rally-round-rand.php?ref=fpb", "visitors": 6}, {"i": "So Not Ready For Prime Time (or Even Sunday) | Talking Points Memo", "path": "\/archives\/2010\/05\/so_not_ready_for_prime_time_or_even_sunday.php", "visitors": 6}, {"i": "Paul Backs Out Of Meet The Press Appearance | TPMDC", "path": "\/2010\/05\/paul-backs-out-of-meet-the-press-appearance.php?ref=fpb", "visitors": 6}, {"i": "Jack Conway To TPMDC: Paul Civil Rights Comments 'Relevant' To General Election Campaign | TPMDC", "path": "\/2010\/05\/jack-conway-to-tpmdc-paul-civil-rights-comments-relevant-to-general-election-campaign.php?ref=fpb", "visitors": 5}, {"i": "TPMDC Saturday Roundup | TPMDC", "path": "\/2010\/05\/obama-announces-commission-on-oil-spill.php", "visitors": 5}, {"i": "What A Week: Rand Paul Takes The National Stage | TPMDC", "path": "\/2010\/05\/what-a-week-rand-paul-takes-the-national-stage.php", "visitors": 5}, {"i": "Dems Brace For Loss Of Usually Ultra-Safe Hawaii House Seat On Saturday | TPMDC", "path": "\/2010\/05\/dems-brace-for-loss-of-usually-ultra-safe-hawaii-house-seat-on-saturday.php", "visitors": 4}, {"i": "That Other Thing: Blumenthal And The Swim Team | TPMMuckraker", "path": "\/2010\/05\/that_other_thing_blumenthal_and_the_swim_team.php?ref=fpc", "visitors": 4}, {"i": "Franken Raises Money To Oppose Rand Paul -- And He Likes Brunch | TPMDC", "path": "\/2010\/05\/franken-raises-money-to-oppose-rand-paul----and-he-likes-brunch.php?ref=fpi", "visitors": 4}, {"i": "What Did Rand Paul Really Say On Maddow Last Night? | TPMDC", "path": "\/2010\/05\/what-did-rand-paul-really-say-on-maddow-last-night.php", "visitors": 4}, {"i": "Like a Friggin' Rock | Talking Points Memo", "path": "\/archives\/2010\/05\/like_a_friggin_rock.php", "visitors": 4}, {"i": "Franken Raises Money To Oppose Rand Paul -- And He Likes Brunch | TPMDC", "path": "\/2010\/05\/franken-raises-money-to-oppose-rand-paul----and-he-likes-brunch.php", "visitors": 4}, {"i": "Unity In Kentucky As GOPers Rally 'Round Rand | TPMDC", "path": "\/2010\/05\/this-is-what-kentucky-unity-looks-like-gopers-rally-round-rand.php?ref=fpa", "visitors": 4}, {"i": "Rand Paul: Economic Collapse Could Lead To 'A Hitler' Coming To Power | TPMMuckraker", "path": "\/2010\/05\/rand_paul_economic_collapse_could_lead_to_a_hitler.php", "visitors": 4}, {"i": "Rand Paul: Obama's BP Comments Sound 'Really Un-American' (VIDEO) | TPM LiveWire", "path": "\/2010\/05\/rand-paul-obamas-bp-comments-sound-really-un-american-video.php?ref=fpblg", "visitors": 4}, {"i": "CNN Headline: 'Miss USA: Muslim Trailblazer Or Hezbollah Spy?' | TPM LiveWire", "path": "\/2010\/05\/cnn-headline-miss-usa-muslim-trailblazer-or-hezbollah-spy.php", "visitors": 3}, {"i": "A Red Tulip Confirms All Crows Are Black | Fred Moolten's Blog", "path": "\/talk\/blogs\/fredmoolten\/2009\/06\/a-red-tulip-confirms-all-crows.php", "visitors": 3}, {"i": "Peter Beinart Unbound? | TPMCafe", "path": "\/2010\/05\/19\/beinart_unbound_here_and_in_bookforumcom\/", "visitors": 3}, {"i": "Bill Clinton To Campaign For Blanche Lincoln In Arkansas | TPMDC", "path": "\/2010\/05\/bill-clinton-to-campaign-for-blanche-lincoln-in-arkansas.php?ref=fpb", "visitors": 3}, {"i": "Texas Board Of Ed Approves Right-Wing History Textbook Standards (VIDEO) | TPMMuckraker", "path": "\/2010\/05\/texas_history_textbooks_final_vote.php?ref=tn", "visitors": 3}, {"i": "What A Week: Rand Paul Takes The National Stage | TPMDC", "path": "\/2010\/05\/what-a-week-rand-paul-takes-the-national-stage.php?ref=dcblt", "visitors": 3}, {"i": "Bwakfat's Dashboard | All", "path": "\/cgi-bin\/mt-current\/mt-cp.cgi?__mode=my_dashboard", "visitors": 3}, {"i": "Paul Has Been Guest Of Conspiracy Theorist Shock Jock Alex Jones (VIDEO) | TPMMuckraker", "path": "\/2010\/05\/paul_has_been_guest_of_conspiracy_theorist_shock_j.php", "visitors": 3}, {"i": "Full Text Of Newsmax Column Suggesting Military Coup Against Obama | TPM News Pages", "path": "\/news\/2009\/09\/full_text_of_newsmax_column_suggesting_military_co.php\/", "visitors": 3}, {"i": "Ron Paul Appeared On Meet The Press In '07 And Spoke Out Against Civil Rights Act (VIDEO) | TPMDC", "path": "\/2010\/05\/ron-paul-appeared-on-meet-the-press-in-07-and-spoke-out-against-civil-rights-act-video.php", "visitors": 3}, {"i": "Rand Paul In '08: Beware The NAFTA Superhighway! (VIDEO) | TPMMuckraker", "path": "\/2010\/05\/rand_paul_beware_the_nafta_superhighway_video.php", "visitors": 3}, {"i": "Jack Conway To TPMDC: Paul Civil Rights Comments 'Relevant' To General Election Campaign | TPMDC", "path": "\/2010\/05\/jack-conway-to-tpmdc-paul-civil-rights-comments-relevant-to-general-election-campaign.php?ref=tn", "visitors": 3}, {"i": "Ahistorical | Talking Points Memo", "path": "\/archives\/2010\/05\/ahistorical.php", "visitors": 3}, {"i": "Harvard Needs To Fire Dershowitz | TPMCafe", "path": "\/2010\/05\/22\/harvard_needs_to_fire_dershowitz\/", "visitors": 3}, {"i": "Scientists see video, adjust Gulf leak estimates | TPM News Pages", "path": "\/news\/2010\/05\/scientists_see_video_adjust_gulf_leak_estimates.php?ref=fpa", "visitors": 3}, {"i": "Rand Paul Defends Criticism Of Civil Rights Act To Rachel Maddow (VIDEO) | TPM LiveWire", "path": "\/2010\/05\/rand-paul-defends-criticism-of-civil-rights-act-to-rachel-maddow.php?ref=fpa", "visitors": 3}, {"i": "Dems Brace For Loss Of Usually Ultra-Safe Hawaii House Seat On Saturday | TPMDC", "path": "\/2010\/05\/dems-brace-for-loss-of-usually-ultra-safe-hawaii-house-seat-on-saturday.php?ref=fpb", "visitors": 3}, {"i": "LisB Blows A Gasket | LisB's Blog", "path": "\/talk\/blogs\/l\/i\/lisb\/2010\/05\/lisb-blows-a-gasket.php?ref=reccafe", "visitors": 2}, {"i": "Rand Paul In '08: Beware The NAFTA Superhighway! (VIDEO) | TPMMuckraker", "path": "\/2010\/05\/rand_paul_beware_the_nafta_superhighway_video.php?ref=fpblg", "visitors": 2}, {"i": "Texas Textbook Hearings | TPMMuckraker", "path": "\/texas_textbook_hearings\/", "visitors": 2}, {"i": "California Gov. Candidate Pushes Plan For 'Pedophile Island' | TPM LiveWire", "path": "\/2010\/05\/california-gov-candidate-pushes-plan-for-pedophile-island.php?ref=fpblg", "visitors": 2}, {"i": "Jack Conway Strikes Back At Paul's 'Un-American' Comment | TPM LiveWire", "path": "\/2010\/05\/jack-conway-strikes-back-at-pauls-un-american-comment.php?ref=fpa", "visitors": 2}, {"i": "Theories of the Fall | Talking Points Memo", "path": "\/archives\/2010\/05\/theories_of_the_fall.php?ref=fpblg", "visitors": 2}, {"i": "Jack Conway Strikes Back At Paul's 'Un-American' Comment | TPM LiveWire", "path": "\/2010\/05\/jack-conway-strikes-back-at-pauls-un-american-comment.php", "visitors": 2}]
|
7
|
+
DOCUMENT
|
8
|
+
|
9
|
+
FakeWeb.register_uri(:get, "http://api.chartbeat.com/toppages/?host=fake.com&limit=50&apikey=fake_key", :body => TEST_DATA)
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{stats_combiner}
|
8
|
+
s.version = "0.0.1"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Al Shaw"]
|
12
|
+
s.date = %q{2010-05-24}
|
13
|
+
s.description = %q{A tool to create most-viewed story widgets from the Chartbeat API.}
|
14
|
+
s.email = %q{almshaw@gmail.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"README.md"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
".gitignore",
|
20
|
+
"README.md",
|
21
|
+
"Rakefile",
|
22
|
+
"VERSION",
|
23
|
+
"combiner_sample.rb",
|
24
|
+
"lib/stats_combiner.rb",
|
25
|
+
"lib/stats_combiner/filterer.rb",
|
26
|
+
"spec/stats_combiner_spec.rb",
|
27
|
+
"spec/test_data.rb",
|
28
|
+
"stats_combiner.gemspec"
|
29
|
+
]
|
30
|
+
s.homepage = %q{http://github.com/tpm/stats_combiner}
|
31
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
32
|
+
s.require_paths = ["lib"]
|
33
|
+
s.rubygems_version = %q{1.3.6}
|
34
|
+
s.summary = %q{StatsCombiner creates most-viewed story widgets from the Chartbeat API}
|
35
|
+
s.test_files = [
|
36
|
+
"spec/stats_combiner_spec.rb",
|
37
|
+
"spec/test_data.rb"
|
38
|
+
]
|
39
|
+
|
40
|
+
if s.respond_to? :specification_version then
|
41
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
42
|
+
s.specification_version = 3
|
43
|
+
|
44
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
45
|
+
s.add_runtime_dependency(%q<crack>, [">= 0"])
|
46
|
+
s.add_runtime_dependency(%q<sequel>, [">= 0"])
|
47
|
+
s.add_development_dependency(%q<rspec>, [">= 0"])
|
48
|
+
s.add_development_dependency(%q<fakeweb>, [">= 0"])
|
49
|
+
s.add_development_dependency(%q<timecop>, [">= 0"])
|
50
|
+
s.add_development_dependency(%q<hpricot>, [">= 0"])
|
51
|
+
else
|
52
|
+
s.add_dependency(%q<crack>, [">= 0"])
|
53
|
+
s.add_dependency(%q<sequel>, [">= 0"])
|
54
|
+
s.add_dependency(%q<rspec>, [">= 0"])
|
55
|
+
s.add_dependency(%q<fakeweb>, [">= 0"])
|
56
|
+
s.add_dependency(%q<timecop>, [">= 0"])
|
57
|
+
s.add_dependency(%q<hpricot>, [">= 0"])
|
58
|
+
end
|
59
|
+
else
|
60
|
+
s.add_dependency(%q<crack>, [">= 0"])
|
61
|
+
s.add_dependency(%q<sequel>, [">= 0"])
|
62
|
+
s.add_dependency(%q<rspec>, [">= 0"])
|
63
|
+
s.add_dependency(%q<fakeweb>, [">= 0"])
|
64
|
+
s.add_dependency(%q<timecop>, [">= 0"])
|
65
|
+
s.add_dependency(%q<hpricot>, [">= 0"])
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
metadata
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: stats_combiner
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
version: 0.0.1
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Al Shaw
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-05-24 00:00:00 -04:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: crack
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 0
|
29
|
+
version: "0"
|
30
|
+
type: :runtime
|
31
|
+
version_requirements: *id001
|
32
|
+
- !ruby/object:Gem::Dependency
|
33
|
+
name: sequel
|
34
|
+
prerelease: false
|
35
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
segments:
|
40
|
+
- 0
|
41
|
+
version: "0"
|
42
|
+
type: :runtime
|
43
|
+
version_requirements: *id002
|
44
|
+
- !ruby/object:Gem::Dependency
|
45
|
+
name: rspec
|
46
|
+
prerelease: false
|
47
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
segments:
|
52
|
+
- 0
|
53
|
+
version: "0"
|
54
|
+
type: :development
|
55
|
+
version_requirements: *id003
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: fakeweb
|
58
|
+
prerelease: false
|
59
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
segments:
|
64
|
+
- 0
|
65
|
+
version: "0"
|
66
|
+
type: :development
|
67
|
+
version_requirements: *id004
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: timecop
|
70
|
+
prerelease: false
|
71
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
segments:
|
76
|
+
- 0
|
77
|
+
version: "0"
|
78
|
+
type: :development
|
79
|
+
version_requirements: *id005
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: hpricot
|
82
|
+
prerelease: false
|
83
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
segments:
|
88
|
+
- 0
|
89
|
+
version: "0"
|
90
|
+
type: :development
|
91
|
+
version_requirements: *id006
|
92
|
+
description: A tool to create most-viewed story widgets from the Chartbeat API.
|
93
|
+
email: almshaw@gmail.com
|
94
|
+
executables: []
|
95
|
+
|
96
|
+
extensions: []
|
97
|
+
|
98
|
+
extra_rdoc_files:
|
99
|
+
- README.md
|
100
|
+
files:
|
101
|
+
- .gitignore
|
102
|
+
- README.md
|
103
|
+
- Rakefile
|
104
|
+
- VERSION
|
105
|
+
- combiner_sample.rb
|
106
|
+
- lib/stats_combiner.rb
|
107
|
+
- lib/stats_combiner/filterer.rb
|
108
|
+
- spec/stats_combiner_spec.rb
|
109
|
+
- spec/test_data.rb
|
110
|
+
- stats_combiner.gemspec
|
111
|
+
has_rdoc: true
|
112
|
+
homepage: http://github.com/tpm/stats_combiner
|
113
|
+
licenses: []
|
114
|
+
|
115
|
+
post_install_message:
|
116
|
+
rdoc_options:
|
117
|
+
- --charset=UTF-8
|
118
|
+
require_paths:
|
119
|
+
- lib
|
120
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
segments:
|
125
|
+
- 0
|
126
|
+
version: "0"
|
127
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
segments:
|
132
|
+
- 0
|
133
|
+
version: "0"
|
134
|
+
requirements: []
|
135
|
+
|
136
|
+
rubyforge_project:
|
137
|
+
rubygems_version: 1.3.6
|
138
|
+
signing_key:
|
139
|
+
specification_version: 3
|
140
|
+
summary: StatsCombiner creates most-viewed story widgets from the Chartbeat API
|
141
|
+
test_files:
|
142
|
+
- spec/stats_combiner_spec.rb
|
143
|
+
- spec/test_data.rb
|