acts_as_list 0.9.17 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -1,7 +1,5 @@
1
1
  source "http://rubygems.org"
2
2
 
3
- gem "rack", "~> 1", platforms: [:ruby_19, :ruby_20, :ruby_21]
4
-
5
3
  gemspec
6
4
 
7
5
  gem "rake"
@@ -13,15 +11,18 @@ end
13
11
 
14
12
  group :test do
15
13
  gem "minitest", "~> 5.0"
16
- gem "test_after_commit", "~> 0.4.2"
17
14
  gem "timecop"
18
15
  gem "mocha"
19
16
  end
20
17
 
21
18
  group :sqlite do
22
- gem "sqlite3", platforms: [:ruby]
19
+ gem "sqlite3", "~> 1.3.13"
23
20
  end
24
21
 
25
22
  group :postgresql do
26
- gem "pg", "~> 0.18.0", platforms: [:ruby]
23
+ gem "pg", "~> 1.1.4"
24
+ end
25
+
26
+ group :mysql do
27
+ gem "mysql2", "~> 0.5.0"
27
28
  end
data/README.md CHANGED
@@ -1,7 +1,8 @@
1
- # ActsAsList
1
+ # Acts As List
2
2
 
3
3
  ## Build Status
4
- [![Build Status](https://secure.travis-ci.org/swanandp/acts_as_list.png)](https://secure.travis-ci.org/swanandp/acts_as_list)
4
+ [![Build Status](https://travis-ci.org/brendon/acts_as_list.svg?branch=master)](https://travis-ci.org/brendon/acts_as_list)
5
+ [![Gem Version](https://badge.fury.io/rb/acts_as_list.svg)](https://badge.fury.io/rb/acts_as_list)
5
6
 
6
7
  ## Description
7
8
 
@@ -11,8 +12,8 @@ This `acts_as` extension provides the capabilities for sorting and reordering a
11
12
 
12
13
  There are a couple of changes of behaviour from `0.8.0` onwards:
13
14
 
14
- - If you specify `add_new_at: :top`, new items will be added to the top of the list like always. But now, if you specify a position at insert time: `.create(position: 3)`, the position will be respected. In this example, the item will end up at position `3` and will move other items further down the list. Before `0.8.0` the position would be ignored and the item would still be added to the top of the list. [#220](https://github.com/swanandp/acts_as_list/pull/220)
15
- - `acts_as_list` now copes with disparate position integers (i.e. gaps between the numbers). There has been a change in behaviour for the `higher_items` method. It now returns items with the first item in the collection being the closest item to the reference item, and the last item in the collection being the furthest from the reference item (a.k.a. the first item in the list). [#223](https://github.com/swanandp/acts_as_list/pull/223)
15
+ - If you specify `add_new_at: :top`, new items will be added to the top of the list like always. But now, if you specify a position at insert time: `.create(position: 3)`, the position will be respected. In this example, the item will end up at position `3` and will move other items further down the list. Before `0.8.0` the position would be ignored and the item would still be added to the top of the list. [#220](https://github.com/brendon/acts_as_list/pull/220)
16
+ - `acts_as_list` now copes with disparate position integers (i.e. gaps between the numbers). There has been a change in behaviour for the `higher_items` method. It now returns items with the first item in the collection being the closest item to the reference item, and the last item in the collection being the furthest from the reference item (a.k.a. the first item in the list). [#223](https://github.com/brendon/acts_as_list/pull/223)
16
17
 
17
18
  ## Installation
18
19
 
@@ -104,6 +105,25 @@ TodoList.all.each do |todo_list|
104
105
  end
105
106
  ```
106
107
 
108
+ When using PostgreSQL, it is also possible to leave this migration up to the database layer. Inside of the `change` block you could write:
109
+
110
+ ```ruby
111
+ execute <<~SQL.squish
112
+ UPDATE todo_items
113
+ SET position = mapping.new_position
114
+ FROM (
115
+ SELECT
116
+ id,
117
+ ROW_NUMBER() OVER (
118
+ PARTITION BY todo_list_id
119
+ ORDER BY updated_at
120
+ ) as new_position
121
+ FROM todo_items
122
+ ) AS mapping
123
+ WHERE todo_items.id = mapping.id;
124
+ SQL
125
+ ```
126
+
107
127
  ## Notes
108
128
  All `position` queries (select, update, etc.) inside gem methods are executed without the default scope (i.e. `Model.unscoped`), this will prevent nasty issues when the default scope is different from `acts_as_list` scope.
109
129
 
@@ -141,6 +161,10 @@ default: `position`. Use this option if the column name in your database is diff
141
161
  default: `1`. Use this option to define the top of the list. Use 0 to make the collection act more like an array in its indexing.
142
162
  - `add_new_at`
143
163
  default: `:bottom`. Use this option to specify whether objects get added to the `:top` or `:bottom` of the list. `nil` will result in new items not being added to the list on create, i.e, position will be kept nil after create.
164
+ - `touch_on_update`
165
+ default: `true`. Use `touch_on_update: false` if you don't want to update the timestamps of the associated records.
166
+ - `sequential_updates`
167
+ Specifies whether insert_at should update objects positions during shuffling one by one to respect position column unique not null constraint. Defaults to true if position column has unique index, otherwise false. If constraint is `deferrable initially deferred` (PostgreSQL), overriding it with false will speed up insert_at.
144
168
 
145
169
  ## Disabling temporarily
146
170
 
@@ -180,15 +204,89 @@ TodoItem.acts_as_list_no_update([TodoAttachment]) do
180
204
  end
181
205
  ```
182
206
 
207
+ ## Troubleshooting Database Deadlock Errors
208
+ When using this gem in an app with a high amount of concurrency, you may see "deadlock" errors raised by your database server.
209
+ It's difficult for the gem to provide a solution that fits every app.
210
+ Here are some steps you can take to mitigate and handle these kinds of errors.
211
+
212
+ ### 1) Use the Most Concise API
213
+ One easy way to reduce deadlocks is to use the most concise gem API available for what you want to accomplish.
214
+ In this specific example, the more concise API for creating a list item at a position results in one transaction instead of two,
215
+ and it issues fewer SQL statements. Issuing fewer statements tends to lead to faster transactions.
216
+ Faster transactions are less likely to deadlock.
217
+
218
+ Example:
219
+ ```ruby
220
+ # Good
221
+ TodoItem.create(todo_list: todo_list, position: 1)
222
+
223
+ # Bad
224
+ item = TodoItem.create(todo_list: todo_list)
225
+ item.insert_at(1)
226
+ ```
227
+
228
+ ### 2) Rescue then Retry
229
+ Deadlocks are always a possibility when updating tables rows concurrently.
230
+ The general advice from MySQL documentation is to catch these errors and simply retry the transaction; it will probably succeed on another attempt. (see [How to Minimize and Handle Deadlocks](https://dev.mysql.com/doc/refman/8.0/en/innodb-deadlocks-handling.html))
231
+ Retrying transactions sounds simple, but there are many details that need to be chosen on a per-app basis:
232
+ How many retry attempts should be made?
233
+ Should there be a wait time between attempts?
234
+ What _other_ statements were in the transaction that got rolled back?
235
+
236
+ Here a simple example of rescuing from deadlock and retrying the operation:
237
+ * `ActiveRecord::Deadlocked` is available in Rails >= 5.1.0.
238
+ * If you have Rails < 5.1.0, you will need to rescue `ActiveRecord::StatementInvalid` and check `#cause`.
239
+ ```ruby
240
+ attempts_left = 2
241
+ while attempts_left > 0
242
+ attempts_left -= 1
243
+ begin
244
+ TodoItem.transaction do
245
+ TodoItem.create(todo_list: todo_list, position: 1)
246
+ end
247
+ attempts_left = 0
248
+ rescue ActiveRecord::Deadlocked
249
+ raise unless attempts_left > 0
250
+ end
251
+ end
252
+ ```
253
+
254
+ You can also use the approach suggested in this StackOverflow post:
255
+ https://stackoverflow.com/questions/4027659/activerecord3-deadlock-retry
256
+
257
+ ### 3) Lock Parent Record
258
+ In addition to reacting to deadlocks, it is possible to reduce their frequency with more pessimistic locking.
259
+ This approach uses the parent record as a mutex for the entire list.
260
+ This kind of locking is very effective at reducing the frequency of deadlocks while updating list items.
261
+ However, there are some things to keep in mind:
262
+ * This locking pattern needs to be used around *every* call that modifies the list; even if it does not reorder list items.
263
+ * This locking pattern effectively serializes operations on the list. The throughput of operations on the list will decrease.
264
+ * Locking the parent record may lead to deadlock elsewhere if some other code also locks the parent table.
265
+
266
+ Example:
267
+ ```ruby
268
+ todo_list = TodoList.create(name: "The List")
269
+ todo_list.with_lock do
270
+ item = TodoItem.create(description: "Buy Groceries", todo_list: todo_list, position: 1)
271
+ end
272
+ ```
273
+
183
274
  ## Versions
184
- Version `0.9.0` adds `acts_as_list_no_update` (https://github.com/swanandp/acts_as_list/pull/244) and compatibility with not-null and uniqueness constraints on the database (https://github.com/swanandp/acts_as_list/pull/246). These additions shouldn't break compatibility with existing implementations.
275
+ Version `0.9.0` adds `acts_as_list_no_update` (https://github.com/brendon/acts_as_list/pull/244) and compatibility with not-null and uniqueness constraints on the database (https://github.com/brendon/acts_as_list/pull/246). These additions shouldn't break compatibility with existing implementations.
185
276
 
186
277
  As of version `0.7.5` Rails 5 is supported.
187
278
 
188
279
  All versions `0.1.5` onwards require Rails 3.0.x and higher.
189
280
 
190
- ## Workflow Status
191
- [![WIP Issues](https://badge.waffle.io/swanandp/acts_as_list.png)](http://waffle.io/swanandp/acts_as_list)
281
+ ## A note about data integrity
282
+
283
+ We often hear complaints that `position` values are repeated, incorrect etc. For example, #254. To ensure data integrity, you should rely on your database. There are two things you can do:
284
+
285
+ 1. Use constraints. If you model `Item` that `belongs_to` an `Order`, and it has a `position` column, then add a unique constraint on `items` with `[:order_id, :position]`. Think of it as a list invariant. What are the properties of your list that don't change no matter how many items you have in it? One such propery is that each item has a distinct position. Another _could be_ that position is always greater than 0. It is strongly recommended that you rely on your database to enforce these invariants or constraints. Here are the docs for [PostgreSQL](https://www.postgresql.org/docs/9.5/static/ddl-constraints.html) and [MySQL](https://dev.mysql.com/doc/refman/8.0/en/alter-table.html).
286
+ 2. Use mutexes or row level locks. At its heart the duplicate problem is that of handling concurrency. Adding a contention resolution mechanism like locks will solve it to some extent. But it is not a solution or replacement for constraints. Locks are also prone to deadlocks.
287
+
288
+ As a library, `acts_as_list` may not always have all the context needed to apply these tools. They are much better suited at the application level.
289
+
192
290
 
193
291
  ## Roadmap
194
292
 
data/Rakefile CHANGED
@@ -38,5 +38,5 @@ end
38
38
  require 'github_changelog_generator/task'
39
39
  GitHubChangelogGenerator::RakeTask.new :changelog do |config|
40
40
  config.project = 'acts_as_list'
41
- config.user = 'swanandp'
41
+ config.user = 'brendon'
42
42
  end
@@ -7,23 +7,28 @@ Gem::Specification.new do |s|
7
7
  s.name = "acts_as_list"
8
8
  s.version = ActiveRecord::Acts::List::VERSION
9
9
  s.platform = Gem::Platform::RUBY
10
- s.authors = ["David Heinemeier Hansson", "Swanand Pagnis", "Quinn Chaffee"]
11
- s.email = ["swanand.pagnis@gmail.com"]
12
- s.homepage = "http://github.com/swanandp/acts_as_list"
10
+ s.authors = ["Swanand Pagnis", "Brendon Muir"]
11
+ s.email = %w(swanand.pagnis@gmail.com brendon@spikeatschool.co.nz)
12
+ s.homepage = "http://github.com/brendon/acts_as_list"
13
13
  s.summary = "A gem adding sorting, reordering capabilities to an active_record model, allowing it to act as a list"
14
14
  s.description = 'This "acts_as" extension provides the capabilities for sorting and reordering a number of objects in a list. The class that has this specified needs to have a "position" column defined as an integer on the mapped database table.'
15
15
  s.license = "MIT"
16
- s.rubyforge_project = "acts_as_list"
17
- s.required_ruby_version = ">= 1.9.2"
16
+ s.required_ruby_version = ">= 2.4.7"
17
+
18
+ if s.respond_to?(:metadata)
19
+ s.metadata['changelog_uri'] = 'https://github.com/brendon/acts_as_list/blob/master/CHANGELOG.md'
20
+ s.metadata['source_code_uri'] = 'https://github.com/brendon/acts_as_list'
21
+ s.metadata['bug_tracker_uri'] = 'https://github.com/brendon/acts_as_list/issues'
22
+ end
18
23
 
19
24
  # Load Paths...
20
- s.files = `git ls-files`.split("\n")
21
- s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
22
- s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
23
- s.require_paths = ["lib"]
25
+ s.files = `git ls-files`.split("\n")
26
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
27
+ s.executables = `git ls-files -- bin/*`.split("\n").map {|f| File.basename(f)}
28
+ s.require_paths = ["lib"]
24
29
 
25
30
 
26
31
  # Dependencies (installed via "bundle install")
27
- s.add_dependency "activerecord", ">= 3.0"
32
+ s.add_dependency "activerecord", ">= 4.2"
28
33
  s.add_development_dependency "bundler", ">= 1.0.0"
29
34
  end
@@ -2,10 +2,9 @@
2
2
 
3
3
  source "http://rubygems.org"
4
4
 
5
- gem "rack", "~> 1", platforms: [:ruby_19, :ruby_20, :ruby_21]
6
5
  gem "rake"
7
6
  gem "appraisal"
8
- gem "activerecord", "~> 4.2.10"
7
+ gem "activerecord", "~> 4.2.0"
9
8
 
10
9
  group :development do
11
10
  gem "github_changelog_generator", "1.9.0"
@@ -13,21 +12,21 @@ end
13
12
 
14
13
  group :test do
15
14
  gem "minitest", "~> 5.0"
16
- gem "test_after_commit", "~> 0.4.2"
17
15
  gem "timecop"
18
16
  gem "mocha"
17
+ gem "test_after_commit", "~> 0.4.2"
19
18
  end
20
19
 
21
20
  group :sqlite do
22
- gem "sqlite3", platforms: [:ruby]
21
+ gem "sqlite3", "~> 1.3.13"
23
22
  end
24
23
 
25
24
  group :postgresql do
26
- gem "pg", "~> 0.18.0", platforms: [:ruby]
25
+ gem "pg", "~> 0.18.4"
27
26
  end
28
27
 
29
28
  group :mysql do
30
- gem "mysql2", "~> 0.4.10", platforms: [:ruby]
29
+ gem "mysql2", "~> 0.4.0"
31
30
  end
32
31
 
33
32
  gemspec path: "../"
@@ -2,10 +2,9 @@
2
2
 
3
3
  source "http://rubygems.org"
4
4
 
5
- gem "rack", "~> 1", platforms: [:ruby_19, :ruby_20, :ruby_21]
6
5
  gem "rake"
7
6
  gem "appraisal"
8
- gem "activerecord", "~> 5.0.6"
7
+ gem "activerecord", "~> 5.0.0"
9
8
 
10
9
  group :development do
11
10
  gem "github_changelog_generator", "1.9.0"
@@ -13,21 +12,20 @@ end
13
12
 
14
13
  group :test do
15
14
  gem "minitest", "~> 5.0"
16
- gem "test_after_commit", "~> 0.4.2"
17
15
  gem "timecop"
18
16
  gem "mocha"
19
17
  end
20
18
 
21
19
  group :sqlite do
22
- gem "sqlite3", platforms: [:ruby]
20
+ gem "sqlite3", "~> 1.3.13"
23
21
  end
24
22
 
25
23
  group :postgresql do
26
- gem "pg", "~> 0.18.0", platforms: [:ruby]
24
+ gem "pg", "~> 1.1.4"
27
25
  end
28
26
 
29
27
  group :mysql do
30
- gem "mysql2", "~> 0.4.10", platforms: [:ruby]
28
+ gem "mysql2", "~> 0.5.0"
31
29
  end
32
30
 
33
31
  gemspec path: "../"
@@ -2,10 +2,9 @@
2
2
 
3
3
  source "http://rubygems.org"
4
4
 
5
- gem "rack", "~> 1", platforms: [:ruby_19, :ruby_20, :ruby_21]
6
5
  gem "rake"
7
6
  gem "appraisal"
8
- gem "activerecord", "~> 5.1.4"
7
+ gem "activerecord", "~> 5.1.0"
9
8
 
10
9
  group :development do
11
10
  gem "github_changelog_generator", "1.9.0"
@@ -13,21 +12,20 @@ end
13
12
 
14
13
  group :test do
15
14
  gem "minitest", "~> 5.0"
16
- gem "test_after_commit", "~> 0.4.2"
17
15
  gem "timecop"
18
16
  gem "mocha"
19
17
  end
20
18
 
21
19
  group :sqlite do
22
- gem "sqlite3", platforms: [:ruby]
20
+ gem "sqlite3", "~> 1.3.13"
23
21
  end
24
22
 
25
23
  group :postgresql do
26
- gem "pg", "~> 0.18.0", platforms: [:ruby]
24
+ gem "pg", "~> 1.1.4"
27
25
  end
28
26
 
29
27
  group :mysql do
30
- gem "mysql2", "~> 0.4.10", platforms: [:ruby]
28
+ gem "mysql2", "~> 0.5.0"
31
29
  end
32
30
 
33
31
  gemspec path: "../"
@@ -2,10 +2,9 @@
2
2
 
3
3
  source "http://rubygems.org"
4
4
 
5
- gem "rack", "~> 1", platforms: [:ruby_19, :ruby_20, :ruby_21]
6
5
  gem "rake"
7
6
  gem "appraisal"
8
- gem "activerecord", "~> 5.2.0.rc1"
7
+ gem "activerecord", "~> 5.2.0"
9
8
 
10
9
  group :development do
11
10
  gem "github_changelog_generator", "1.9.0"
@@ -13,21 +12,20 @@ end
13
12
 
14
13
  group :test do
15
14
  gem "minitest", "~> 5.0"
16
- gem "test_after_commit", "~> 0.4.2"
17
15
  gem "timecop"
18
16
  gem "mocha"
19
17
  end
20
18
 
21
19
  group :sqlite do
22
- gem "sqlite3", platforms: [:ruby]
20
+ gem "sqlite3", "~> 1.3.13"
23
21
  end
24
22
 
25
23
  group :postgresql do
26
- gem "pg", "~> 0.18.0", platforms: [:ruby]
24
+ gem "pg", "~> 1.1.4"
27
25
  end
28
26
 
29
27
  group :mysql do
30
- gem "mysql2", "~> 0.4.10", platforms: [:ruby]
28
+ gem "mysql2", "~> 0.5.0"
31
29
  end
32
30
 
33
31
  gemspec path: "../"
@@ -2,10 +2,9 @@
2
2
 
3
3
  source "http://rubygems.org"
4
4
 
5
- gem "rack", "~> 1", platforms: [:ruby_19, :ruby_20, :ruby_21]
6
5
  gem "rake"
7
6
  gem "appraisal"
8
- gem "activerecord", "~> 4.1.16"
7
+ gem "activerecord", "~> 6.0.0"
9
8
 
10
9
  group :development do
11
10
  gem "github_changelog_generator", "1.9.0"
@@ -13,22 +12,20 @@ end
13
12
 
14
13
  group :test do
15
14
  gem "minitest", "~> 5.0"
16
- gem "test_after_commit", "~> 0.4.2"
17
15
  gem "timecop"
18
16
  gem "mocha"
19
- gem "after_commit_exception_notification"
20
17
  end
21
18
 
22
19
  group :sqlite do
23
- gem "sqlite3", platforms: [:ruby]
20
+ gem "sqlite3", "~> 1.4"
24
21
  end
25
22
 
26
23
  group :postgresql do
27
- gem "pg", "~> 0.18.0", platforms: [:ruby]
24
+ gem "pg", "~> 1.1.4"
28
25
  end
29
26
 
30
27
  group :mysql do
31
- gem "mysql2", "~> 0.3.21", platforms: [:ruby]
28
+ gem "mysql2", "~> 0.5.0"
32
29
  end
33
30
 
34
31
  gemspec path: "../"
@@ -20,13 +20,14 @@ module ActiveRecord
20
20
  # one by one to respect position column unique not null constraint.
21
21
  # Defaults to true if position column has unique index, otherwise false.
22
22
  # If constraint is <tt>deferrable initially deferred<tt>, overriding it with false will speed up insert_at.
23
+ # * +touch_on_update+ - configuration to disable the update of the model timestamps when the positions are updated.
23
24
  def acts_as_list(options = {})
24
- configuration = { column: "position", scope: "1 = 1", top_of_list: 1, add_new_at: :bottom }
25
+ configuration = { column: "position", scope: "1 = 1", top_of_list: 1, add_new_at: :bottom, touch_on_update: true }
25
26
  configuration.update(options) if options.is_a?(Hash)
26
27
 
27
28
  caller_class = self
28
29
 
29
- ActiveRecord::Acts::List::PositionColumnMethodDefiner.call(caller_class, configuration[:column])
30
+ ActiveRecord::Acts::List::PositionColumnMethodDefiner.call(caller_class, configuration[:column], configuration[:touch_on_update])
30
31
  ActiveRecord::Acts::List::ScopeMethodDefiner.call(caller_class, configuration[:scope])
31
32
  ActiveRecord::Acts::List::TopOfListMethodDefiner.call(caller_class, configuration[:top_of_list])
32
33
  ActiveRecord::Acts::List::AddNewAtMethodDefiner.call(caller_class, configuration[:add_new_at])
@@ -64,6 +65,12 @@ module ActiveRecord
64
65
  end
65
66
 
66
67
  module InstanceMethods
68
+ # Get the current position of the item in the list
69
+ def current_position
70
+ position = send(position_column)
71
+ position ? position.to_i : nil
72
+ end
73
+
67
74
  # Insert the item at the given position (defaults to the top position of 1).
68
75
  def insert_at(position = acts_as_list_top)
69
76
  insert_at_position(position)
@@ -78,8 +85,8 @@ module ActiveRecord
78
85
  return unless lower_item
79
86
 
80
87
  acts_as_list_class.transaction do
81
- if lower_item.send(position_column) != self.send(position_column)
82
- swap_positions(lower_item, self)
88
+ if lower_item.current_position != current_position
89
+ swap_positions_with(lower_item)
83
90
  else
84
91
  lower_item.decrement_position
85
92
  increment_position
@@ -92,8 +99,8 @@ module ActiveRecord
92
99
  return unless higher_item
93
100
 
94
101
  acts_as_list_class.transaction do
95
- if higher_item.send(position_column) != self.send(position_column)
96
- swap_positions(higher_item, self)
102
+ if higher_item.current_position != current_position
103
+ swap_positions_with(higher_item)
97
104
  else
98
105
  higher_item.increment_position
99
106
  decrement_position
@@ -133,13 +140,13 @@ module ActiveRecord
133
140
  # Increase the position of this item without adjusting the rest of the list.
134
141
  def increment_position
135
142
  return unless in_list?
136
- set_list_position(self.send(position_column).to_i + 1)
143
+ set_list_position(current_position + 1)
137
144
  end
138
145
 
139
146
  # Decrease the position of this item without adjusting the rest of the list.
140
147
  def decrement_position
141
148
  return unless in_list?
142
- set_list_position(self.send(position_column).to_i - 1)
149
+ set_list_position(current_position - 1)
143
150
  end
144
151
 
145
152
  def first?
@@ -162,9 +169,8 @@ module ActiveRecord
162
169
  # selects all higher items by default
163
170
  def higher_items(limit=nil)
164
171
  limit ||= acts_as_list_list.count
165
- position_value = send(position_column)
166
172
  acts_as_list_list.
167
- where("#{quoted_position_column_with_table_name} <= ?", position_value).
173
+ where("#{quoted_position_column_with_table_name} <= ?", current_position).
168
174
  where("#{quoted_table_name}.#{self.class.primary_key} != ?", self.send(self.class.primary_key)).
169
175
  reorder(acts_as_list_order_argument(:desc)).
170
176
  limit(limit)
@@ -180,9 +186,8 @@ module ActiveRecord
180
186
  # selects all lower items by default
181
187
  def lower_items(limit=nil)
182
188
  limit ||= acts_as_list_list.count
183
- position_value = send(position_column)
184
189
  acts_as_list_list.
185
- where("#{quoted_position_column_with_table_name} >= ?", position_value).
190
+ where("#{quoted_position_column_with_table_name} >= ?", current_position).
186
191
  where("#{quoted_table_name}.#{self.class.primary_key} != ?", self.send(self.class.primary_key)).
187
192
  reorder(acts_as_list_order_argument(:asc)).
188
193
  limit(limit)
@@ -194,40 +199,34 @@ module ActiveRecord
194
199
  end
195
200
 
196
201
  def not_in_list?
197
- send(position_column).nil?
202
+ current_position.nil?
198
203
  end
199
204
 
200
205
  def default_position
201
- acts_as_list_class.columns_hash[position_column.to_s].default
206
+ acts_as_list_class.column_defaults[position_column.to_s]
202
207
  end
203
208
 
204
209
  def default_position?
205
- default_position && default_position.to_i == send(position_column)
210
+ default_position && default_position == current_position
206
211
  end
207
212
 
208
213
  # Sets the new position and saves it
209
214
  def set_list_position(new_position, raise_exception_if_save_fails=false)
210
- write_attribute position_column, new_position
215
+ self[position_column] = new_position
211
216
  raise_exception_if_save_fails ? save! : save
212
217
  end
213
218
 
214
219
  private
215
220
 
216
- def swap_positions(item1, item2)
217
- item1_position = item1.send(position_column)
221
+ def swap_positions_with(item)
222
+ item_position = item.current_position
218
223
 
219
- item1.set_list_position(item2.send(position_column))
220
- item2.set_list_position(item1_position)
224
+ item.set_list_position(current_position)
225
+ set_list_position(item_position)
221
226
  end
222
227
 
223
228
  def acts_as_list_list
224
- if ActiveRecord::VERSION::MAJOR < 4
225
- acts_as_list_class.unscoped do
226
- acts_as_list_class.where(scope_condition)
227
- end
228
- else
229
- acts_as_list_class.unscope(:select, :where).where(scope_condition)
230
- end
229
+ acts_as_list_class.unscope(:select, :where).where(scope_condition)
231
230
  end
232
231
 
233
232
  # Poorly named methods. They will insert the item at the desired position if the position
@@ -275,7 +274,7 @@ module ActiveRecord
275
274
  # bottom_position_in_list # => 2
276
275
  def bottom_position_in_list(except = nil)
277
276
  item = bottom_item(except)
278
- item ? item.send(position_column) : acts_as_list_top - 1
277
+ item ? item.current_position : acts_as_list_top - 1
279
278
  end
280
279
 
281
280
  # Returns the bottom item
@@ -302,7 +301,7 @@ module ActiveRecord
302
301
  # This has the effect of moving all the higher items down one.
303
302
  def increment_positions_on_higher_items
304
303
  return unless in_list?
305
- acts_as_list_list.where("#{quoted_position_column_with_table_name} < ?", send(position_column).to_i).increment_all
304
+ acts_as_list_list.where("#{quoted_position_column_with_table_name} < ?", current_position).increment_all
306
305
  end
307
306
 
308
307
  # This has the effect of moving all the lower items down one.
@@ -313,7 +312,11 @@ module ActiveRecord
313
312
  scope = scope.where("#{quoted_table_name}.#{self.class.primary_key} != ?", avoid_id)
314
313
  end
315
314
 
316
- scope.where("#{quoted_position_column_with_table_name} >= ?", position).increment_all
315
+ if sequential_updates?
316
+ scope.where("#{quoted_position_column_with_table_name} >= ?", position).reorder(acts_as_list_order_argument(:desc)).increment_sequentially
317
+ else
318
+ scope.where("#{quoted_position_column_with_table_name} >= ?", position).increment_all
319
+ end
317
320
  end
318
321
 
319
322
  # This has the effect of moving all the higher items up one.
@@ -322,14 +325,11 @@ module ActiveRecord
322
325
  end
323
326
 
324
327
  # This has the effect of moving all the lower items up one.
325
- def decrement_positions_on_lower_items(position=nil)
328
+ def decrement_positions_on_lower_items(position=current_position)
326
329
  return unless in_list?
327
- position ||= send(position_column).to_i
328
330
 
329
331
  if sequential_updates?
330
- acts_as_list_list.where("#{quoted_position_column_with_table_name} > ?", position).reorder(acts_as_list_order_argument(:asc)).each do |item|
331
- item.decrement!(position_column)
332
- end
332
+ acts_as_list_list.where("#{quoted_position_column_with_table_name} > ?", position).reorder(acts_as_list_order_argument(:asc)).decrement_sequentially
333
333
  else
334
334
  acts_as_list_list.where("#{quoted_position_column_with_table_name} > ?", position).decrement_all
335
335
  end
@@ -365,9 +365,7 @@ module ActiveRecord
365
365
  )
366
366
 
367
367
  if sequential_updates?
368
- items.reorder(acts_as_list_order_argument(:asc)).each do |item|
369
- item.decrement!(position_column)
370
- end
368
+ items.reorder(acts_as_list_order_argument(:asc)).decrement_sequentially
371
369
  else
372
370
  items.decrement_all
373
371
  end
@@ -383,9 +381,7 @@ module ActiveRecord
383
381
  )
384
382
 
385
383
  if sequential_updates?
386
- items.reorder(acts_as_list_order_argument(:desc)).each do |item|
387
- item.increment!(position_column)
388
- end
384
+ items.reorder(acts_as_list_order_argument(:desc)).increment_sequentially
389
385
  else
390
386
  items.increment_all
391
387
  end
@@ -397,7 +393,7 @@ module ActiveRecord
397
393
  return set_list_position(position, raise_exception_if_save_fails) if new_record?
398
394
  with_lock do
399
395
  if in_list?
400
- old_position = send(position_column).to_i
396
+ old_position = current_position
401
397
  return if position == old_position
402
398
  # temporary move after bottom with gap, avoiding duplicate values
403
399
  # gap is required to leave room for position increments
@@ -416,31 +412,27 @@ module ActiveRecord
416
412
  return unless position_before_save_changed?
417
413
 
418
414
  old_position = position_before_save || bottom_position_in_list + 1
419
- new_position = send(position_column).to_i
420
415
 
421
- return unless acts_as_list_list.where(
422
- "#{quoted_position_column_with_table_name} = #{new_position}"
416
+ return unless current_position && acts_as_list_list.where(
417
+ "#{quoted_position_column_with_table_name} = #{current_position}"
423
418
  ).count > 1
424
- shuffle_positions_on_intermediate_items old_position, new_position, id
419
+
420
+ shuffle_positions_on_intermediate_items old_position, current_position, id
425
421
  end
426
422
 
427
423
  def position_before_save_changed?
428
- if ActiveRecord::VERSION::MAJOR == 5 && ActiveRecord::VERSION::MINOR >= 1 ||
429
- ActiveRecord::VERSION::MAJOR > 5
430
-
424
+ if active_record_version_is?('>= 5.1')
431
425
  saved_change_to_attribute? position_column
432
426
  else
433
- send "#{position_column}_changed?"
427
+ attribute_changed? position_column
434
428
  end
435
429
  end
436
430
 
437
431
  def position_before_save
438
- if ActiveRecord::VERSION::MAJOR == 5 && ActiveRecord::VERSION::MINOR >= 1 ||
439
- ActiveRecord::VERSION::MAJOR > 5
440
-
432
+ if active_record_version_is?('>= 5.1')
441
433
  attribute_before_last_save position_column
442
434
  else
443
- send "#{position_column}_was"
435
+ attribute_was position_column
444
436
  end
445
437
  end
446
438
 
@@ -469,7 +461,7 @@ module ActiveRecord
469
461
  # This check is skipped if the position is currently the default position from the table
470
462
  # as modifying the default position on creation is handled elsewhere
471
463
  def check_top_position
472
- if send(position_column) && !default_position? && send(position_column) < acts_as_list_top
464
+ if current_position && !default_position? && current_position < acts_as_list_top
473
465
  self[position_column] = acts_as_list_top
474
466
  end
475
467
  end
@@ -489,11 +481,13 @@ module ActiveRecord
489
481
  end
490
482
 
491
483
  def acts_as_list_order_argument(direction = :asc)
492
- if ActiveRecord::VERSION::MAJOR >= 4
493
- { position_column => direction }
494
- else
495
- "#{quoted_position_column_with_table_name} #{direction.to_s.upcase}"
496
- end
484
+ { position_column => direction }
485
+ end
486
+
487
+ def active_record_version_is?(version_requirement)
488
+ requirement = Gem::Requirement.new(version_requirement)
489
+ version = Gem.loaded_specs['activerecord'].version
490
+ requirement.satisfied_by?(version)
497
491
  end
498
492
  end
499
493