ranked-model-rails2 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/.rbenv-vars +1 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +82 -0
- data/Gemfile +32 -0
- data/LICENSE +19 -0
- data/Rakefile +7 -0
- data/Readme.mkd +205 -0
- data/lib/ranked-model.rb +69 -0
- data/lib/ranked-model/railtie.rb +14 -0
- data/lib/ranked-model/ranker.rb +327 -0
- data/lib/ranked-model/version.rb +3 -0
- data/rails/init.rb +1 -0
- data/ranked-model-rails2.gemspec +29 -0
- data/spec/duck-model/duck_spec.rb +770 -0
- data/spec/duck-model/lots_of_ducks_spec.rb +169 -0
- data/spec/duck-model/wrong_ducks_spec.rb +36 -0
- data/spec/ego-model/ego_spec.rb +36 -0
- data/spec/player-model/records_already_exist_spec.rb +22 -0
- data/spec/ranked-model/ranker_spec.rb +64 -0
- data/spec/ranked-model/version_spec.rb +7 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/sti-model/element_spec.rb +122 -0
- data/spec/sti-model/vehicle_spec.rb +75 -0
- data/spec/support/active_record.rb +150 -0
- data/spec/support/database.yml +30 -0
- metadata +171 -0
@@ -0,0 +1,169 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Duck do
|
4
|
+
|
5
|
+
before {
|
6
|
+
200.times do |i|
|
7
|
+
Duck.create \
|
8
|
+
:name => "Duck #{i}"
|
9
|
+
end
|
10
|
+
}
|
11
|
+
|
12
|
+
describe "setting and fetching by position" do
|
13
|
+
|
14
|
+
describe '137' do
|
15
|
+
|
16
|
+
before {
|
17
|
+
@last = Duck.last
|
18
|
+
@last.update_attribute :row_position, 137
|
19
|
+
}
|
20
|
+
|
21
|
+
subject { Duck.ranker(:row).with(Duck.new).current_at_position(137).instance }
|
22
|
+
|
23
|
+
its(:id) { should == @last.id }
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '2' do
|
28
|
+
|
29
|
+
before {
|
30
|
+
@last = Duck.last
|
31
|
+
@last.update_attribute :row_position, 2
|
32
|
+
}
|
33
|
+
|
34
|
+
subject { Duck.ranker(:row).with(Duck.new).current_at_position(2).instance }
|
35
|
+
|
36
|
+
its(:id) { should == @last.id }
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
describe 'last' do
|
41
|
+
|
42
|
+
before {
|
43
|
+
@last = Duck.last
|
44
|
+
@last.update_attribute :row_position, :last
|
45
|
+
}
|
46
|
+
|
47
|
+
subject { Duck.rank(:row).last }
|
48
|
+
|
49
|
+
its(:id) { should == @last.id }
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
describe 'first' do
|
54
|
+
|
55
|
+
before {
|
56
|
+
@last = Duck.last
|
57
|
+
@last.update_attribute :row_position, :first
|
58
|
+
}
|
59
|
+
|
60
|
+
subject { Duck.rank(:row).first }
|
61
|
+
|
62
|
+
its(:id) { should == @last.id }
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "a rearrangement" do
|
69
|
+
|
70
|
+
describe "with max value" do
|
71
|
+
|
72
|
+
before {
|
73
|
+
@first = Duck.first
|
74
|
+
@second = Duck.offset(1).first
|
75
|
+
@ordered = Duck.rank(:row).where(%Q{"id" NOT IN (#{@first.id}, #{@second.id})}).collect {|d| d.id }
|
76
|
+
@first.update_attribute :row, RankedModel::MAX_RANK_VALUE
|
77
|
+
@second.update_attribute :row, RankedModel::MAX_RANK_VALUE
|
78
|
+
}
|
79
|
+
|
80
|
+
context {
|
81
|
+
|
82
|
+
subject { Duck.rank(:row).collect {|d| d.id } }
|
83
|
+
|
84
|
+
it { should == (@ordered[0..-2] + [@ordered[-1], @first.id, @second.id]) }
|
85
|
+
|
86
|
+
}
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
describe "with max value and with_same pond" do
|
91
|
+
|
92
|
+
before {
|
93
|
+
Duck.order("id ASC").limit(50).each_with_index do |d, index|
|
94
|
+
d.update_attributes :age => index % 10, :pond => "Pond #{index / 10}"
|
95
|
+
end
|
96
|
+
@duck_11 = Duck.offset(10).first
|
97
|
+
@duck_12 = Duck.offset(11).first
|
98
|
+
@ordered = Duck.where(:pond => 'Pond 1').rank(:age).where(%Q{"id" NOT IN (#{@duck_11.id}, #{@duck_12.id})}).collect {|d| d.id }
|
99
|
+
@duck_11.update_attribute :age, RankedModel::MAX_RANK_VALUE
|
100
|
+
@duck_12.update_attribute :age, RankedModel::MAX_RANK_VALUE
|
101
|
+
}
|
102
|
+
|
103
|
+
context {
|
104
|
+
subject { Duck.where(:pond => 'Pond 1').rank(:age).collect {|d| d.id } }
|
105
|
+
|
106
|
+
it { should == (@ordered[0..-2] + [@ordered[-1], @duck_11.id, @duck_12.id]) }
|
107
|
+
}
|
108
|
+
|
109
|
+
context {
|
110
|
+
subject { Duck.first.age }
|
111
|
+
it { should == 0}
|
112
|
+
}
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
describe "with min value" do
|
117
|
+
|
118
|
+
before {
|
119
|
+
@first = Duck.first
|
120
|
+
@second = Duck.offset(1).first
|
121
|
+
@ordered = Duck.rank(:row).where(%Q{"id" NOT IN (#{@first.id}, #{@second.id})}).collect {|d| d.id }
|
122
|
+
@first.update_attribute :row, RankedModel::MIN_RANK_VALUE
|
123
|
+
@second.update_attribute :row, RankedModel::MIN_RANK_VALUE
|
124
|
+
}
|
125
|
+
|
126
|
+
context {
|
127
|
+
|
128
|
+
subject { Duck.rank(:row).collect {|d| d.id } }
|
129
|
+
|
130
|
+
it { should == ([@second.id, @first.id] + @ordered) }
|
131
|
+
|
132
|
+
}
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
describe "with no more gaps" do
|
137
|
+
|
138
|
+
before {
|
139
|
+
@first = Duck.first
|
140
|
+
@second = Duck.where(:row => RankedModel::MAX_RANK_VALUE).first || Duck.offset(1).first
|
141
|
+
@third = Duck.offset(2).first
|
142
|
+
@fourth = Duck.offset(4).first
|
143
|
+
@lower = Duck.rank(:row).
|
144
|
+
where(%Q{"id" NOT IN (#{@first.id}, #{@second.id}, #{@third.id}, #{@fourth.id})}).
|
145
|
+
where(%Q{"row" < ?}, RankedModel::MAX_RANK_VALUE / 2).
|
146
|
+
collect {|d| d.id }
|
147
|
+
@upper = Duck.rank(:row).
|
148
|
+
where(%Q{"id" NOT IN (#{@first.id}, #{@second.id}, #{@third.id}, #{@fourth.id})}).
|
149
|
+
where(%Q{"row" >= ?}, RankedModel::MAX_RANK_VALUE / 2).
|
150
|
+
collect {|d| d.id }
|
151
|
+
@first.update_attribute :row, RankedModel::MIN_RANK_VALUE
|
152
|
+
@second.update_attribute :row, RankedModel::MAX_RANK_VALUE
|
153
|
+
@third.update_attribute :row, (RankedModel::MAX_RANK_VALUE / 2)
|
154
|
+
@fourth.update_attribute :row, @third.row
|
155
|
+
}
|
156
|
+
|
157
|
+
context {
|
158
|
+
|
159
|
+
subject { Duck.rank(:row).collect {|d| d.id } }
|
160
|
+
|
161
|
+
it { should == ([@first.id] + @lower + [@fourth.id, @third.id] + @upper + [@second.id]) }
|
162
|
+
|
163
|
+
}
|
164
|
+
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe WrongScopeDuck do
|
4
|
+
|
5
|
+
it "should raise an error because of an unknown scope" do
|
6
|
+
|
7
|
+
expect {
|
8
|
+
WrongScopeDuck.create(:name => 'Quocky', :pond => 'Shin')
|
9
|
+
}.to raise_error(RankedModel::InvalidScope, 'No scope called "non_existant_scope" found in model')
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
describe WrongFieldDuck do
|
16
|
+
|
17
|
+
it "should raise an error because of an unknown field" do
|
18
|
+
|
19
|
+
expect {
|
20
|
+
WrongFieldDuck.create(:name => 'Quicky', :pond => 'Shin')
|
21
|
+
}.to raise_error(RankedModel::InvalidField, 'No field called "non_existant_field" found in model')
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
describe ReallyWrongFieldDuck do
|
28
|
+
|
29
|
+
it "should raise an error because of a specific unknown field" do
|
30
|
+
|
31
|
+
expect {
|
32
|
+
ReallyWrongFieldDuck.create(:name => 'Quicky', :pond => 'Shin')
|
33
|
+
}.to raise_error(RankedModel::InvalidField, 'No field called "non_existant_field" found in model')
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Ego do
|
4
|
+
|
5
|
+
before {
|
6
|
+
@egos = {
|
7
|
+
:bob => Ego.create(:name => 'Bob'),
|
8
|
+
:nick => Ego.create(:name => 'Nick'),
|
9
|
+
:sally => Ego.create(:name => 'Sally')
|
10
|
+
}
|
11
|
+
@egos.each { |name, ego|
|
12
|
+
ego.reload
|
13
|
+
ego.update_attribute :size_position, 0
|
14
|
+
ego.save!
|
15
|
+
}
|
16
|
+
@egos.each {|name, ego| ego.reload }
|
17
|
+
}
|
18
|
+
|
19
|
+
describe "sorting on size alternative primary key" do
|
20
|
+
|
21
|
+
before {
|
22
|
+
@egos[:nick].update_attribute :size_position, 0
|
23
|
+
@egos[:sally].update_attribute :size_position, 2
|
24
|
+
}
|
25
|
+
|
26
|
+
subject { Ego.rank(:size).to_a }
|
27
|
+
|
28
|
+
its(:size) { should == 3 }
|
29
|
+
|
30
|
+
its(:first) { should == @egos[:nick] }
|
31
|
+
|
32
|
+
its(:last) { should == @egos[:sally] }
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Player do
|
4
|
+
|
5
|
+
before {
|
6
|
+
@players = {}
|
7
|
+
@players[:dave] = Player.create!(:name => "Dave", :city => "Detroit")
|
8
|
+
@players[:bob] = Player.create!(:name => "Bob", :city => "Portland")
|
9
|
+
@players[:nigel] = Player.create!(:name => "Nigel", :city => "New York")
|
10
|
+
Player.class_eval do
|
11
|
+
include RankedModel
|
12
|
+
ranks :score
|
13
|
+
end
|
14
|
+
|
15
|
+
}
|
16
|
+
|
17
|
+
describe "setting the position of a record that already exists" do
|
18
|
+
it "sets the rank without error" do
|
19
|
+
expect{@players[:bob].update_attributes! :score_position => 1}.to_not raise_error
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe RankedModel::Ranker, 'initialized' do
|
4
|
+
|
5
|
+
subject {
|
6
|
+
RankedModel::Ranker.new \
|
7
|
+
:overview,
|
8
|
+
:column => :a_sorting_column,
|
9
|
+
:scope => :a_scope,
|
10
|
+
:with_same => :a_column,
|
11
|
+
:class_name => 'SomeClass',
|
12
|
+
:unless => :a_method
|
13
|
+
}
|
14
|
+
|
15
|
+
its(:name) { should == :overview }
|
16
|
+
its(:column) { should == :a_sorting_column }
|
17
|
+
its(:scope) { should == :a_scope }
|
18
|
+
its(:with_same) { should == :a_column }
|
19
|
+
its(:class_name) { should == 'SomeClass' }
|
20
|
+
its(:unless) { should == :a_method }
|
21
|
+
end
|
22
|
+
|
23
|
+
describe RankedModel::Ranker, 'unless as Symbol' do
|
24
|
+
let(:receiver) { mock('model') }
|
25
|
+
|
26
|
+
subject {
|
27
|
+
RankedModel::Ranker.new(:overview, :unless => :a_method).with(receiver)
|
28
|
+
}
|
29
|
+
|
30
|
+
context 'returns true' do
|
31
|
+
before { receiver.expects(:a_method).once.returns(true) }
|
32
|
+
|
33
|
+
its(:handle_ranking) { should == nil }
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'returns false' do
|
37
|
+
before { receiver.expects(:a_method).once.returns(false) }
|
38
|
+
|
39
|
+
it {
|
40
|
+
subject.expects(:update_index_from_position).once
|
41
|
+
subject.expects(:assure_unique_position).once
|
42
|
+
|
43
|
+
subject.handle_ranking
|
44
|
+
}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe RankedModel::Ranker, 'unless as Proc' do
|
49
|
+
context 'returns true' do
|
50
|
+
subject { RankedModel::Ranker.new(:overview, :unless => Proc.new { true }).with(Class.new) }
|
51
|
+
its(:handle_ranking) { should == nil }
|
52
|
+
end
|
53
|
+
|
54
|
+
context 'returns false' do
|
55
|
+
subject { RankedModel::Ranker.new(:overview, :unless => Proc.new { false }).with(Class.new) }
|
56
|
+
|
57
|
+
it {
|
58
|
+
subject.expects(:update_index_from_position).once
|
59
|
+
subject.expects(:assure_unique_position).once
|
60
|
+
|
61
|
+
subject.handle_ranking
|
62
|
+
}
|
63
|
+
end
|
64
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
|
4
|
+
require 'ranked-model'
|
5
|
+
|
6
|
+
Dir[File.join(File.dirname(__FILE__), 'support/**/*.rb')].each {|f| require f}
|
7
|
+
|
8
|
+
# After the DB connection is setup
|
9
|
+
require 'database_cleaner'
|
10
|
+
|
11
|
+
RSpec.configure do |config|
|
12
|
+
config.mock_with :mocha
|
13
|
+
|
14
|
+
config.before(:suite) do
|
15
|
+
DatabaseCleaner.strategy = :transaction
|
16
|
+
DatabaseCleaner.clean_with(:truncation)
|
17
|
+
end
|
18
|
+
|
19
|
+
config.before(:each) do
|
20
|
+
DatabaseCleaner.start
|
21
|
+
end
|
22
|
+
|
23
|
+
config.after(:each) do
|
24
|
+
DatabaseCleaner.clean
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
RSpec::Matchers.define :define_constant do |expected|
|
30
|
+
match { |actual| actual.const_defined?(expected) }
|
31
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Element do
|
4
|
+
|
5
|
+
before {
|
6
|
+
@elements = {
|
7
|
+
:chromium => TransitionMetal.create( :symbol => 'Cr' ),
|
8
|
+
:manganese => TransitionMetal.create( :symbol => 'Mn' ),
|
9
|
+
:argon => NobleGas.create( :symbol => 'Ar' ),
|
10
|
+
:helium => NobleGas.create( :symbol => 'He' ),
|
11
|
+
:xenon => NobleGas.create( :symbol => 'Xe' )
|
12
|
+
}
|
13
|
+
@elements.each { |name, element|
|
14
|
+
element.reload
|
15
|
+
element.update_attribute :combination_order_position, 0
|
16
|
+
}
|
17
|
+
@elements.each {|name, element| element.reload }
|
18
|
+
}
|
19
|
+
|
20
|
+
describe "rebalancing on an STI class should not affect the other class" do
|
21
|
+
|
22
|
+
before {
|
23
|
+
@elements[:helium].update_attribute :combination_order_position, :first
|
24
|
+
@elements[:xenon].update_attribute :combination_order_position, :first
|
25
|
+
@elements[:argon].update_attribute :combination_order_position, :last
|
26
|
+
|
27
|
+
TransitionMetal.ranker(:combination_order).with(@elements[:chromium]).instance_eval { rebalance_ranks }
|
28
|
+
}
|
29
|
+
|
30
|
+
subject { NobleGas.rank(:combination_order) }
|
31
|
+
|
32
|
+
its(:size) { should == 3 }
|
33
|
+
|
34
|
+
its(:first) { should == @elements[:xenon] }
|
35
|
+
|
36
|
+
its(:last) { should == @elements[:argon] }
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "setting positions on STI classes" do
|
41
|
+
|
42
|
+
before {
|
43
|
+
@elements[:helium].update_attribute :combination_order_position, :first
|
44
|
+
@elements[:xenon].update_attribute :combination_order_position, :first
|
45
|
+
@elements[:argon].update_attribute :combination_order_position, :first
|
46
|
+
|
47
|
+
@elements[:chromium].update_attribute :combination_order_position, 1
|
48
|
+
@elements[:manganese].update_attribute :combination_order_position, 1
|
49
|
+
@elements[:manganese].update_attribute :combination_order_position, 0
|
50
|
+
@elements[:chromium].update_attribute :combination_order_position, 0
|
51
|
+
@elements[:manganese].update_attribute :combination_order_position, 0
|
52
|
+
@elements[:chromium].update_attribute :combination_order_position, 0
|
53
|
+
}
|
54
|
+
|
55
|
+
describe "NobleGas" do
|
56
|
+
|
57
|
+
subject { NobleGas.rank(:combination_order) }
|
58
|
+
|
59
|
+
its(:size) { should == 3 }
|
60
|
+
|
61
|
+
its(:first) { should == @elements[:argon] }
|
62
|
+
|
63
|
+
its(:last) { should == @elements[:helium] }
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
describe "TransitionMetal" do
|
68
|
+
|
69
|
+
subject { TransitionMetal.rank(:combination_order) }
|
70
|
+
|
71
|
+
its(:size) { should == 2 }
|
72
|
+
|
73
|
+
its(:first) { should == @elements[:chromium] }
|
74
|
+
|
75
|
+
its(:last) { should == @elements[:manganese] }
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
describe "setting positions on STI classes" do
|
82
|
+
|
83
|
+
before {
|
84
|
+
@elements[:helium].update_attribute :combination_order_position, :first
|
85
|
+
@elements[:xenon].update_attribute :combination_order_position, :first
|
86
|
+
@elements[:argon].update_attribute :combination_order_position, :first
|
87
|
+
|
88
|
+
@elements[:chromium].update_attribute :combination_order_position, 1
|
89
|
+
@elements[:manganese].update_attribute :combination_order_position, 1
|
90
|
+
@elements[:manganese].update_attribute :combination_order_position, 0
|
91
|
+
@elements[:chromium].update_attribute :combination_order_position, 0
|
92
|
+
@elements[:manganese].update_attribute :combination_order_position, 0
|
93
|
+
@elements[:chromium].update_attribute :combination_order_position, 0
|
94
|
+
}
|
95
|
+
|
96
|
+
describe "NobleGas" do
|
97
|
+
|
98
|
+
subject { NobleGas.rank(:combination_order) }
|
99
|
+
|
100
|
+
its(:size) { should == 3 }
|
101
|
+
|
102
|
+
its(:first) { should == @elements[:argon] }
|
103
|
+
|
104
|
+
its(:last) { should == @elements[:helium] }
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
describe "TransitionMetal" do
|
109
|
+
|
110
|
+
subject { TransitionMetal.rank(:combination_order) }
|
111
|
+
|
112
|
+
its(:size) { should == 2 }
|
113
|
+
|
114
|
+
its(:first) { should == @elements[:chromium] }
|
115
|
+
|
116
|
+
its(:last) { should == @elements[:manganese] }
|
117
|
+
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|