randumb 0.2.0 → 0.3.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/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
|