ranked-model 0.4.7 → 0.4.8

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8dac9a4acefc5a44cfe74e77931854bb8edd031cc986f3bc0bd8232f543dc0f9
4
- data.tar.gz: 9bd6120114a23ef47ae08581b860cd0cb4fa18779fb237b9b9570d94a889cc75
3
+ metadata.gz: '004680e8b292451d304375f3ae01d0100702db74867d93b2a713e7c908241d78'
4
+ data.tar.gz: cc831e2837ba8ad07c21f6b890959e071005824a4f53bd51c135c8055fb1904f
5
5
  SHA512:
6
- metadata.gz: dc8d0ad9348de7596934a2eab69da78228c3351bd4d16303eae592b3893ecfedc28ff4620997977e8a88dc9d26dc27999c99e6e2abddc7ab6fe1f24aeb108527
7
- data.tar.gz: 03a77db0fc8e9f59b4ffaccf24c966a54125141ab07a931046387e30cf62b7bf0577d9d983d268322dc69a7004c68c001168083d428cffff118832085afe3d21
6
+ metadata.gz: d4494e9eede725ef30ecbd05006a4d91deb89f62aced56b96fa3be2f0f5732f41336d5a3d1c450e3f5d2c0f1821a15de096a1ee68732d8cb5d10a300008270ca
7
+ data.tar.gz: 04fa0d21ff8b2c6533ebc6705cc00c7258bd955c88a91636995d362136de9cf2a57c6e8ca49c43e112757e1209dc9a9fe3c2285b2bfde47639bcab90d2368fd4
data/.travis.yml CHANGED
@@ -26,6 +26,7 @@ gemfile:
26
26
  - gemfiles/rails_5_1.gemfile
27
27
  - gemfiles/rails_5_2.gemfile
28
28
  - gemfiles/rails_6_0.gemfile
29
+ - gemfiles/rails_6_1.gemfile
29
30
  matrix:
30
31
  exclude:
31
32
  # Rails <6 does not support Ruby 3, see:
@@ -49,3 +50,7 @@ matrix:
49
50
  gemfile: gemfiles/rails_6_0.gemfile
50
51
  - rvm: jruby-9.1.17.0
51
52
  gemfile: gemfiles/rails_6_0.gemfile
53
+ - rvm: 2.4
54
+ gemfile: gemfiles/rails_6_1.gemfile
55
+ - rvm: jruby-9.1.17.0
56
+ gemfile: gemfiles/rails_6_1.gemfile
data/Appraisals CHANGED
@@ -70,3 +70,17 @@ appraise "rails-6-0" do
70
70
  end
71
71
  gem "activerecord", "~> 6.0.0"
72
72
  end
73
+
74
+ appraise "rails-6-1" do
75
+ group :sqlite do
76
+ gem "sqlite3", "~> 1.4", platform: :ruby
77
+ gem "activerecord-jdbcsqlite3-adapter", "~> 61.0", platform: :jruby
78
+ end
79
+ group :mysql do
80
+ gem "activerecord-jdbcmysql-adapter", "~> 61.0", platform: :jruby
81
+ end
82
+ group :postgresql do
83
+ gem "activerecord-jdbcpostgresql-adapter", "~> 61.0", platform: :jruby
84
+ end
85
+ gem "activerecord", "~> 6.1.0"
86
+ end
data/Gemfile CHANGED
@@ -4,13 +4,13 @@ source "https://rubygems.org"
4
4
  gemspec
5
5
 
6
6
  group :sqlite do
7
- gem "sqlite3", "~> 1.3.13", platform: :ruby
7
+ gem "sqlite3", platform: :ruby
8
8
  end
9
9
 
10
10
  group :postgresql do
11
- gem "pg", "~> 1.2.0", platform: :ruby
11
+ gem "pg", platform: :ruby
12
12
  end
13
13
 
14
14
  group :mysql do
15
- gem "mysql2", "~> 0.5.0", platform: :ruby
15
+ gem "mysql2", platform: :ruby
16
16
  end
data/Readme.mkd CHANGED
@@ -5,7 +5,7 @@
5
5
  Installation
