ranked-model 0.4.0 → 0.4.7
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.
- checksums.yaml +5 -5
- data/.gitignore +2 -0
- data/.rspec +1 -0
- data/.travis.yml +43 -74
- data/Appraisals +72 -0
- data/Gemfile +7 -23
- data/Readme.mkd +139 -19
- data/gemfiles/rails_4_2.gemfile +23 -0
- data/gemfiles/rails_5_0.gemfile +22 -0
- data/gemfiles/rails_5_1.gemfile +22 -0
- data/gemfiles/rails_5_2.gemfile +22 -0
- data/gemfiles/rails_6_0.gemfile +22 -0
- data/lib/ranked-model.rb +20 -5
- data/lib/ranked-model/ranker.rb +91 -70
- data/lib/ranked-model/version.rb +1 -1
- data/ranked-model.gemspec +9 -8
- data/spec/duck-model/column_default_ducks_spec.rb +29 -0
- data/spec/duck-model/duck_spec.rb +207 -78
- data/spec/duck-model/lots_of_ducks_spec.rb +50 -38
- data/spec/duck-model/wrong_ducks_spec.rb +11 -0
- data/spec/ego-model/ego_spec.rb +3 -3
- data/spec/number-model/number_spec.rb +39 -0
- data/spec/player-model/records_already_exist_spec.rb +1 -1
- data/spec/ranked-model/ranker_spec.rb +18 -0
- data/spec/ranked-model/version_spec.rb +1 -1
- data/spec/spec_helper.rb +7 -0
- data/spec/sti-model/element_spec.rb +24 -24
- data/spec/sti-model/vehicle_spec.rb +6 -6
- data/spec/support/active_record.rb +28 -5
- data/spec/support/database.yml +9 -17
- metadata +67 -44
@@ -0,0 +1,23 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "activerecord", "~> 4.2.0"
|
6
|
+
|
7
|
+
group :sqlite do
|
8
|
+
gem "sqlite3", "~> 1.3.13", platform: :ruby
|
9
|
+
gem "activerecord-jdbcsqlite3-adapter", "~> 1.3.24", platform: :jruby
|
10
|
+
end
|
11
|
+
|
12
|
+
group :postgresql do
|
13
|
+
gem "pg", "~> 0.18.4", platform: :ruby
|
14
|
+
gem "activerecord-jdbcpostgresql-adapter", "~> 1.3.24", platform: :jruby
|
15
|
+
end
|
16
|
+
|
17
|
+
group :mysql do
|
18
|
+
gem "mysql2", "~> 0.4.0", platform: :ruby
|
19
|
+
gem "jdbc-mysql", "~> 5.1.47", platform: :jruby
|
20
|
+
gem "activerecord-jdbcmysql-adapter", "~> 1.3.24", platform: :jruby
|
21
|
+
end
|
22
|
+
|
23
|
+
gemspec path: "../"
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "activerecord", "~> 5.0.0"
|
6
|
+
|
7
|
+
group :sqlite do
|
8
|
+
gem "sqlite3", "~> 1.3.13", platform: :ruby
|
9
|
+
gem "activerecord-jdbcsqlite3-adapter", "~> 50.0", platform: :jruby
|
10
|
+
end
|
11
|
+
|
12
|
+
group :postgresql do
|
13
|
+
gem "pg", "~> 1.2.0", platform: :ruby
|
14
|
+
gem "activerecord-jdbcpostgresql-adapter", "~> 50.0", platform: :jruby
|
15
|
+
end
|
16
|
+
|
17
|
+
group :mysql do
|
18
|
+
gem "mysql2", "~> 0.5.0", platform: :ruby
|
19
|
+
gem "activerecord-jdbcmysql-adapter", "~> 50.0", platform: :jruby
|
20
|
+
end
|
21
|
+
|
22
|
+
gemspec path: "../"
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "activerecord", "~> 5.1.0"
|
6
|
+
|
7
|
+
group :sqlite do
|
8
|
+
gem "sqlite3", "~> 1.3.13", platform: :ruby
|
9
|
+
gem "activerecord-jdbcsqlite3-adapter", "~> 51.0", platform: :jruby
|
10
|
+
end
|
11
|
+
|
12
|
+
group :postgresql do
|
13
|
+
gem "pg", "~> 1.2.0", platform: :ruby
|
14
|
+
gem "activerecord-jdbcpostgresql-adapter", "~> 51.0", platform: :jruby
|
15
|
+
end
|
16
|
+
|
17
|
+
group :mysql do
|
18
|
+
gem "mysql2", "~> 0.5.0", platform: :ruby
|
19
|
+
gem "activerecord-jdbcmysql-adapter", "~> 51.0", platform: :jruby
|
20
|
+
end
|
21
|
+
|
22
|
+
gemspec path: "../"
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "activerecord", "~> 5.2.0"
|
6
|
+
|
7
|
+
group :sqlite do
|
8
|
+
gem "sqlite3", "~> 1.3.13", platform: :ruby
|
9
|
+
gem "activerecord-jdbcsqlite3-adapter", "~> 52.0", platform: :jruby
|
10
|
+
end
|
11
|
+
|
12
|
+
group :postgresql do
|
13
|
+
gem "pg", "~> 1.2.0", platform: :ruby
|
14
|
+
gem "activerecord-jdbcpostgresql-adapter", "~> 52.0", platform: :jruby
|
15
|
+
end
|
16
|
+
|
17
|
+
group :mysql do
|
18
|
+
gem "mysql2", "~> 0.5.0", platform: :ruby
|
19
|
+
gem "activerecord-jdbcmysql-adapter", "~> 52.0", platform: :jruby
|
20
|
+
end
|
21
|
+
|
22
|
+
gemspec path: "../"
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "activerecord", "~> 6.0.0"
|
6
|
+
|
7
|
+
group :sqlite do
|
8
|
+
gem "sqlite3", "~> 1.4", platform: :ruby
|
9
|
+
gem "activerecord-jdbcsqlite3-adapter", "~> 60.0", platform: :jruby
|
10
|
+
end
|
11
|
+
|
12
|
+
group :postgresql do
|
13
|
+
gem "pg", "~> 1.2.0", platform: :ruby
|
14
|
+
gem "activerecord-jdbcpostgresql-adapter", "~> 60.0", platform: :jruby
|
15
|
+
end
|
16
|
+
|
17
|
+
group :mysql do
|
18
|
+
gem "mysql2", "~> 0.5.0", platform: :ruby
|
19
|
+
gem "activerecord-jdbcmysql-adapter", "~> 60.0", platform: :jruby
|
20
|
+
end
|
21
|
+
|
22
|
+
gemspec path: "../"
|
data/lib/ranked-model.rb
CHANGED
@@ -3,10 +3,12 @@ require File.dirname(__FILE__)+'/ranked-model/railtie' if defined?(Rails::Railti
|
|
3
3
|
|
4
4
|
module RankedModel
|
5
5
|
|
6
|
-
|
6
|
+
class NonNilColumnDefault < StandardError; end
|
7
|
+
|
8
|
+
# Signed INT in MySQL
|
7
9
|
#
|
8
|
-
MAX_RANK_VALUE =
|
9
|
-
MIN_RANK_VALUE = -
|
10
|
+
MAX_RANK_VALUE = 2147483647
|
11
|
+
MIN_RANK_VALUE = -2147483648
|
10
12
|
|
11
13
|
def self.included base
|
12
14
|
|
@@ -18,7 +20,7 @@ module RankedModel
|
|
18
20
|
before_save :handle_ranking
|
19
21
|
|
20
22
|
scope :rank, lambda { |name|
|
21
|
-
|
23
|
+
reorder ranker(name.to_sym).column
|
22
24
|
}
|
23
25
|
end
|
24
26
|
|
@@ -40,11 +42,16 @@ module RankedModel
|
|
40
42
|
end
|
41
43
|
end
|
42
44
|
|
43
|
-
|
45
|
+
private
|
44
46
|
|
45
47
|
def ranks *args
|
46
48
|
self.rankers ||= []
|
47
49
|
ranker = RankedModel::Ranker.new(*args)
|
50
|
+
|
51
|
+
if column_default(ranker)
|
52
|
+
raise NonNilColumnDefault, %Q{Your ranked model column "#{ranker.name}" must not have a default value in the database.}
|
53
|
+
end
|
54
|
+
|
48
55
|
self.rankers << ranker
|
49
56
|
attr_reader "#{ranker.name}_position"
|
50
57
|
define_method "#{ranker.name}_position=" do |position|
|
@@ -54,9 +61,17 @@ module RankedModel
|
|
54
61
|
end
|
55
62
|
end
|
56
63
|
|
64
|
+
define_method "#{ranker.name}_rank" do
|
65
|
+
ranker.with(self).relative_rank
|
66
|
+
end
|
67
|
+
|
57
68
|
public "#{ranker.name}_position", "#{ranker.name}_position="
|
58
69
|
end
|
59
70
|
|
71
|
+
def column_default ranker
|
72
|
+
column_defaults[ranker.name.to_s] if ActiveRecord::Base.connected? && table_exists?
|
73
|
+
end
|
74
|
+
|
60
75
|
end
|
61
76
|
|
62
77
|
end
|
data/lib/ranked-model/ranker.rb
CHANGED
@@ -40,11 +40,11 @@ module RankedModel
|
|
40
40
|
when Symbol
|
41
41
|
!instance.respond_to?(ranker.with_same)
|
42
42
|
when Array
|
43
|
-
ranker.with_same.detect {|attr| !instance.respond_to?(attr) }
|
43
|
+
array_element = ranker.with_same.detect {|attr| !instance.respond_to?(attr) }
|
44
44
|
else
|
45
45
|
false
|
46
46
|
end)
|
47
|
-
raise RankedModel::InvalidField, %Q{No field called "#{ranker.with_same}" found in model}
|
47
|
+
raise RankedModel::InvalidField, %Q{No field called "#{array_element || ranker.with_same}" found in model}
|
48
48
|
end
|
49
49
|
end
|
50
50
|
end
|
@@ -66,13 +66,23 @@ module RankedModel
|
|
66
66
|
#
|
67
67
|
instance_class.
|
68
68
|
where(instance_class.primary_key => instance.id).
|
69
|
-
update_all(
|
69
|
+
update_all(ranker.column => value)
|
70
|
+
end
|
71
|
+
|
72
|
+
def reset_ranks!
|
73
|
+
finder.update_all(ranker.column => nil)
|
70
74
|
end
|
71
75
|
|
72
76
|
def position
|
73
77
|
instance.send "#{ranker.name}_position"
|
74
78
|
end
|
75
79
|
|
80
|
+
def relative_rank
|
81
|
+
escaped_column = instance_class.connection.quote_column_name ranker.column
|
82
|
+
|
83
|
+
finder.where("#{escaped_column} < #{rank}").count(:all)
|
84
|
+
end
|
85
|
+
|
76
86
|
def rank
|
77
87
|
instance.send "#{ranker.column}"
|
78
88
|
end
|
@@ -89,6 +99,10 @@ module RankedModel
|
|
89
99
|
|
90
100
|
private
|
91
101
|
|
102
|
+
def reset_cache
|
103
|
+
@finder, @current_order, @current_first, @current_last = nil
|
104
|
+
end
|
105
|
+
|
92
106
|
def instance_class
|
93
107
|
ranker.class_name.nil? ? instance.class : ranker.class_name.constantize
|
94
108
|
end
|
@@ -114,31 +128,31 @@ module RankedModel
|
|
114
128
|
case position
|
115
129
|
when :first, 'first'
|
116
130
|
if current_first && current_first.rank
|
117
|
-
|
131
|
+
rank_at_average current_first.rank, RankedModel::MIN_RANK_VALUE
|
118
132
|
else
|
119
133
|
position_at :middle
|
120
134
|
end
|
121
135
|
when :last, 'last'
|
122
136
|
if current_last && current_last.rank
|
123
|
-
|
137
|
+
rank_at_average current_last.rank, RankedModel::MAX_RANK_VALUE
|
124
138
|
else
|
125
139
|
position_at :middle
|
126
140
|
end
|
127
141
|
when :middle, 'middle'
|
128
|
-
|
142
|
+
rank_at_average RankedModel::MIN_RANK_VALUE, RankedModel::MAX_RANK_VALUE
|
129
143
|
when :down, 'down'
|
130
144
|
neighbors = find_next_two(rank)
|
131
145
|
if neighbors[:lower]
|
132
146
|
min = neighbors[:lower].rank
|
133
147
|
max = neighbors[:upper] ? neighbors[:upper].rank : RankedModel::MAX_RANK_VALUE
|
134
|
-
|
148
|
+
rank_at_average min, max
|
135
149
|
end
|
136
150
|
when :up, 'up'
|
137
151
|
neighbors = find_previous_two(rank)
|
138
152
|
if neighbors[:upper]
|
139
153
|
max = neighbors[:upper].rank
|
140
154
|
min = neighbors[:lower] ? neighbors[:lower].rank : RankedModel::MIN_RANK_VALUE
|
141
|
-
|
155
|
+
rank_at_average min, max
|
142
156
|
end
|
143
157
|
when String
|
144
158
|
position_at position.to_i
|
@@ -148,7 +162,7 @@ module RankedModel
|
|
148
162
|
neighbors = neighbors_at_position(position)
|
149
163
|
min = ((neighbors[:lower] && neighbors[:lower].has_rank?) ? neighbors[:lower].rank : RankedModel::MIN_RANK_VALUE)
|
150
164
|
max = ((neighbors[:upper] && neighbors[:upper].has_rank?) ? neighbors[:upper].rank : RankedModel::MAX_RANK_VALUE)
|
151
|
-
|
165
|
+
rank_at_average min, max
|
152
166
|
when NilClass
|
153
167
|
if !rank
|
154
168
|
position_at :last
|
@@ -156,13 +170,18 @@ module RankedModel
|
|
156
170
|
end
|
157
171
|
end
|
158
172
|
|
173
|
+
def rank_at_average(min, max)
|
174
|
+
if (max - min).between?(-1, 1) # No room at the inn...
|
175
|
+
rebalance_ranks
|
176
|
+
position_at position
|
177
|
+
else
|
178
|
+
rank_at( ( ( max - min ).to_f / 2 ).ceil + min )
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
159
182
|
def assure_unique_position
|
160
183
|
if ( new_record? || rank_changed? )
|
161
|
-
|
162
|
-
rank_at( RankedModel::MAX_RANK_VALUE )
|
163
|
-
end
|
164
|
-
|
165
|
-
if (rank > RankedModel::MAX_RANK_VALUE) || current_at_rank(rank)
|
184
|
+
if (rank > RankedModel::MAX_RANK_VALUE) || rank_taken?
|
166
185
|
rearrange_ranks
|
167
186
|
end
|
168
187
|
end
|
@@ -170,22 +189,25 @@ module RankedModel
|
|
170
189
|
|
171
190
|
def rearrange_ranks
|
172
191
|
_scope = finder
|
173
|
-
|
174
|
-
|
175
|
-
_scope = _scope.where( instance_class.arel_table[instance_class.primary_key].not_eq(instance.id) )
|
176
|
-
end
|
192
|
+
escaped_column = instance_class.connection.quote_column_name ranker.column
|
193
|
+
# If there is room at the bottom of the list and we're added to the very top of the list...
|
177
194
|
if current_first.rank && current_first.rank > RankedModel::MIN_RANK_VALUE && rank == RankedModel::MAX_RANK_VALUE
|
195
|
+
# ...then move everyone else down 1 to make room for us at the end
|
178
196
|
_scope.
|
179
197
|
where( instance_class.arel_table[ranker.column].lteq(rank) ).
|
180
|
-
update_all(
|
198
|
+
update_all( "#{escaped_column} = #{escaped_column} - 1" )
|
199
|
+
# If there is room at the top of the list and we're added below the last value in the list...
|
181
200
|
elsif current_last.rank && current_last.rank < (RankedModel::MAX_RANK_VALUE - 1) && rank < current_last.rank
|
201
|
+
# ...then move everyone else at or above our desired rank up 1 to make room for us
|
182
202
|
_scope.
|
183
203
|
where( instance_class.arel_table[ranker.column].gteq(rank) ).
|
184
|
-
update_all(
|
204
|
+
update_all( "#{escaped_column} = #{escaped_column} + 1" )
|
205
|
+
# If there is room at the bottom of the list and we're added above the lowest value in the list...
|
185
206
|
elsif current_first.rank && current_first.rank > RankedModel::MIN_RANK_VALUE && rank > current_first.rank
|
207
|
+
# ...then move everyone else below us down 1 and change our rank down 1 to avoid the collission
|
186
208
|
_scope.
|
187
209
|
where( instance_class.arel_table[ranker.column].lt(rank) ).
|
188
|
-
update_all(
|
210
|
+
update_all( "#{escaped_column} = #{escaped_column} - 1" )
|
189
211
|
rank_at( rank - 1 )
|
190
212
|
else
|
191
213
|
rebalance_ranks
|
@@ -193,66 +215,70 @@ module RankedModel
|
|
193
215
|
end
|
194
216
|
|
195
217
|
def rebalance_ranks
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
else
|
205
|
-
if !current_order[index] ||
|
206
|
-
( !current_order[index].rank.nil? &&
|
207
|
-
current_order[index].rank >= rank )
|
208
|
-
rank_at rank_value
|
209
|
-
has_set_self = true
|
210
|
-
next
|
218
|
+
ActiveRecord::Base.transaction do
|
219
|
+
if rank && instance.persisted?
|
220
|
+
origin = current_order.index { |item| item.instance.id == instance.id }
|
221
|
+
if origin
|
222
|
+
destination = current_order.index { |item| rank <= item.rank }
|
223
|
+
destination -= 1 if origin < destination
|
224
|
+
|
225
|
+
current_order.insert destination, current_order.delete_at(origin)
|
211
226
|
end
|
212
227
|
end
|
213
|
-
|
228
|
+
|
229
|
+
gaps = current_order.size + 1
|
230
|
+
range = (RankedModel::MAX_RANK_VALUE - RankedModel::MIN_RANK_VALUE).to_f
|
231
|
+
gap_size = (range / gaps).ceil
|
232
|
+
|
233
|
+
reset_ranks!
|
234
|
+
|
235
|
+
current_order.each.with_index(1) do |item, position|
|
236
|
+
new_rank = (gap_size * position) + RankedModel::MIN_RANK_VALUE
|
237
|
+
|
238
|
+
if item.instance.id == instance.id
|
239
|
+
rank_at new_rank
|
240
|
+
else
|
241
|
+
item.update_rank! new_rank
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
reset_cache
|
214
246
|
end
|
215
247
|
end
|
216
248
|
|
217
249
|
def finder(order = :asc)
|
218
|
-
@finder ||=
|
250
|
+
@finder ||= {}
|
251
|
+
@finder[order] ||= begin
|
219
252
|
_finder = instance_class
|
220
|
-
columns = [instance_class.
|
253
|
+
columns = [instance_class.primary_key.to_sym, ranker.column]
|
254
|
+
|
221
255
|
if ranker.scope
|
222
256
|
_finder = _finder.send ranker.scope
|
223
257
|
end
|
258
|
+
|
224
259
|
case ranker.with_same
|
225
|
-
|
226
|
-
|
227
|
-
_finder = _finder.where \
|
228
|
-
instance_class.arel_table[ranker.with_same].eq(instance.attributes["#{ranker.with_same}"])
|
229
|
-
when Array
|
230
|
-
ranker.with_same.each {|c| columns.push instance_class.arel_table[c] }
|
231
|
-
_finder = _finder.where(
|
232
|
-
ranker.with_same[1..-1].inject(
|
233
|
-
instance_class.arel_table[ranker.with_same.first].eq(
|
234
|
-
instance.attributes["#{ranker.with_same.first}"]
|
235
|
-
)
|
236
|
-
) {|scoper, attr|
|
237
|
-
scoper.and(
|
238
|
-
instance_class.arel_table[attr].eq(
|
239
|
-
instance.attributes["#{attr}"]
|
240
|
-
)
|
241
|
-
)
|
242
|
-
}
|
243
|
-
)
|
244
|
-
end
|
245
|
-
if !new_record?
|
260
|
+
when Symbol
|
261
|
+
columns << ranker.with_same
|
246
262
|
_finder = _finder.where \
|
247
|
-
|
263
|
+
ranker.with_same => instance.attributes[ranker.with_same.to_s]
|
264
|
+
when Array
|
265
|
+
ranker.with_same.each do |column|
|
266
|
+
columns << column
|
267
|
+
_finder = _finder.where column => instance.attributes[column.to_s]
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
unless new_record?
|
272
|
+
_finder = _finder.where.not instance_class.primary_key.to_sym => instance.id
|
248
273
|
end
|
249
|
-
|
274
|
+
|
275
|
+
_finder.reorder(ranker.column.to_sym => order).select(columns)
|
250
276
|
end
|
251
277
|
end
|
252
278
|
|
253
279
|
def current_order
|
254
280
|
@current_order ||= begin
|
255
|
-
finder.collect { |ordered_instance|
|
281
|
+
finder.unscope(where: instance_class.primary_key.to_sym).collect { |ordered_instance|
|
256
282
|
RankedModel::Ranker::Mapper.new ranker, ordered_instance
|
257
283
|
}
|
258
284
|
end
|
@@ -276,13 +302,8 @@ module RankedModel
|
|
276
302
|
end
|
277
303
|
end
|
278
304
|
|
279
|
-
def
|
280
|
-
|
281
|
-
except( :order ).
|
282
|
-
where( ranker.column => _rank ).
|
283
|
-
first)
|
284
|
-
RankedModel::Ranker::Mapper.new ranker, ordered_instance
|
285
|
-
end
|
305
|
+
def rank_taken?
|
306
|
+
finder.except(:order).where(ranker.column => rank).exists?
|
286
307
|
end
|
287
308
|
|
288
309
|
def neighbors_at_position _pos
|