randumb 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/randumb.rb +4 -45
- data/lib/randumb/relation.rb +160 -0
- data/lib/randumb/version.rb +3 -0
- data/test/models/album.rb +4 -0
- data/test/models/artist.rb +5 -0
- data/test/models/factories.rb +13 -0
- data/test/randumb_test.rb +193 -0
- data/test/test_helper.rb +67 -0
- data/test/weighted_test.rb +125 -0
- metadata +85 -6
data/lib/randumb.rb
CHANGED
@@ -1,54 +1,13 @@
|
|
1
|
-
require '
|
2
|
-
require '
|
3
|
-
|
4
|
-
module Randumb
|
5
|
-
# https://github.com/rails/rails/blob/master/activerecord/lib/active_record/relation/query_methods.rb
|
6
|
-
module ActiveRecord
|
7
|
-
|
8
|
-
module Relation
|
9
|
-
|
10
|
-
def random(max_items = nil)
|
11
|
-
return_first_record = max_items.nil? # see return switch at end
|
12
|
-
max_items ||= 1
|
13
|
-
relation = clone
|
14
|
-
|
15
|
-
if connection.adapter_name =~ /sqlite/i || connection.adapter_name =~ /postgres/i
|
16
|
-
rand_syntax = "RANDOM()"
|
17
|
-
elsif connection.adapter_name =~ /mysql/i
|
18
|
-
rand_syntax = "RAND()"
|
19
|
-
else
|
20
|
-
raise Exception, "ActiveRecord adapter: '#{connection.adapter_name}' not supported by randumb. Send a pull request or open a ticket: https://github.com/spilliton/randumb"
|
21
|
-
end
|
22
|
-
|
23
|
-
the_scope = relation.order(rand_syntax)
|
24
|
-
the_scope = the_scope.limit(max_items) unless relation.limit_value && relation.limit_value < max_items
|
25
|
-
|
26
|
-
# return first record if method was called without parameters
|
27
|
-
if return_first_record
|
28
|
-
the_scope.first
|
29
|
-
else
|
30
|
-
the_scope.all
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
end # Relation
|
35
|
-
|
36
|
-
module Base
|
37
|
-
# Class method
|
38
|
-
def random(max_items = nil)
|
39
|
-
relation.random(max_items)
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
|
44
|
-
end # ActiveRecord
|
45
|
-
end # Randumb
|
1
|
+
require 'randumb/version'
|
2
|
+
require 'randumb/relation'
|
46
3
|
|
47
4
|
# Mix it in
|
48
5
|
class ActiveRecord::Relation
|
49
6
|
include Randumb::ActiveRecord::Relation
|
7
|
+
include Randumb::ActiveRecord::MethodMissingMagicks
|
50
8
|
end
|
51
9
|
|
52
10
|
class ActiveRecord::Base
|
53
11
|
extend Randumb::ActiveRecord::Base
|
12
|
+
extend Randumb::ActiveRecord::MethodMissingMagicks
|
54
13
|
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'active_support/core_ext/module/delegation'
|
2
|
+
require 'active_record/relation'
|
3
|
+
|
4
|
+
module Randumb
|
5
|
+
# https://github.com/rails/rails/blob/master/activerecord/lib/active_record/relation/query_methods.rb
|
6
|
+
module ActiveRecord
|
7
|
+
|
8
|
+
module Relation
|
9
|
+
|
10
|
+
# If the max_items argument is omitted, one random entity will be returned.
|
11
|
+
# If you provide the integer argument, you will get back an array of records.
|
12
|
+
def random(max_items = nil)
|
13
|
+
random_weighted(nil, max_items)
|
14
|
+
end
|
15
|
+
|
16
|
+
# If ranking_column is provided, that named column wil be multiplied
|
17
|
+
# by a random number to determine probability of order. The ranking column must be numeric.
|
18
|
+
def random_weighted(ranking_column, max_items = nil)
|
19
|
+
relation = clone
|
20
|
+
|
21
|
+
# postgres won't let you do an order_by when also doing a distinct
|
22
|
+
# let's just use the in-memory option in this case
|
23
|
+
if relation.uniq_value && connection.adapter_name =~ /postgres/i
|
24
|
+
if ranking_column
|
25
|
+
raise Exception, "random_weighted: not possible when using .uniq and the postgres db adapter"
|
26
|
+
else
|
27
|
+
return random_by_id_shuffle(max_items)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# ensure a valid column for ranking
|
32
|
+
if ranking_column
|
33
|
+
column_data = @klass.columns_hash[ranking_column.to_s]
|
34
|
+
raise ArgumentError.new("random_weighted: #{ranking_column} is not a column on #{@klass.table_name}!") unless column_data
|
35
|
+
raise ArgumentError.new("random_weighted: #{ranking_column} is not a numeric column on #{@klass.table_name}!") unless [:integer, :float].include?(column_data.type)
|
36
|
+
end
|
37
|
+
|
38
|
+
# choose the right syntax
|
39
|
+
if connection.adapter_name =~ /(sqlite|postgres)/i
|
40
|
+
rand_syntax = "RANDOM()"
|
41
|
+
elsif connection.adapter_name =~ /mysql/i
|
42
|
+
rand_syntax = "RAND()"
|
43
|
+
else
|
44
|
+
raise Exception, "ActiveRecord adapter: '#{connection.adapter_name}' not supported by randumb. Send a pull request or open a ticket: https://github.com/spilliton/randumb"
|
45
|
+
end
|
46
|
+
|
47
|
+
order_clause = if ranking_column.nil?
|
48
|
+
rand_syntax
|
49
|
+
else
|
50
|
+
if connection.adapter_name =~ /sqlite/i
|
51
|
+
# computer multiplication is faster than division I was once taught...so translate here
|
52
|
+
max_int = 9223372036854775807.0
|
53
|
+
multiplier = 1.0 / max_int
|
54
|
+
"(#{ranking_column} * ABS(#{rand_syntax} * #{multiplier}) ) DESC"
|
55
|
+
else
|
56
|
+
"(#{ranking_column} * #{rand_syntax}) DESC"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
the_scope = relation.order(order_clause)
|
61
|
+
|
62
|
+
# override the limit if they are requesting multiple records
|
63
|
+
if max_items && (!relation.limit_value || relation.limit_value > max_items)
|
64
|
+
the_scope = the_scope.limit(max_items)
|
65
|
+
end
|
66
|
+
|
67
|
+
# return first record if method was called without parameters
|
68
|
+
if max_items.nil?
|
69
|
+
the_scope.first
|
70
|
+
else
|
71
|
+
the_scope.all
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
# This was my first implementation, adding it as an option for people to use
|
77
|
+
# and to fall back on for pesky DB one off situations...
|
78
|
+
# https://github.com/spilliton/randumb/issues/7
|
79
|
+
def random_by_id_shuffle(max_items = nil)
|
80
|
+
return_first_record = max_items.nil?# see return switch at end
|
81
|
+
max_items ||= 1
|
82
|
+
relation = clone
|
83
|
+
|
84
|
+
# store these for including on final scope
|
85
|
+
original_includes = relation.includes_values
|
86
|
+
original_selects = relation.select_values
|
87
|
+
|
88
|
+
# clear these for our id only query
|
89
|
+
relation.select_values = []
|
90
|
+
relation.includes_values = []
|
91
|
+
|
92
|
+
# do original query but only for id field
|
93
|
+
id_only_relation = relation.select("#{table_name}.id")
|
94
|
+
id_results = connection.select_all(id_only_relation.to_sql)
|
95
|
+
|
96
|
+
# get requested number of random ids
|
97
|
+
if max_items == 1 && id_results.length > 0
|
98
|
+
ids = [ id_results[ rand(id_results.length) ]['id'] ]
|
99
|
+
else
|
100
|
+
ids = id_results.shuffle![0,max_items].collect!{ |h| h['id'] }
|
101
|
+
end
|
102
|
+
|
103
|
+
# build scope for final query
|
104
|
+
the_scope = klass.includes(original_includes)
|
105
|
+
|
106
|
+
# specifying empty selects caused bug in rails 3.0.0/3.0.1
|
107
|
+
the_scope = the_scope.select(original_selects) unless original_selects.empty?
|
108
|
+
|
109
|
+
# get the records and shuffle since the order of the ids
|
110
|
+
# passed to find_all_by_id isn't retained in the result set
|
111
|
+
records = the_scope.find_all_by_id(ids).shuffle!
|
112
|
+
|
113
|
+
# return first record if method was called without parameters
|
114
|
+
if return_first_record
|
115
|
+
records.first
|
116
|
+
else
|
117
|
+
records
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
# Class methods
|
125
|
+
module Base
|
126
|
+
def random(max_items = nil)
|
127
|
+
relation.random(max_items)
|
128
|
+
end
|
129
|
+
|
130
|
+
def random_weighted(ranking_column, max_items = nil)
|
131
|
+
relation.random_weighted(ranking_column, max_items)
|
132
|
+
end
|
133
|
+
|
134
|
+
def random_by_id_shuffle(max_items = nil)
|
135
|
+
relation.random_by_id_shuffle(max_items)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
|
140
|
+
# These get registered as class and instance methods
|
141
|
+
module MethodMissingMagicks
|
142
|
+
def method_missing(symbol, *args)
|
143
|
+
if symbol.to_s =~ /^random_weighted_by_(\w+)$/
|
144
|
+
random_weighted($1, *args)
|
145
|
+
else
|
146
|
+
super
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def respond_to?(symbol, include_private=false)
|
151
|
+
if symbol.to_s =~ /^random_weighted_by_(\w+)$/
|
152
|
+
true
|
153
|
+
else
|
154
|
+
super
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
end # ActiveRecord
|
160
|
+
end # Randumb
|
@@ -0,0 +1,13 @@
|
|
1
|
+
FactoryGirl.define do
|
2
|
+
factory :artist do
|
3
|
+
name { Faker::Lorem.words(3).join(' ') }
|
4
|
+
views { Random.rand(50) }
|
5
|
+
rating { Random.rand(100) / 100 }
|
6
|
+
end
|
7
|
+
|
8
|
+
factory :album do
|
9
|
+
name { Faker::Lorem.words(3).join(' ') }
|
10
|
+
views { Random.rand(50) }
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
$:.unshift '.'; require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class RandumbTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def assert_equal_for_both_methods(expected, obj, params = nil)
|
6
|
+
assert_equal expected, obj.send(:random, params), "when calling random"
|
7
|
+
assert_equal expected, obj.send(:random_by_id_shuffle, params), "when calling random_by_id_shuffle"
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
should "should return empty when no record in table" do
|
12
|
+
assert_equal 0, Artist.count
|
13
|
+
|
14
|
+
assert_equal_for_both_methods nil, Artist
|
15
|
+
# above is equivalent to:
|
16
|
+
# assert_equal nil, Artist.random
|
17
|
+
# assert_equal nil, Artist.random_by_id_shuffle
|
18
|
+
|
19
|
+
assert_equal_for_both_methods [], Artist, 1
|
20
|
+
# above is equivalent to:
|
21
|
+
# assert_equal [], Artist.random(1)
|
22
|
+
# assert_equal [], Artist.random_by_id_shuffle(1)
|
23
|
+
|
24
|
+
assert_equal_for_both_methods nil, Artist.limit(50)
|
25
|
+
end
|
26
|
+
|
27
|
+
context "1 record in the table" do
|
28
|
+
setup do
|
29
|
+
@high_on_fire = FactoryGirl.create(:artist, :name => "High On Fire", :views => 1)
|
30
|
+
end
|
31
|
+
|
32
|
+
should "select only 1 record even when you request more" do
|
33
|
+
assert_equal 1, Artist.count
|
34
|
+
|
35
|
+
assert_equal_for_both_methods @high_on_fire, Artist
|
36
|
+
assert_equal_for_both_methods [@high_on_fire], Artist, 1
|
37
|
+
assert_equal_for_both_methods [@high_on_fire], Artist, 30
|
38
|
+
end
|
39
|
+
|
40
|
+
should "not return a record that doesnt match where" do
|
41
|
+
assert_equal_for_both_methods nil, Artist.where(:name => "The Little Gentlemen")
|
42
|
+
end
|
43
|
+
|
44
|
+
context "3 records in table" do
|
45
|
+
setup do
|
46
|
+
@fiona_apple = FactoryGirl.create(:artist, :name => "Fiona Apple", :views => 3)
|
47
|
+
@magnetic_fields = FactoryGirl.create(:artist, :name => "The Magnetic Fields", :views => 2)
|
48
|
+
end
|
49
|
+
|
50
|
+
should "apply randomness after other orders when using sql method" do
|
51
|
+
assert_equal @fiona_apple, Artist.order("views desc").random
|
52
|
+
assert_equal [@fiona_apple, @magnetic_fields], Artist.order("views desc").random(2)
|
53
|
+
end
|
54
|
+
|
55
|
+
should "take smaller limit if one is provided in scope" do
|
56
|
+
assert_equal 2, Artist.limit(2).random(3).length
|
57
|
+
assert_equal 2, Artist.limit(2).random_by_id_shuffle(3).length
|
58
|
+
|
59
|
+
assert_equal 2, Artist.limit(3).random(2).length
|
60
|
+
assert_equal 2, Artist.limit(3).random_by_id_shuffle(2).length
|
61
|
+
end
|
62
|
+
|
63
|
+
should "respect selecting certain columns" do
|
64
|
+
assert_equal 3, Artist.find(@fiona_apple.id).views
|
65
|
+
|
66
|
+
artists = Artist.select(:name).random(3)
|
67
|
+
assert_equal false, artists.first.name.nil?
|
68
|
+
assert_raise (ActiveModel::MissingAttributeError) {artists.first.views}
|
69
|
+
|
70
|
+
artists = Artist.select(:name).random_by_id_shuffle(3)
|
71
|
+
assert_equal false, artists.first.name.nil?
|
72
|
+
assert_raise (ActiveModel::MissingAttributeError) {artists.first.views}
|
73
|
+
end
|
74
|
+
|
75
|
+
should "respect scopes" do
|
76
|
+
assert_equal_for_both_methods [@fiona_apple], Artist.at_least_three_views, 3
|
77
|
+
end
|
78
|
+
|
79
|
+
should "select only as many as in the db if we request more" do
|
80
|
+
random_artists = Artist.random(10)
|
81
|
+
assert_equal 3, random_artists.length
|
82
|
+
assert_equal true, random_artists.include?(@high_on_fire)
|
83
|
+
assert_equal true, random_artists.include?(@fiona_apple)
|
84
|
+
assert_equal true, random_artists.include?(@magnetic_fields)
|
85
|
+
|
86
|
+
random_artists = Artist.random_by_id_shuffle(10)
|
87
|
+
assert_equal 3, random_artists.length
|
88
|
+
assert_equal true, random_artists.include?(@high_on_fire)
|
89
|
+
assert_equal true, random_artists.include?(@fiona_apple)
|
90
|
+
assert_equal true, random_artists.include?(@magnetic_fields)
|
91
|
+
end
|
92
|
+
|
93
|
+
context "with some albums" do
|
94
|
+
setup do
|
95
|
+
@tidal = FactoryGirl.create(:album, :name => "Tidal", :artist => @fiona_apple)
|
96
|
+
@extraordinary_machine = FactoryGirl.create(:album, :name => "Extraordinary Machine", :artist => @fiona_apple)
|
97
|
+
@sixty_nine_love_songs = FactoryGirl.create(:album, :name => "69 Love Songs", :artist => @magnetic_fields)
|
98
|
+
@snakes_for_the_divine = FactoryGirl.create(:album, :name => "Snakes For the Divine", :artist => @high_on_fire)
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
should "work with includes for default method" do
|
103
|
+
artists = Artist.includes(:albums).random(10)
|
104
|
+
fiona_apple = artists.find { |a| a.name == "Fiona Apple" }
|
105
|
+
# if I add a new album now, it shouldn't be in the albums assocation yet b/c it was already loaded
|
106
|
+
FactoryGirl.create(:album, :name => "When The Pawn", :artist => @fiona_apple)
|
107
|
+
|
108
|
+
assert_equal 2, fiona_apple.albums.length
|
109
|
+
assert_equal 3, @fiona_apple.reload.albums.length
|
110
|
+
end
|
111
|
+
|
112
|
+
should "work with includes for shuffle method" do
|
113
|
+
artists = Artist.includes(:albums).random_by_id_shuffle(10)
|
114
|
+
fiona_apple = artists.find { |a| a.name == "Fiona Apple" }
|
115
|
+
# if I add a new album now, it shouldn't be in the albums assocation yet b/c it was already loaded
|
116
|
+
FactoryGirl.create(:album, :name => "When The Pawn", :artist => @fiona_apple)
|
117
|
+
|
118
|
+
assert_equal 2, fiona_apple.albums.length
|
119
|
+
assert_equal 3, @fiona_apple.reload.albums.length
|
120
|
+
end
|
121
|
+
|
122
|
+
should "work with joins for default method" do
|
123
|
+
albums = Album.joins(:artist).where("artists.views > 1").random(3)
|
124
|
+
|
125
|
+
assert_equal 3, albums.length
|
126
|
+
assert_equal false, albums.include?(@snakes_for_the_divine)
|
127
|
+
assert_equal true, albums.include?(@tidal)
|
128
|
+
assert_equal true, albums.include?(@extraordinary_machine)
|
129
|
+
assert_equal true, albums.include?(@sixty_nine_love_songs)
|
130
|
+
end
|
131
|
+
|
132
|
+
should "work with joins for shuffle method" do
|
133
|
+
albums = Album.joins(:artist).where("artists.views > 1").random_by_id_shuffle(3)
|
134
|
+
|
135
|
+
assert_equal 3, albums.length
|
136
|
+
assert_equal false, albums.include?(@snakes_for_the_divine)
|
137
|
+
assert_equal true, albums.include?(@tidal)
|
138
|
+
assert_equal true, albums.include?(@extraordinary_machine)
|
139
|
+
assert_equal true, albums.include?(@sixty_nine_love_songs)
|
140
|
+
end
|
141
|
+
|
142
|
+
should "work with uniq" do
|
143
|
+
assert_equal 2, Artist.uniq.random(2).length
|
144
|
+
assert_equal 2, Artist.uniq.random_by_id_shuffle(2).length
|
145
|
+
|
146
|
+
assert_not_nil Artist.uniq.random
|
147
|
+
assert_not_nil Artist.uniq.random_by_id_shuffle
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
|
156
|
+
context "2 records in table" do
|
157
|
+
setup do
|
158
|
+
@hum = FactoryGirl.create(:artist, :name => "Hum", :views => 3)
|
159
|
+
@minutemen = FactoryGirl.create(:artist, :name => "Minutemen", :views => 2)
|
160
|
+
end
|
161
|
+
|
162
|
+
should "eventually render the 2 possible orders using default method" do
|
163
|
+
order1 = [@hum, @minutemen]
|
164
|
+
order2 = [@minutemen, @hum]
|
165
|
+
order1_found = false
|
166
|
+
order2_found = false
|
167
|
+
100.times do
|
168
|
+
order = Artist.random(2)
|
169
|
+
order1_found = true if order == order1
|
170
|
+
order2_found = true if order == order2
|
171
|
+
break if order1_found && order2_found
|
172
|
+
end
|
173
|
+
assert order1_found
|
174
|
+
assert order2_found
|
175
|
+
end
|
176
|
+
|
177
|
+
should "eventually render the 2 possible orders using shuffle method" do
|
178
|
+
order1 = [@hum, @minutemen]
|
179
|
+
order2 = [@minutemen, @hum]
|
180
|
+
order1_found = false
|
181
|
+
order2_found = false
|
182
|
+
100.times do
|
183
|
+
order = Artist.random_by_id_shuffle(2)
|
184
|
+
order1_found = true if order == order1
|
185
|
+
order2_found = true if order == order2
|
186
|
+
break if order1_found && order2_found
|
187
|
+
end
|
188
|
+
assert order1_found
|
189
|
+
assert order2_found
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
$LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
|
2
|
+
require 'rubygems'
|
3
|
+
require 'test/unit'
|
4
|
+
require 'shoulda'
|
5
|
+
require 'factory_girl'
|
6
|
+
require 'faker'
|
7
|
+
require 'active_record'
|
8
|
+
require 'active_support/dependencies'
|
9
|
+
require 'active_support/core_ext/logger'
|
10
|
+
# require 'active_record/fixtures'
|
11
|
+
require 'randumb'
|
12
|
+
|
13
|
+
MODELS_PATH = File.join(File.dirname(__FILE__), 'models')
|
14
|
+
|
15
|
+
|
16
|
+
ActiveRecord::Base.logger = Logger.new(STDERR)
|
17
|
+
ActiveRecord::Base.logger.level = Logger::WARN
|
18
|
+
|
19
|
+
config = YAML::load(File.open(File.expand_path("../databases.yml", __FILE__)))
|
20
|
+
version = ActiveRecord::VERSION::STRING
|
21
|
+
driver = (ENV["DB"] or "sqlite3").downcase
|
22
|
+
in_memory = config[driver]["database"] == ":memory:"
|
23
|
+
|
24
|
+
# http://about.travis-ci.org/docs/user/database-setup/
|
25
|
+
commands = {
|
26
|
+
"mysql" => "mysql -e 'create database randumb_test;'",
|
27
|
+
"postgres" => "psql -c 'create database randumb_test;' -U postgres"
|
28
|
+
}
|
29
|
+
%x{#{commands[driver] || true}}
|
30
|
+
|
31
|
+
ActiveRecord::Base.establish_connection config[driver]
|
32
|
+
puts "Using #{RUBY_VERSION} AR #{version} with #{driver}"
|
33
|
+
|
34
|
+
|
35
|
+
ActiveRecord::Base.connection.create_table(:artists, :force => true) do |t|
|
36
|
+
t.string "name"
|
37
|
+
t.integer "views"
|
38
|
+
t.float "rating"
|
39
|
+
t.datetime "created_at"
|
40
|
+
t.datetime "updated_at"
|
41
|
+
end
|
42
|
+
|
43
|
+
ActiveRecord::Base.connection.create_table(:albums, :force => true) do |t|
|
44
|
+
t.string "name"
|
45
|
+
t.integer "views"
|
46
|
+
t.integer "artist_id"
|
47
|
+
t.datetime "created_at"
|
48
|
+
t.datetime "updated_at"
|
49
|
+
end
|
50
|
+
|
51
|
+
# setup models for lazy load
|
52
|
+
dep = defined?(ActiveSupport::Dependencies) ? ActiveSupport::Dependencies : ::Dependencies
|
53
|
+
dep.autoload_paths.unshift MODELS_PATH
|
54
|
+
|
55
|
+
# load factories now
|
56
|
+
require 'test/models/factories'
|
57
|
+
|
58
|
+
# clear db for every test
|
59
|
+
class Test::Unit::TestCase
|
60
|
+
|
61
|
+
def setup
|
62
|
+
Artist.delete_all
|
63
|
+
Album.delete_all
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
@@ -0,0 +1,125 @@
|
|
1
|
+
$:.unshift '.'; require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class WeightedTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
should "raise exception when called with a non-existent column" do
|
6
|
+
assert_raises(ArgumentError) do
|
7
|
+
Artist.random_weighted(:blah)
|
8
|
+
end
|
9
|
+
assert_raises(ArgumentError) do
|
10
|
+
Artist.random_weighted_by_blah
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
should "raise exception when called with a non-numeric column" do
|
15
|
+
assert_raises(ArgumentError) do
|
16
|
+
Artist.random_weighted(:name)
|
17
|
+
end
|
18
|
+
assert_raises(ArgumentError) do
|
19
|
+
Artist.random_weighted_by_name
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
if ENV["DB"] == "postgres"
|
24
|
+
should "raise exception if being called with uniq/postgres" do
|
25
|
+
assert_raises(Exception) do
|
26
|
+
Artist.uniq.random_weighted(:name)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
else
|
30
|
+
should "work with uniq if not postgres" do
|
31
|
+
assert_nil Artist.uniq.random_weighted_by_views
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
should "not blow up with integer columns and float column types" do
|
36
|
+
assert_nil Artist.random_weighted_by_views
|
37
|
+
assert_nil Artist.random_weighted_by_rating
|
38
|
+
end
|
39
|
+
|
40
|
+
should "not interfere with active record dynamic methods that use method_missing" do
|
41
|
+
@artist = FactoryGirl.create(:artist, :name => 'Spiritualized')
|
42
|
+
assert_equal @artist, Artist.find_by_name('Spiritualized')
|
43
|
+
end
|
44
|
+
|
45
|
+
should "respond to respond_to?" do
|
46
|
+
assert Artist.respond_to?(:random_weighted_by_views)
|
47
|
+
assert Artist.respond_to?(:random_weighted_by_xxxxxx)
|
48
|
+
assert Artist.at_least_three_views.respond_to?(:random_weighted_by_xxxxxx)
|
49
|
+
end
|
50
|
+
|
51
|
+
should "not interfere with active record dynamic methods that use respond_to?" do
|
52
|
+
assert Artist.respond_to?(:find_by_name)
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
context "order by ranking_column" do
|
57
|
+
setup do
|
58
|
+
@view_counts = [1, 2, 3, 4, 5]
|
59
|
+
@view_counts.each { |views| FactoryGirl.create(:artist, :views => views) }
|
60
|
+
end
|
61
|
+
|
62
|
+
should "order by ranking column with explicit method call" do
|
63
|
+
assert_hits_per_views do
|
64
|
+
Artist.random_weighted("views").views
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
should "order by ranking column with method_missing" do
|
69
|
+
assert_hits_per_views do
|
70
|
+
Artist.random_weighted_by_views.views
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
should "order by ranking column with explicit method call and max_items" do
|
75
|
+
assert_hits_per_views do
|
76
|
+
result = Artist.random_weighted("views", 5)
|
77
|
+
assert(result.size == 5)
|
78
|
+
result.first.views
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
should "order by ranking column with method_missing using max_items" do
|
83
|
+
assert_hits_per_views do
|
84
|
+
result = Artist.random_weighted_by_views(10)
|
85
|
+
assert(result.size == 5)
|
86
|
+
result.first.views
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
should "LAST order should fail" do
|
91
|
+
assert_raises(MiniTest::Assertion) do
|
92
|
+
assert_hits_per_views do
|
93
|
+
result = Artist.random_weighted_by_views(3)
|
94
|
+
assert(result.size == 3)
|
95
|
+
result.last.views
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
should "order by ranking column with method_missing using 1 max_items" do
|
101
|
+
assert_hits_per_views do
|
102
|
+
result = Artist.random_weighted_by_views(1)
|
103
|
+
assert(result.size == 1)
|
104
|
+
result.first.views
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def assert_hits_per_views
|
109
|
+
hits_per_views = Hash.new
|
110
|
+
@view_counts.each { |views| hits_per_views[views] = 0 }
|
111
|
+
|
112
|
+
1000.times do
|
113
|
+
hits_per_views[yield] += 1
|
114
|
+
end
|
115
|
+
last_count = 0
|
116
|
+
@view_counts.each do |views|
|
117
|
+
hits = hits_per_views[views]
|
118
|
+
assert(hits >= last_count, "#{hits} > #{last_count} : There were an unexpected number of visits: #{hits_per_views.to_yaml}")
|
119
|
+
last_count = hits
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
end
|
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: randumb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.
|
5
|
+
version: 0.3.0
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Zachary Kloepping
|
@@ -10,19 +10,85 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2012-
|
13
|
+
date: 2012-07-07 00:00:00 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
|
-
name:
|
16
|
+
name: activesupport
|
17
17
|
prerelease: false
|
18
18
|
requirement: &id001 !ruby/object:Gem::Requirement
|
19
|
+
none: false
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 3.0.0
|
24
|
+
type: :runtime
|
25
|
+
version_requirements: *id001
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: activerecord
|
28
|
+
prerelease: false
|
29
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
30
|
+
none: false
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: 3.0.0
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id002
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: rake
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: "0"
|
46
|
+
type: :runtime
|
47
|
+
version_requirements: *id003
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: pg
|
50
|
+
prerelease: false
|
51
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
19
52
|
none: false
|
20
53
|
requirements:
|
21
54
|
- - ">="
|
22
55
|
- !ruby/object:Gem::Version
|
23
56
|
version: "0"
|
24
57
|
type: :development
|
25
|
-
version_requirements: *
|
58
|
+
version_requirements: *id004
|
59
|
+
- !ruby/object:Gem::Dependency
|
60
|
+
name: shoulda
|
61
|
+
prerelease: false
|
62
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
63
|
+
none: false
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: "0"
|
68
|
+
type: :development
|
69
|
+
version_requirements: *id005
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: factory_girl
|
72
|
+
prerelease: false
|
73
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ~>
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: "3.0"
|
79
|
+
type: :development
|
80
|
+
version_requirements: *id006
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
name: faker
|
83
|
+
prerelease: false
|
84
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
85
|
+
none: false
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: "0"
|
90
|
+
type: :development
|
91
|
+
version_requirements: *id007
|
26
92
|
description:
|
27
93
|
email:
|
28
94
|
executables: []
|
@@ -32,7 +98,15 @@ extensions: []
|
|
32
98
|
extra_rdoc_files: []
|
33
99
|
|
34
100
|
files:
|
101
|
+
- lib/randumb/relation.rb
|
102
|
+
- lib/randumb/version.rb
|
35
103
|
- lib/randumb.rb
|
104
|
+
- test/models/album.rb
|
105
|
+
- test/models/artist.rb
|
106
|
+
- test/models/factories.rb
|
107
|
+
- test/randumb_test.rb
|
108
|
+
- test/test_helper.rb
|
109
|
+
- test/weighted_test.rb
|
36
110
|
homepage: https://github.com/spilliton/randumb
|
37
111
|
licenses: []
|
38
112
|
|
@@ -60,5 +134,10 @@ rubygems_version: 1.8.24
|
|
60
134
|
signing_key:
|
61
135
|
specification_version: 3
|
62
136
|
summary: Adds the ability to pull random records from ActiveRecord
|
63
|
-
test_files:
|
64
|
-
|
137
|
+
test_files:
|
138
|
+
- test/models/album.rb
|
139
|
+
- test/models/artist.rb
|
140
|
+
- test/models/factories.rb
|
141
|
+
- test/randumb_test.rb
|
142
|
+
- test/test_helper.rb
|
143
|
+
- test/weighted_test.rb
|