6
6
  ------------
7
7
 
8
- ranked-model passes specs with Rails 4.2, 5.0, 5.1, 5.2 and 6.0 for MySQL, Postgres, and SQLite on Ruby 2.4 through 3.0 (with exceptions, please check the CI setup for supported combinations), and jruby-9.1.17.0 where Rails supports the platform.
8
+ ranked-model passes specs with Rails 4.2, 5.0, 5.1, 5.2, 6.0 and 6.1 for MySQL, Postgres, and SQLite on Ruby 2.4 through 3.0 (with exceptions, please check the CI setup for supported combinations), and jruby-9.1.17.0 where Rails supports the platform.
9
9
 
10
10
  To install ranked-model, just add it to your `Gemfile`:
11
11
 
@@ -212,7 +212,7 @@ class AddRowOrderToDucks < ActiveRecord::Migration[6.0]
212
212
  Duck.update_all('row_order = EXTRACT(EPOCH FROM created_at)')
213
213
 
214
214
  # Alternatively, implement any other sorting default
215
- # Duck.order(created_at: :desc).desc.each do |duck|
215
+ # Duck.order(created_at: :desc).each do |duck|
216
216
  # duck.update!(row_order: duck.created_at.to_i + duck.age / 2)
217
217
  # end
218
218
  end
