ranked-model 0.4.0 → 0.4.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -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