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.
@@ -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
- # Signed MEDIUMINT in MySQL
6
+ class NonNilColumnDefault < StandardError; end
7
+
8
+ # Signed INT in MySQL
7
9
  #
8
- MAX_RANK_VALUE = 8388607
9
- MIN_RANK_VALUE = -8388607
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
- order ranker(name.to_sym).column
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
- private
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
@@ -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([%Q{#{ranker.column} = ?}, value])
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
- rank_at( ( ( RankedModel::MIN_RANK_VALUE - current_first.rank ).to_f / 2 ).ceil + current_first.rank)
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
- rank_at( ( ( RankedModel::MAX_RANK_VALUE - current_last.rank ).to_f / 2 ).ceil + current_last.rank )
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
- rank_at( ( ( RankedModel::MAX_RANK_VALUE - RankedModel::MIN_RANK_VALUE ).to_f / 2 ).ceil + RankedModel::MIN_RANK_VALUE )
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
- rank_at( ( ( max - min ).to_f / 2 ).ceil + min )
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
- rank_at( ( ( max - min ).to_f / 2 ).ceil + min )
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
- rank_at( ( ( max - min ).to_f / 2 ).ceil + min )
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
- unless rank
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
- unless instance.id.nil?
174
- # Never update ourself, shift others around us.
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( %Q{#{ranker.column} = #{ranker.column} - 1} )
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( %Q{#{ranker.column} = #{ranker.column} + 1} )
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( %Q{#{ranker.column} = #{ranker.column} - 1} )
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
- total = current_order.size + 2
197
- has_set_self = false
198
- total.times do |index|
199
- next if index == 0 || index == total
200
- rank_value = ((((RankedModel::MAX_RANK_VALUE - RankedModel::MIN_RANK_VALUE).to_f / total) * index ).ceil + RankedModel::MIN_RANK_VALUE)
201
- index = index - 1
202
- if has_set_self
203
- index = index - 1
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
- current_order[index].update_rank! rank_value
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 ||= begin
250
+ @finder ||= {}
251
+ @finder[order] ||= begin
219
252
  _finder = instance_class
220
- columns = [instance_class.arel_table[instance_class.primary_key], instance_class.arel_table[ranker.column]]
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
- when Symbol
226
- columns << instance_class.arel_table[ranker.with_same]
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
- instance_class.arel_table[instance_class.primary_key].not_eq(instance.id)
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
- _finder.order(instance_class.arel_table[ranker.column].send(order)).select(columns)
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 current_at_rank _rank
280
- if (ordered_instance = finder.
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