@@ -0,0 +1,22 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 6.1.0"
6
+
7
+ group :sqlite do
8
+ gem "sqlite3", "~> 1.4", platform: :ruby
9
+ gem "activerecord-jdbcsqlite3-adapter", "~> 61.0", platform: :jruby
10
+ end
11
+
12
+ group :postgresql do
13
+ gem "pg", "~> 1.2.0", platform: :ruby
14
+ gem "activerecord-jdbcpostgresql-adapter", "~> 61.0", platform: :jruby
15
+ end
16
+
17
+ group :mysql do
18
+ gem "mysql2", "~> 0.5.0", platform: :ruby
19
+ gem "activerecord-jdbcmysql-adapter", "~> 61.0", platform: :jruby
20
+ end
21
+
22
+ gemspec path: "../"
@@ -1,358 +1,356 @@
1
- module RankedModel
2
-
3
- class InvalidScope < StandardError; end
4
- class InvalidField < StandardError; end
5
-
6
- class Ranker
7
- attr_accessor :name, :column, :scope, :with_same, :class_name, :unless
8
-
9
- def initialize name, options={}
10
- self.name = name.to_sym
11
- self.column = options[:column] || name
12
- self.class_name = options[:class_name]
13
-
14
- [ :scope, :with_same, :unless ].each do |key|
15
- self.send "#{key}=", options[key]
16
- end
17
- end
18
-
19
- def with instance
20
- Mapper.new self, instance
21
- end
22
-
23
- class Mapper
24
- attr_accessor :ranker, :instance
25
-
26
- def initialize ranker, instance
27
- self.ranker = ranker
28
- self.instance = instance
29
-
30
- validate_ranker_for_instance!
31
- end
32
-
33
- def validate_ranker_for_instance!
34
- if ranker.scope && !instance_class.respond_to?(ranker.scope)
35
- raise RankedModel::InvalidScope, %Q{No scope called "#{ranker.scope}" found in model}
36
- end
37
-
38
- if ranker.with_same
39
- if (case ranker.with_same
40
- when Symbol
41
- !instance.respond_to?(ranker.with_same)
42
- when Array
43
- array_element = ranker.with_same.detect {|attr| !instance.respond_to?(attr) }
44
- else
45
- false
46
- end)
47
- raise RankedModel::InvalidField, %Q{No field called "#{array_element || ranker.with_same}" found in model}
48
- end
49
- end
50
- end
51
-
52
- def handle_ranking
53
- case ranker.unless
54
- when Proc
55
- return if ranker.unless.call(instance)
56
- when Symbol
57
- return if instance.send(ranker.unless)
58
- end
59
-
60
- update_index_from_position
61
- assure_unique_position
62
- end
63
-
64
- def update_rank! value
65
- # Bypass callbacks
66
- #
67
- instance_class.
68
- where(instance_class.primary_key => instance.id).
69
- update_all(ranker.column => value)
70
- end
71
-
72
- def reset_ranks!
73
- finder.update_all(ranker.column => nil)
74
- end
75
-
76
- def position
77
- instance.send "#{ranker.name}_position"
78
- end
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
-
86
- def rank
87
- instance.send "#{ranker.column}"
88
- end
89
-
90
- def current_at_position _pos
91
- if (ordered_instance = finder.offset(_pos).first)
92
- RankedModel::Ranker::Mapper.new ranker, ordered_instance
93
- end
94
- end
95
-
96
- def has_rank?
97
- !rank.nil?
98
- end
99
-
100
- private
101
-
102
- def reset_cache
103
- @finder, @current_order, @current_first, @current_last = nil
104
- end
105
-
106
- def instance_class
107
- ranker.class_name.nil? ? instance.class : ranker.class_name.constantize
108
- end
109
-
110
- def position_at value
111
- instance.send "#{ranker.name}_position=", value
112
- update_index_from_position
113
- end
114
-
115
- def rank_at value
116
- instance.send "#{ranker.column}=", value
117
- end
118
-
119
- def rank_changed?
120
- instance.send "#{ranker.column}_changed?"
121
- end
122
-
123
- def new_record?
124
- instance.new_record?
125
- end
126
-
127
- def update_index_from_position
128
- case position
129
- when :first, 'first'
130
- if current_first && current_first.rank
131
- rank_at_average current_first.rank, RankedModel::MIN_RANK_VALUE
132
- else
133
- position_at :middle
134
- end
135
- when :last, 'last'
136
- if current_last && current_last.rank
137
- rank_at_average current_last.rank, RankedModel::MAX_RANK_VALUE
138
- else
139
- position_at :middle
140
- end
141
- when :middle, 'middle'
142
- rank_at_average RankedModel::MIN_RANK_VALUE, RankedModel::MAX_RANK_VALUE
143
- when :down, 'down'
144
- neighbors = find_next_two(rank)
145
- if neighbors[:lower]
146
- min = neighbors[:lower].rank
147
- max = neighbors[:upper] ? neighbors[:upper].rank : RankedModel::MAX_RANK_VALUE
148
- rank_at_average min, max
149
- end
150
- when :up, 'up'
151
- neighbors = find_previous_two(rank)
152
- if neighbors[:upper]
153
- max = neighbors[:upper].rank
154
- min = neighbors[:lower] ? neighbors[:lower].rank : RankedModel::MIN_RANK_VALUE
155
- rank_at_average min, max
156
- end
157
- when String
158
- position_at position.to_i
159
- when 0
160
- position_at :first
161
- when Integer
162
- neighbors = neighbors_at_position(position)
163
- min = ((neighbors[:lower] && neighbors[:lower].has_rank?) ? neighbors[:lower].rank : RankedModel::MIN_RANK_VALUE)
164
- max = ((neighbors[:upper] && neighbors[:upper].has_rank?) ? neighbors[:upper].rank : RankedModel::MAX_RANK_VALUE)
165
- rank_at_average min, max
166
- when NilClass
167
- if !rank
168
- position_at :last
169
- end
170
- end
171
- end
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
-
182
- def assure_unique_position
183
- if ( new_record? || rank_changed? )
184
- if (rank > RankedModel::MAX_RANK_VALUE) || rank_taken?
185
- rearrange_ranks
186
- end
187
- end
188
- end
189
-
190
- def rearrange_ranks
191
- _scope = finder
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...
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
196
- _scope.
197
- where( instance_class.arel_table[ranker.column].lteq(rank) ).
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...
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
202
- _scope.
203
- where( instance_class.arel_table[ranker.column].gteq(rank) ).
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...
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
208
- _scope.
209
- where( instance_class.arel_table[ranker.column].lt(rank) ).
210
- update_all( "#{escaped_column} = #{escaped_column} - 1" )
211
- rank_at( rank - 1 )
212
- else
213
- rebalance_ranks
214
- end
215
- end
216
-
217
- def rebalance_ranks
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)
226
- end
227
- end
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
246
- end
247
- end
248
-
249
- def finder(order = :asc)
250
- @finder ||= {}
251
- @finder[order] ||= begin
252
- _finder = instance_class
253
- columns = [instance_class.primary_key.to_sym, ranker.column]
254
-
255
- if ranker.scope
256
- _finder = _finder.send ranker.scope
257
- end
258
-
259
- case ranker.with_same
260
- when Symbol
261
- columns << ranker.with_same
262
- _finder = _finder.where \
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
273
- end
274
-
275
- _finder.reorder(ranker.column.to_sym => order).select(columns)
276
- end
277
- end
278
-
279
- def current_order
280
- @current_order ||= begin
281
- finder.unscope(where: instance_class.primary_key.to_sym).collect { |ordered_instance|
282
- RankedModel::Ranker::Mapper.new ranker, ordered_instance
283
- }
284
- end
285
- end
286
-
287
- def current_first
288
- @current_first ||= begin
289
- if (ordered_instance = finder.first)
290
- RankedModel::Ranker::Mapper.new ranker, ordered_instance
291
- end
292
- end
293
- end
294
-
295
- def current_last
296
- @current_last ||= begin
297
- if (ordered_instance = finder.
298
- reverse.
299
- first)
300
- RankedModel::Ranker::Mapper.new ranker, ordered_instance
301
- end
302
- end
303
- end
304
-
305
- def rank_taken?
306
- finder.except(:order).where(ranker.column => rank).exists?
307
- end
308
-
309
- def neighbors_at_position _pos
310
- if _pos > 0
311
- if (ordered_instances = finder.offset(_pos-1).limit(2).to_a)
312
- if ordered_instances[1]
313
- { :lower => RankedModel::Ranker::Mapper.new( ranker, ordered_instances[0] ),
314
- :upper => RankedModel::Ranker::Mapper.new( ranker, ordered_instances[1] ) }
315
- elsif ordered_instances[0]
316
- { :lower => RankedModel::Ranker::Mapper.new( ranker, ordered_instances[0] ) }
317
- else
318
- { :lower => current_last }
319
- end
320
- end
321
- else
322
- if (ordered_instance = finder.first)
323
- { :upper => RankedModel::Ranker::Mapper.new( ranker, ordered_instance ) }
324
- else
325
- {}
326
- end
327
- end
328
- end
329
-
330
- def find_next_two _rank
331
- ordered_instances = finder.where(instance_class.arel_table[ranker.column].gt _rank).limit(2)
332
- if ordered_instances[1]
333
- { :lower => RankedModel::Ranker::Mapper.new( ranker, ordered_instances[0] ),
334
- :upper => RankedModel::Ranker::Mapper.new( ranker, ordered_instances[1] ) }
335
- elsif ordered_instances[0]
336
- { :lower => RankedModel::Ranker::Mapper.new( ranker, ordered_instances[0] ) }
337
- else
338
- {}
339
- end
340
- end
341
-
342
- def find_previous_two _rank
343
- ordered_instances = finder(:desc).where(instance_class.arel_table[ranker.column].lt _rank).limit(2)
344
- if ordered_instances[1]
345
- { :upper => RankedModel::Ranker::Mapper.new( ranker, ordered_instances[0] ),
346
- :lower => RankedModel::Ranker::Mapper.new( ranker, ordered_instances[1] ) }
347
- elsif ordered_instances[0]
348
- { :upper => RankedModel::Ranker::Mapper.new( ranker, ordered_instances[0] ) }
349
- else
350
- {}
351
- end
352
- end
353
-
354
- end
355
-
356
- end
357
-
358
- end
1
+ module RankedModel
2
+
3
+ class InvalidScope < StandardError; end
4
+ class InvalidField < StandardError; end
5
+
6
+ class Ranker
7
+ attr_accessor :name, :column, :scope, :with_same, :class_name, :unless
8
+
9
+ def initialize name, options={}
10
+ self.name = name.to_sym
11
+ self.column = options[:column] || name
12
+ self.class_name = options[:class_name]
13
+
14
+ [ :scope, :with_same, :unless ].each do |key|
15
+ self.send "#{key}=", options[key]
16
+ end
17
+ end
18
+
19
+ def with instance
20
+ Mapper.new self, instance
21
+ end
22
+
23
+ class Mapper
24
+ attr_accessor :ranker, :instance
25
+
26
+ def initialize ranker, instance
27
+ self.ranker = ranker
28
+ self.instance = instance
29
+
30
+ validate_ranker_for_instance!
31
+ end
32
+
33
+ def validate_ranker_for_instance!
34
+ if ranker.scope && !instance_class.respond_to?(ranker.scope)
35
+ raise RankedModel::InvalidScope, %Q{No scope called "#{ranker.scope}" found in model}
36
+ end
37
+
38
+ if ranker.with_same
39
+ if (case ranker.with_same
40
+ when Symbol
41
+ !instance.respond_to?(ranker.with_same)
42
+ when Array
43
+ array_element = ranker.with_same.detect {|attr| !instance.respond_to?(attr) }
44
+ else
45
+ false
46
+ end)
47
+ raise RankedModel::InvalidField, %Q{No field called "#{array_element || ranker.with_same}" found in model}
48
+ end
49
+ end
50
+ end
51
+
52
+ def handle_ranking
53
+ case ranker.unless
54
+ when Proc
55
+ return if ranker.unless.call(instance)
56
+ when Symbol
57
+ return if instance.send(ranker.unless)
58
+ end
59
+
60
+ update_index_from_position
61
+ assure_unique_position
62
+ end
63
+
64
+ def update_rank! value
65
+ # Bypass callbacks
66
+ #
67
+ instance_class.
68
+ where(instance_class.primary_key => instance.id).
69
+ update_all(ranker.column => value)
70
+ end
71
+
72
+ def reset_ranks!
73
+ finder.update_all(ranker.column => nil)
74
+ end
75
+
76
+ def position
77
+ instance.send "#{ranker.name}_position"
78
+ end
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
+
86
+ def rank
87
+ instance.send "#{ranker.column}"
88
+ end
89
+
90
+ def current_at_position _pos
91
+ if (ordered_instance = finder.offset(_pos).first)
92
+ RankedModel::Ranker::Mapper.new ranker, ordered_instance
93
+ end
94
+ end
95
+
96
+ def has_rank?
97
+ !rank.nil?
98
+ end
99
+
100
+ private
101
+
102
+ def reset_cache
103
+ @finder, @current_order, @current_first, @current_last = nil
104
+ end
105
+
106
+ def instance_class
107
+ ranker.class_name.nil? ? instance.class : ranker.class_name.constantize
108
+ end
109
+
110
+ def position_at value
111
+ instance.send "#{ranker.name}_position=", value
112
+ update_index_from_position
113
+ end
114
+
115
+ def rank_at value
116
+ instance.send "#{ranker.column}=", value
117
+ end
118
+
119
+ def rank_changed?
120
+ instance.send "#{ranker.column}_changed?"
121
+ end
122
+
123
+ def new_record?
124
+ instance.new_record?
125
+ end
126
+
127
+ def update_index_from_position
128
+ case position
129
+ when :first, 'first'
130
+ if current_first && current_first.rank
131
+ rank_at_average current_first.rank, RankedModel::MIN_RANK_VALUE
132
+ else
133
+ position_at :middle
134
+ end
135
+ when :last, 'last'
136
+ if current_last && current_last.rank
137
+ rank_at_average current_last.rank, RankedModel::MAX_RANK_VALUE
138
+ else
139
+ position_at :middle
140
+ end
141
+ when :middle, 'middle'
142
+ rank_at_average RankedModel::MIN_RANK_VALUE, RankedModel::MAX_RANK_VALUE
143
+ when :down, 'down'
144
+ neighbors = find_next_two(rank)
145
+ if neighbors[:lower]
146
+ min = neighbors[:lower].rank
147
+ max = neighbors[:upper] ? neighbors[:upper].rank : RankedModel::MAX_RANK_VALUE
148
+ rank_at_average min, max
149
+ end
150
+ when :up, 'up'
151
+ neighbors = find_previous_two(rank)
152
+ if neighbors[:upper]
153
+ max = neighbors[:upper].rank
154
+ min = neighbors[:lower] ? neighbors[:lower].rank : RankedModel::MIN_RANK_VALUE
155
+ rank_at_average min, max
156
+ end
157
+ when String
158
+ position_at position.to_i
159
+ when 0
160
+ position_at :first
161
+ when Integer
162
+ neighbors = neighbors_at_position(position)
163
+ min = ((neighbors[:lower] && neighbors[:lower].has_rank?) ? neighbors[:lower].rank : RankedModel::MIN_RANK_VALUE)
164
+ max = ((neighbors[:upper] && neighbors[:upper].has_rank?) ? neighbors[:upper].rank : RankedModel::MAX_RANK_VALUE)
165
+ rank_at_average min, max
166
+ when NilClass
167
+ if !rank
168
+ position_at :last
169
+ end
170
+ end
171
+ end
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
+
182
+ def assure_unique_position
183
+ if ( new_record? || rank_changed? )
184
+ if (rank > RankedModel::MAX_RANK_VALUE) || rank_taken?
185
+ rearrange_ranks
186
+ end
187
+ end
188
+ end
189
+
190
+ def rearrange_ranks
191
+ _scope = finder
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...
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
196
+ _scope.
197
+ where( instance_class.arel_table[ranker.column].lteq(rank) ).
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...
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
202
+ _scope.
203
+ where( instance_class.arel_table[ranker.column].gteq(rank) ).
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...
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
208
+ _scope.
209
+ where( instance_class.arel_table[ranker.column].lt(rank) ).
210
+ update_all( "#{escaped_column} = #{escaped_column} - 1" )
211
+ rank_at( rank - 1 )
212
+ else
213
+ rebalance_ranks
214
+ end
215
+ end
216
+
217
+ def rebalance_ranks
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)
226
+ end
227
+ end
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
246
+ end
247
+ end
248
+
249
+ def finder(order = :asc)
250
+ @finder ||= {}
251
+ @finder[order] ||= begin
252
+ _finder = instance_class
253
+ columns = [instance_class.primary_key.to_sym, ranker.column]
254
+
255
+ if ranker.scope
256
+ _finder = _finder.send ranker.scope
257
+ end
258
+
259
+ case ranker.with_same
260
+ when Symbol
261
+ columns << ranker.with_same
262
+ _finder = _finder.where \
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
273
+ end
274
+
275
+ _finder.reorder(ranker.column.to_sym => order).select(columns)
276
+ end
277
+ end
278
+
279
+ def current_order
280
+ @current_order ||= begin
281
+ finder.unscope(where: instance_class.primary_key.to_sym).collect { |ordered_instance|
282
+ RankedModel::Ranker::Mapper.new ranker, ordered_instance
283
+ }
284
+ end
285
+ end
286
+
287
+ def current_first
288
+ @current_first ||= begin
289
+ if (ordered_instance = finder.first)
290
+ RankedModel::Ranker::Mapper.new ranker, ordered_instance
291
+ end
292
+ end
293
+ end
294
+
295
+ def current_last
296
+ @current_last ||= begin
297
+ if (ordered_instance = finder.last)
298
+ RankedModel::Ranker::Mapper.new ranker, ordered_instance
299
+ end
300
+ end
301
+ end
302
+
303
+ def rank_taken?
304
+ finder.except(:order).where(ranker.column => rank).exists?
305
+ end
306
+
307
+ def neighbors_at_position _pos
308
+ if _pos > 0
309
+ if (ordered_instances = finder.offset(_pos-1).limit(2).to_a)
310
+ if ordered_instances[1]
311
+ { :lower => RankedModel::Ranker::Mapper.new( ranker, ordered_instances[0] ),
312
+ :upper => RankedModel::Ranker::Mapper.new( ranker, ordered_instances[1] ) }
313
+ elsif ordered_instances[0]
314
+ { :lower => RankedModel::Ranker::Mapper.new( ranker, ordered_instances[0] ) }
315
+ else
316
+ { :lower => current_last }
317
+ end
318
+ end
319
+ else
320
+ if (ordered_instance = finder.first)
321
+ { :upper => RankedModel::Ranker::Mapper.new( ranker, ordered_instance ) }
322
+ else
323
+ {}
324
+ end
325
+ end
326
+ end
327
+
328
+ def find_next_two _rank
329
+ ordered_instances = finder.where(instance_class.arel_table[ranker.column].gt _rank).limit(2)
330
+ if ordered_instances[1]
331
+ { :lower => RankedModel::Ranker::Mapper.new( ranker, ordered_instances[0] ),
332
+ :upper => RankedModel::Ranker::Mapper.new( ranker, ordered_instances[1] ) }
333
+ elsif ordered_instances[0]
334
+ { :lower => RankedModel::Ranker::Mapper.new( ranker, ordered_instances[0] ) }
335
+ else
336
+ {}
337
+ end
338
+ end
339
+
340
+ def find_previous_two _rank
341
+ ordered_instances = finder(:desc).where(instance_class.arel_table[ranker.column].lt _rank).limit(2)
342
+ if ordered_instances[1]
343
+ { :upper => RankedModel::Ranker::Mapper.new( ranker, ordered_instances[0] ),
344
+ :lower => RankedModel::Ranker::Mapper.new( ranker, ordered_instances[1] ) }
345
+ elsif ordered_instances[0]
346
+ { :upper => RankedModel::Ranker::Mapper.new( ranker, ordered_instances[0] ) }
347
+ else
348
+ {}
349
+ end
350
+ end
351
+
352
+ end
353
+
354
+ end
355
+
356
+ end
@@ -1,3 +1,3 @@
1
1
  module RankedModel
2
- VERSION = "0.4.7"
2
+ VERSION = "0.4.8"
3
3
  end
data/ranked-model.gemspec CHANGED
@@ -18,7 +18,7 @@ Gem::Specification.new do |s|
18
18
  s.add_development_dependency "rspec-its"
19
19
  s.add_development_dependency "mocha"
20
20
  s.add_development_dependency "database_cleaner", "~> 1.7.0"
21
- s.add_development_dependency "rake", "~> 10.1.0"
21
+ s.add_development_dependency "rake", ">= 12.3.3"
22
22
  s.add_development_dependency "appraisal"
23
23
  s.add_development_dependency "pry"
24
24
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ranked-model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.7
4
+ version: 0.4.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew Beale
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-01-16 00:00:00.000000000 Z
11
+ date: 2022-02-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -84,16 +84,16 @@ dependencies:
84
84
  name: rake
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - "~>"
87
+ - - ">="
88
88
  - !ruby/object:Gem::Version
89
- version: 10.1.0
89
+ version: 12.3.3
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
- - - "~>"
94
+ - - ">="
95
95
  - !ruby/object:Gem::Version
96
- version: 10.1.0
96
+ version: 12.3.3
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: appraisal
99
99
  requirement: !ruby/object:Gem::Requirement
@@ -143,6 +143,7 @@ files:
143
143
  - gemfiles/rails_5_1.gemfile
144
144
  - gemfiles/rails_5_2.gemfile
145
145
  - gemfiles/rails_6_0.gemfile
146
+ - gemfiles/rails_6_1.gemfile
146
147
  - lib/ranked-model.rb
147
148
  - lib/ranked-model/railtie.rb
148
149
  - lib/ranked-model/ranker.rb
@@ -168,7 +169,7 @@ homepage: https://github.com/mixonic/ranked-model
168
169
  licenses:
169
170
  - MIT
170
171
  metadata: {}
171
- post_install_message:
172
+ post_install_message:
172
173
  rdoc_options: []
173
174
  require_paths:
174
175
  - lib
@@ -183,8 +184,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
183
184
  - !ruby/object:Gem::Version
184
185
  version: '0'
185
186
  requirements: []
186
- rubygems_version: 3.0.3
187
- signing_key:
187
+ rubygems_version: 3.2.32
188
+ signing_key:
188
189
  specification_version: 4
189
190
  summary: An acts_as_sortable replacement built for Rails 4.2+
190
191
  test_files: