acts_as_list 0.8.2 → 0.9.0

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
  SHA1:
3
- metadata.gz: 5b311fb3c5f9b0839f22ff361020142db9c63045
4
- data.tar.gz: 7600388596e829712e8936f9a31b70008ee7d18b
3
+ metadata.gz: 8f0d9ba8f5cf3b633b34697998e60026fbe0e911
4
+ data.tar.gz: ade9d56a50252ff67e5254ac95af5709e3529e1c
5
5
  SHA512:
6
- metadata.gz: dc169373691b7a79c44393a368f4f589b04462c341f8f423f7eeedf846f29346dd3c6821f618287244a24ceafe04ebe416b4c6b6704f0be8470e69a1cd098a76
7
- data.tar.gz: 748929065353ef8d675953d0469a4bd26ec8aee6921edfe6710b97186b25f75abc4a57bd648a2b076923cf5283e6dafb225ba0e6596efd21b23734897adc2f66
6
+ metadata.gz: 4b3799c6c28e9484a89aafa9456d558376da4f9fd9a748ec58ece08398eb618641e44050e9c952221ed260fea7929016a07ffef68f8428e7d2d9e66bda70ab31
7
+ data.tar.gz: 64c1c416482deb55de8c40bbcb3f738eb16967c07df7e0e31729ae4c347268c421f30cf7279d562f910731318646f4dc4daa4e86d2a2a9d5fad8386c7b5e7f16
@@ -1,17 +1,25 @@
1
1
  language: ruby
2
+ cache: bundler
2
3
  # Explicit usage of containerized builds, should provide faster feedback
3
4
  # see https://docs.travis-ci.com/user/workers/container-based-infrastructure/
4
5
  # and https://docs.travis-ci.com/user/ci-environment/#Virtualization-environments
5
6
  sudo: false
6
7
  before_install:
7
- - gem update bundler
8
+ - gem install bundler -v 1.13.7
9
+ before_script:
10
+ - mysql -e 'create database acts_as_list;'
11
+ - psql -c 'create database acts_as_list;' -U postgres
8
12
  rvm:
9
13
  - 1.9.3
10
14
  - 2.0.0
11
- - 2.1.0
12
- - 2.2.2
15
+ - 2.1.9
16
+ - 2.2.6
17
+ - 2.3.3
13
18
  - jruby-19mode
14
- - rbx-2
19
+ env:
20
+ - DB=sqlite
21
+ - DB=mysql
22
+ - DB=postgresql
15
23
  gemfile:
16
24
  - gemfiles/rails_3_2.gemfile
17
25
  - gemfiles/rails_4_1.gemfile
@@ -23,10 +31,7 @@ matrix:
23
31
  gemfile: gemfiles/rails_5_0.gemfile
24
32
  - rvm: 2.0.0
25
33
  gemfile: gemfiles/rails_5_0.gemfile
26
- - rvm: 2.1.0
34
+ - rvm: 2.1.9
27
35
  gemfile: gemfiles/rails_5_0.gemfile
28
36
  - rvm: jruby-19mode
29
37
  gemfile: gemfiles/rails_5_0.gemfile
30
- - rvm: rbx-2
31
- gemfile: gemfiles/rails_5_0.gemfile
32
-
@@ -1,5 +1,19 @@
1
1
  # Change Log
2
2
 
3
+ ## [v0.8.2](https://github.com/swanandp/acts_as_list/tree/v0.8.2) (2016-09-23)
4
+ [Full Changelog](https://github.com/swanandp/acts_as_list/compare/v0.8.1...v0.8.2)
5
+
6
+ **Closed issues:**
7
+
8
+ - We're a repo now, no longer a fork attached to rails/acts\_as\_list [\#232](https://github.com/swanandp/acts_as_list/issues/232)
9
+ - Break away from rails/acts\_as\_list [\#224](https://github.com/swanandp/acts_as_list/issues/224)
10
+ - Problem when inserting straight at top of list [\#109](https://github.com/swanandp/acts_as_list/issues/109)
11
+
12
+ **Merged pull requests:**
13
+
14
+ - Show items with same position in higher and lower items [\#231](https://github.com/swanandp/acts_as_list/pull/231) ([jpalumickas](https://github.com/jpalumickas))
15
+ - fix setting position when previous position was nil [\#230](https://github.com/swanandp/acts_as_list/pull/230) ([StoneFrog](https://github.com/StoneFrog))
16
+
3
17
  ## [v0.8.1](https://github.com/swanandp/acts_as_list/tree/v0.8.1) (2016-09-06)
4
18
  [Full Changelog](https://github.com/swanandp/acts_as_list/compare/v0.8.0...v0.8.1)
5
19
 
data/Gemfile CHANGED
@@ -1,8 +1,5 @@
1
1
  source "http://rubygems.org"
2
2
 
3
- gem "sqlite3", platforms: [:ruby]
4
- gem "activerecord-jdbcsqlite3-adapter", platforms: [:jruby]
5
-
6
3
  gem "rack", "~> 1", platforms: [:ruby_19, :ruby_20, :ruby_21, :jruby]
7
4
 
8
5
  gemspec
@@ -14,4 +11,20 @@ gem "github_changelog_generator", "1.9.0"
14
11
  group :test do
15
12
  gem "minitest", "~> 5.0"
16
13
  gem "test_after_commit", "~> 0.4.2"
14
+ gem "timecop"
15
+ end
16
+
17
+ group :sqlite do
18
+ gem "sqlite3", platforms: [:ruby]
19
+ gem "activerecord-jdbcsqlite3-adapter", platforms: [:jruby]
20
+ end
21
+
22
+ group :postgresql do
23
+ gem "pg", "~> 0.18.0", platforms: [:ruby]
24
+ gem "activerecord-jdbcpostgresql-adapter", platforms: [:jruby]
25
+ end
26
+
27
+ group :mysql do
28
+ gem "mysql2", "~> 0.3.10", platforms: [:ruby]
29
+ gem "activerecord-jdbcmysql-adapter", platforms: [:jruby]
17
30
  end
data/README.md CHANGED
@@ -77,6 +77,30 @@ In `acts_as_list`, "higher" means further up the list (a lower `position`), and
77
77
  - `list_item.lower_item`
78
78
  - `list_item.lower_items` will return all the items below `list_item` in the list (ordered by the position, ascending)
79
79
 
80
+ ## Adding `acts_as_list` To An Existing Model
81
+ As it stands `acts_as_list` requires position values to be set on the model before the instance methods above will work. Adding something like the below to your migration will set the default position. Change the parameters to order if you want a different initial ordering.
82
+
83
+ ```ruby
84
+ class AddPositionToTodoItem < ActiveRecord::Migration
85
+ def change
86
+ add_column :todo_items, :position, :integer
87
+ TodoItem.order(:updated_at).each.with_index(1) do |todo_item, index|
88
+ todo_item.update_column :position, index
89
+ end
90
+ end
91
+ end
92
+ ```
93
+
94
+ If you are using the scope option things can get a bit more complicated. Let's say you have `acts_as_list scope: :todo_list`, you might instead need something like this:
95
+
96
+ ```ruby
97
+ TodoList.all.each do |todo_list|
98
+ todo_list.todo_items.order(:updated_at).each.with_index(1) do |todo_item, index|
99
+ todo_item.update_column :position, index
100
+ end
101
+ end
102
+ ```
103
+
80
104
  ## Notes
81
105
  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.
82
106
 
@@ -99,7 +123,20 @@ default: `1`. Use this option to define the top of the list. Use 0 to make the c
99
123
  - `add_new_at`
100
124
  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.
101
125
 
126
+ ## Disabling temporarily
127
+
128
+ If you need to temporarily disable `acts_as_list` during specific operations such as mass-update or imports:
129
+ ```ruby
130
+ TodoItem.acts_as_list_no_update do
131
+ perform_mass_update
132
+ end
133
+ ```
134
+ In an `acts_as_list_no_update` block, all callbacks are disabled, and positions are not updated. New records will be created with
135
+ the default value from the database. It is your responsibility to correctly manage `positions` values.
136
+
102
137
  ## Versions
138
+ 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.
139
+
103
140
  As of version `0.7.5` Rails 5 is supported.
104
141
 
105
142
  All versions `0.1.5` onwards require Rails 3.0.x and higher.
@@ -2,8 +2,6 @@
2
2
 
3
3
  source "http://rubygems.org"
4
4
 
5
- gem "sqlite3", :platforms => [:ruby]
6
- gem "activerecord-jdbcsqlite3-adapter", :platforms => [:jruby]
7
5
  gem "rack", "~> 1", :platforms => [:ruby_19, :ruby_20, :ruby_21, :jruby]
8
6
  gem "rake"
9
7
  gem "appraisal"
@@ -13,7 +11,23 @@ gem "activerecord", "~> 3.2.22.2"
13
11
  group :test do
14
12
  gem "minitest", "~> 5.0"
15
13
  gem "test_after_commit", "~> 0.4.2"
14
+ gem "timecop"
16
15
  gem "after_commit_exception_notification"
17
16
  end
18
17
 
18
+ group :sqlite do
19
+ gem "sqlite3", :platforms => [:ruby]
20
+ gem "activerecord-jdbcsqlite3-adapter", :platforms => [:jruby]
21
+ end
22
+
23
+ group :postgresql do
24
+ gem "pg", "~> 0.18.0", :platforms => [:ruby]
25
+ gem "activerecord-jdbcpostgresql-adapter", :platforms => [:jruby]
26
+ end
27
+
28
+ group :mysql do
29
+ gem "mysql2", "~> 0.3.10", :platforms => [:ruby]
30
+ gem "activerecord-jdbcmysql-adapter", :platforms => [:jruby]
31
+ end
32
+
19
33
  gemspec :path => "../"
@@ -2,8 +2,6 @@
2
2
 
3
3
  source "http://rubygems.org"
4
4
 
5
- gem "sqlite3", :platforms => [:ruby]
6
- gem "activerecord-jdbcsqlite3-adapter", :platforms => [:jruby]
7
5
  gem "rack", "~> 1", :platforms => [:ruby_19, :ruby_20, :ruby_21, :jruby]
8
6
  gem "rake"
9
7
  gem "appraisal"
@@ -13,7 +11,23 @@ gem "activerecord", "~> 4.1.16"
13
11
  group :test do
14
12
  gem "minitest", "~> 5.0"
15
13
  gem "test_after_commit", "~> 0.4.2"
14
+ gem "timecop"
16
15
  gem "after_commit_exception_notification"
17
16
  end
18
17
 
18
+ group :sqlite do
19
+ gem "sqlite3", :platforms => [:ruby]
20
+ gem "activerecord-jdbcsqlite3-adapter", :platforms => [:jruby]
21
+ end
22
+
23
+ group :postgresql do
24
+ gem "pg", "~> 0.18.0", :platforms => [:ruby]
25
+ gem "activerecord-jdbcpostgresql-adapter", :platforms => [:jruby]
26
+ end
27
+
28
+ group :mysql do
29
+ gem "mysql2", "~> 0.3.10", :platforms => [:ruby]
30
+ gem "activerecord-jdbcmysql-adapter", :platforms => [:jruby]
31
+ end
32
+
19
33
  gemspec :path => "../"
@@ -2,8 +2,6 @@
2
2
 
3
3
  source "http://rubygems.org"
4
4
 
5
- gem "sqlite3", :platforms => [:ruby]
6
- gem "activerecord-jdbcsqlite3-adapter", :platforms => [:jruby]
7
5
  gem "rack", "~> 1", :platforms => [:ruby_19, :ruby_20, :ruby_21, :jruby]
8
6
  gem "rake"
9
7
  gem "appraisal"
@@ -13,6 +11,22 @@ gem "activerecord", "~> 4.2.7"
13
11
  group :test do
14
12
  gem "minitest", "~> 5.0"
15
13
  gem "test_after_commit", "~> 0.4.2"
14
+ gem "timecop"
15
+ end
16
+
17
+ group :sqlite do
18
+ gem "sqlite3", :platforms => [:ruby]
19
+ gem "activerecord-jdbcsqlite3-adapter", :platforms => [:jruby]
20
+ end
21
+
22
+ group :postgresql do
23
+ gem "pg", "~> 0.18.0", :platforms => [:ruby]
24
+ gem "activerecord-jdbcpostgresql-adapter", :platforms => [:jruby]
25
+ end
26
+
27
+ group :mysql do
28
+ gem "mysql2", "~> 0.3.10", :platforms => [:ruby]
29
+ gem "activerecord-jdbcmysql-adapter", :platforms => [:jruby]
16
30
  end
17
31
 
18
32
  gemspec :path => "../"
@@ -2,8 +2,6 @@
2
2
 
3
3
  source "http://rubygems.org"
4
4
 
5
- gem "sqlite3", :platforms => [:ruby]
6
- gem "activerecord-jdbcsqlite3-adapter", :platforms => [:jruby]
7
5
  gem "rack", "~> 1", :platforms => [:ruby_19, :ruby_20, :ruby_21, :jruby]
8
6
  gem "rake"
9
7
  gem "appraisal"
@@ -13,6 +11,22 @@ gem "activerecord", "~> 5.0.0"
13
11
  group :test do
14
12
  gem "minitest", "~> 5.0"
15
13
  gem "test_after_commit", "~> 0.4.2"
14
+ gem "timecop"
15
+ end
16
+
17
+ group :sqlite do
18
+ gem "sqlite3", :platforms => [:ruby]
19
+ gem "activerecord-jdbcsqlite3-adapter", :platforms => [:jruby]
20
+ end
21
+
22
+ group :postgresql do
23
+ gem "pg", "~> 0.18.0", :platforms => [:ruby]
24
+ gem "activerecord-jdbcpostgresql-adapter", :platforms => [:jruby]
25
+ end
26
+
27
+ group :mysql do
28
+ gem "mysql2", "~> 0.3.10", :platforms => [:ruby]
29
+ gem "activerecord-jdbcmysql-adapter", :platforms => [:jruby]
16
30
  end
17
31
 
18
32
  gemspec :path => "../"
@@ -1,15 +1,9 @@
1
1
  require 'acts_as_list/active_record/acts/list'
2
-
3
- module ActsAsList
4
- if defined?(Rails::Railtie)
5
- class Railtie < Rails::Railtie
6
- initializer 'acts_as_list.insert_into_active_record' do
7
- ActiveSupport.on_load :active_record do
8
- ActiveRecord::Base.send(:include, ActiveRecord::Acts::List)
9
- end
10
- end
11
- end
12
- else
13
- ActiveRecord::Base.send(:include, ActiveRecord::Acts::List) if defined?(ActiveRecord)
14
- end
15
- end
2
+ require "acts_as_list/active_record/acts/column_method_definer"
3
+ require "acts_as_list/active_record/acts/scope_method_definer"
4
+ require "acts_as_list/active_record/acts/top_of_list_method_definer"
5
+ require "acts_as_list/active_record/acts/add_new_at_method_definer"
6
+ require "acts_as_list/active_record/acts/aux_method_definer"
7
+ require "acts_as_list/active_record/acts/callback_definer"
8
+ require 'acts_as_list/active_record/acts/no_update'
9
+ require "acts_as_list/active_record/acts/sequential_updates_method_definer"
@@ -0,0 +1,9 @@
1
+ module ActiveRecord::Acts::List::AddNewAtMethodDefiner #:nodoc:
2
+ def self.call(caller_class, add_new_at)
3
+ caller_class.class_eval do
4
+ define_method :add_new_at do
5
+ add_new_at
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module ActiveRecord::Acts::List::AuxMethodDefiner #:nodoc:
2
+ def self.call(caller_class)
3
+ caller_class.class_eval do
4
+ define_method :acts_as_list_class do
5
+ caller_class
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,19 @@
1
+ module ActiveRecord::Acts::List::CallbackDefiner #:nodoc:
2
+ def self.call(caller_class, add_new_at)
3
+ caller_class.class_eval do
4
+ before_validation :check_top_position, unless: :act_as_list_no_update?
5
+
6
+ before_destroy :lock!
7
+ after_destroy :decrement_positions_on_lower_items, unless: :act_as_list_no_update?
8
+
9
+ before_update :check_scope, unless: :act_as_list_no_update?
10
+ after_update :update_positions, unless: :act_as_list_no_update?
11
+
12
+ after_commit :clear_scope_changed
13
+
14
+ if add_new_at.present?
15
+ before_create "add_to_list_#{add_new_at}".to_sym, unless: :act_as_list_no_update?
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,50 @@
1
+ module ActiveRecord::Acts::List::ColumnMethodDefiner #:nodoc:
2
+ def self.call(caller_class, column)
3
+ caller_class.class_eval do
4
+ attr_reader :position_changed
5
+
6
+ define_method :position_column do
7
+ column
8
+ end
9
+
10
+ define_method :"#{column}=" do |position|
11
+ write_attribute(column, position)
12
+ @position_changed = true
13
+ end
14
+
15
+ # only add to attr_accessible
16
+ # if the class has some mass_assignment_protection
17
+ if defined?(accessible_attributes) and !accessible_attributes.blank?
18
+ attr_accessible :"#{column}"
19
+ end
20
+
21
+ define_singleton_method :quoted_position_column do
22
+ @_quoted_position_column ||= connection.quote_column_name(column)
23
+ end
24
+
25
+ define_singleton_method :quoted_position_column_with_table_name do
26
+ @_quoted_position_column_with_table_name ||= "#{caller_class.quoted_table_name}.#{quoted_position_column}"
27
+ end
28
+
29
+ define_singleton_method :decrement_all do
30
+ update_all_with_touch "#{quoted_position_column} = (#{quoted_position_column_with_table_name} - 1)"
31
+ end
32
+
33
+ define_singleton_method :increment_all do
34
+ update_all_with_touch "#{quoted_position_column} = (#{quoted_position_column_with_table_name} + 1)"
35
+ end
36
+
37
+ define_singleton_method :update_all_with_touch do |updates|
38
+ record = new
39
+ attrs = record.send(:timestamp_attributes_for_update_in_model)
40
+ now = record.send(:current_time_from_proper_timezone)
41
+
42
+ attrs.each do |attr|
43
+ updates << ", #{connection.quote_column_name(attr)} = #{connection.quote(connection.quoted_date(now))}"
44
+ end
45
+
46
+ update_all(updates)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -1,10 +1,42 @@
1
+ class << ActiveRecord::Base
2
+ # Configuration options are:
3
+ #
4
+ # * +column+ - specifies the column name to use for keeping the position integer (default: +position+)
5
+ # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach <tt>_id</tt>
6
+ # (if it hasn't already been added) and use that as the foreign key restriction. It's also possible
7
+ # to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
8
+ # Example: <tt>acts_as_list scope: 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
9
+ # * +top_of_list+ - defines the integer used for the top of the list. Defaults to 1. Use 0 to make the collection
10
+ # act more like an array in its indexing.
11
+ # * +add_new_at+ - specifies whether objects get added to the :top or :bottom of the list. (default: +bottom+)
12
+ # `nil` will result in new items not being added to the list on create.
13
+ # * +sequential_updates+ - specifies whether insert_at should update objects positions during shuffling
14
+ # one by one to respect position column unique not null constraint.
15
+ # Defaults to true if position column has unique index, otherwise false.
16
+ # If constraint is <tt>deferrable initially deferred<tt>, overriding it with false will speed up insert_at.
17
+ def acts_as_list(options = {})
18
+ configuration = { column: "position", scope: "1 = 1", top_of_list: 1, add_new_at: :bottom }
19
+ configuration.update(options) if options.is_a?(Hash)
20
+
21
+ caller_class = self
22
+
23
+ ActiveRecord::Acts::List::ColumnMethodDefiner.call(caller_class, configuration[:column])
24
+ ActiveRecord::Acts::List::ScopeMethodDefiner.call(caller_class, configuration[:scope])
25
+ ActiveRecord::Acts::List::TopOfListMethodDefiner.call(caller_class, configuration[:top_of_list])
26
+ ActiveRecord::Acts::List::AddNewAtMethodDefiner.call(caller_class, configuration[:add_new_at])
27
+
28
+ ActiveRecord::Acts::List::AuxMethodDefiner.call(caller_class)
29
+ ActiveRecord::Acts::List::CallbackDefiner.call(caller_class, configuration[:add_new_at])
30
+ ActiveRecord::Acts::List::SequentialUpdatesMethodDefiner.call(caller_class, configuration[:column], configuration[:sequential_updates])
31
+
32
+ include ActiveRecord::Acts::List::InstanceMethods
33
+ include ActiveRecord::Acts::List::NoUpdate
34
+ end
35
+ end
36
+
1
37
  module ActiveRecord
2
38
  module Acts #:nodoc:
3
39
  module List #:nodoc:
4
- def self.included(base)
5
- base.extend(ClassMethods)
6
- end
7
-
8
40
  # This +acts_as+ extension provides the capabilities for sorting and reordering a number of objects in a list.
9
41
  # The class that has this specified needs to have a +position+ column defined as an integer on
10
42
  # the mapped database table.
@@ -22,142 +54,6 @@ module ActiveRecord
22
54
  #
23
55
  # todo_list.first.move_to_bottom
24
56
  # todo_list.last.move_higher
25
- module ClassMethods
26
- # Configuration options are:
27
- #
28
- # * +column+ - specifies the column name to use for keeping the position integer (default: +position+)
29
- # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach <tt>_id</tt>
30
- # (if it hasn't already been added) and use that as the foreign key restriction. It's also possible
31
- # to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
32
- # Example: <tt>acts_as_list scope: 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
33
- # * +top_of_list+ - defines the integer used for the top of the list. Defaults to 1. Use 0 to make the collection
34
- # act more like an array in its indexing.
35
- # * +add_new_at+ - specifies whether objects get added to the :top or :bottom of the list. (default: +bottom+)
36
- # `nil` will result in new items not being added to the list on create
37
- def acts_as_list(options = {})
38
- configuration = { column: "position", scope: "1 = 1", top_of_list: 1, add_new_at: :bottom}
39
- configuration.update(options) if options.is_a?(Hash)
40
-
41
- if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
42
- configuration[:scope] = :"#{configuration[:scope]}_id"
43
- end
44
-
45
- caller_class = self
46
-
47
- class_eval do
48
- define_singleton_method :acts_as_list_top do
49
- configuration[:top_of_list].to_i
50
- end
51
-
52
- define_method :acts_as_list_top do
53
- configuration[:top_of_list].to_i
54
- end
55
-
56
- define_method :acts_as_list_class do
57
- caller_class
58
- end
59
-
60
- define_method :position_column do
61
- configuration[:column]
62
- end
63
-
64
- define_method :scope_name do
65
- configuration[:scope]
66
- end
67
-
68
- define_method :add_new_at do
69
- configuration[:add_new_at]
70
- end
71
-
72
- define_method :"#{configuration[:column]}=" do |position|
73
- write_attribute(configuration[:column], position)
74
- @position_changed = true
75
- end
76
-
77
- if configuration[:scope].is_a?(Symbol)
78
- define_method :scope_condition do
79
- { configuration[:scope] => send(:"#{configuration[:scope]}") }
80
- end
81
-
82
- define_method :scope_changed? do
83
- changed.include?(scope_name.to_s)
84
- end
85
- elsif configuration[:scope].is_a?(Array)
86
- define_method :scope_condition do
87
- configuration[:scope].inject({}) do |hash, column|
88
- hash.merge!({ column.to_sym => read_attribute(column.to_sym) })
89
- end
90
- end
91
-
92
- define_method :scope_changed? do
93
- (scope_condition.keys & changed.map(&:to_sym)).any?
94
- end
95
- else
96
- define_method :scope_condition do
97
- eval "%{#{configuration[:scope]}}"
98
- end
99
-
100
- define_method :scope_changed? do
101
- false
102
- end
103
- end
104
-
105
- # only add to attr_accessible
106
- # if the class has some mass_assignment_protection
107
- if defined?(accessible_attributes) and !accessible_attributes.blank?
108
- attr_accessible :"#{configuration[:column]}"
109
- end
110
-
111
- define_singleton_method :quoted_position_column do
112
- @_quoted_position_column ||= connection.quote_column_name(configuration[:column])
113
- end
114
-
115
- define_singleton_method :quoted_position_column_with_table_name do
116
- @_quoted_position_column_with_table_name ||= "#{caller_class.quoted_table_name}.#{quoted_position_column}"
117
- end
118
-
119
- scope :in_list, lambda { where("#{quoted_position_column_with_table_name} IS NOT NULL") }
120
-
121
- define_singleton_method :decrement_all do
122
- update_all_with_touch "#{quoted_position_column} = (#{quoted_position_column_with_table_name} - 1)"
123
- end
124
-
125
- define_singleton_method :increment_all do
126
- update_all_with_touch "#{quoted_position_column} = (#{quoted_position_column_with_table_name} + 1)"
127
- end
128
-
129
- define_singleton_method :update_all_with_touch do |updates|
130
- record = new
131
- attrs = record.send(:timestamp_attributes_for_update_in_model)
132
- now = record.send(:current_time_from_proper_timezone)
133
-
134
- query = attrs.map { |attr| "#{connection.quote_column_name(attr)} = :now" }
135
- query.push updates
136
- query = query.join(", ")
137
-
138
- update_all([query, now: now])
139
- end
140
- end
141
-
142
- attr_reader :position_changed
143
-
144
- before_validation :check_top_position
145
-
146
- before_destroy :lock!
147
- after_destroy :decrement_positions_on_lower_items
148
-
149
- before_update :check_scope
150
- after_update :update_positions
151
-
152
- after_commit :clear_scope_changed
153
-
154
- if configuration[:add_new_at].present?
155
- before_create "add_to_list_#{configuration[:add_new_at]}".to_sym
156
- end
157
-
158
- include ::ActiveRecord::Acts::List::InstanceMethods
159
- end
160
- end
161
57
 
162
58
  # All the methods available to a record that has had <tt>acts_as_list</tt> specified. Each method works
163
59
  # by assuming the object to be the item in the list, so <tt>chapter.move_lower</tt> would move that chapter
@@ -315,214 +211,240 @@ module ActiveRecord
315
211
 
316
212
  private
317
213
 
318
- def swap_positions(item1, item2)
319
- item1.set_list_position(item2.send(position_column))
320
- item2.set_list_position(item1.send("#{position_column}_was"))
321
- end
214
+ def swap_positions(item1, item2)
215
+ item1_position = item1.send(position_column)
322
216
 
323
- def acts_as_list_list
324
- acts_as_list_class.unscoped do
325
- acts_as_list_class.where(scope_condition)
326
- end
217
+ item1.set_list_position(item2.send(position_column))
218
+ item2.set_list_position(item1_position)
219
+ end
220
+
221
+ def acts_as_list_list
222
+ acts_as_list_class.unscoped do
223
+ acts_as_list_class.where(scope_condition)
327
224
  end
225
+ end
328
226
 
329
- # Poorly named methods. They will insert the item at the desired position if the position
330
- # has been set manually using position=, not necessarily the top or bottom of the list:
227
+ # Poorly named methods. They will insert the item at the desired position if the position
228
+ # has been set manually using position=, not necessarily the top or bottom of the list:
331
229
 
332
- def add_to_list_top
333
- if not_in_list? || internal_scope_changed? && !position_changed || default_position?
334
- increment_positions_on_all_items
335
- self[position_column] = acts_as_list_top
336
- else
337
- increment_positions_on_lower_items(self[position_column], id)
338
- end
230
+ def add_to_list_top
231
+ if not_in_list? || internal_scope_changed? && !position_changed || default_position?
232
+ increment_positions_on_all_items
233
+ self[position_column] = acts_as_list_top
234
+ else
235
+ increment_positions_on_lower_items(self[position_column], id)
236
+ end
339
237
 
340
- # Make sure we know that we've processed this scope change already
341
- @scope_changed = false
238
+ # Make sure we know that we've processed this scope change already
239
+ @scope_changed = false
342
240
 
343
- # Don't halt the callback chain
344
- true
241
+ # Don't halt the callback chain
242
+ true
243
+ end
244
+
245
+ def add_to_list_bottom
246
+ if not_in_list? || internal_scope_changed? && !position_changed || default_position?
247
+ self[position_column] = bottom_position_in_list.to_i + 1
248
+ else
249
+ increment_positions_on_lower_items(self[position_column], id)
345
250
  end
346
251
 
347
- def add_to_list_bottom
348
- if not_in_list? || internal_scope_changed? && !position_changed || default_position?
349
- self[position_column] = bottom_position_in_list.to_i + 1
350
- else
351
- increment_positions_on_lower_items(self[position_column], id)
352
- end
252
+ # Make sure we know that we've processed this scope change already
253
+ @scope_changed = false
353
254
 
354
- # Make sure we know that we've processed this scope change already
355
- @scope_changed = false
255
+ # Don't halt the callback chain
256
+ true
257
+ end
356
258
 
357
- # Don't halt the callback chain
358
- true
359
- end
259
+ # Overwrite this method to define the scope of the list changes
260
+ def scope_condition() {} end
360
261
 
361
- # Overwrite this method to define the scope of the list changes
362
- def scope_condition() {} end
262
+ # Returns the bottom position number in the list.
263
+ # bottom_position_in_list # => 2
264
+ def bottom_position_in_list(except = nil)
265
+ item = bottom_item(except)
266
+ item ? item.send(position_column) : acts_as_list_top - 1
267
+ end
363
268
 
364
- # Returns the bottom position number in the list.
365
- # bottom_position_in_list # => 2
366
- def bottom_position_in_list(except = nil)
367
- item = bottom_item(except)
368
- item ? item.send(position_column) : acts_as_list_top - 1
369
- end
269
+ # Returns the bottom item
270
+ def bottom_item(except = nil)
271
+ conditions = except ? "#{quoted_table_name}.#{self.class.primary_key} != #{self.class.connection.quote(except.id)}" : {}
272
+ acts_as_list_list.in_list.where(
273
+ conditions
274
+ ).order(
275
+ "#{quoted_position_column_with_table_name} DESC"
276
+ ).first
277
+ end
370
278
 
371
- # Returns the bottom item
372
- def bottom_item(except = nil)
373
- conditions = except ? "#{quoted_table_name}.#{self.class.primary_key} != #{self.class.connection.quote(except.id)}" : {}
374
- acts_as_list_list.in_list.where(
375
- conditions
376
- ).order(
377
- "#{quoted_position_column_with_table_name} DESC"
378
- ).first
379
- end
279
+ # Forces item to assume the bottom position in the list.
280
+ def assume_bottom_position
281
+ set_list_position(bottom_position_in_list(self).to_i + 1)
282
+ end
380
283
 
381
- # Forces item to assume the bottom position in the list.
382
- def assume_bottom_position
383
- set_list_position(bottom_position_in_list(self).to_i + 1)
384
- end
284
+ # Forces item to assume the top position in the list.
285
+ def assume_top_position
286
+ set_list_position(acts_as_list_top)
287
+ end
385
288
 
386
- # Forces item to assume the top position in the list.
387
- def assume_top_position
388
- set_list_position(acts_as_list_top)
389
- end
289
+ # This has the effect of moving all the higher items down one.
290
+ def increment_positions_on_higher_items
291
+ return unless in_list?
292
+ acts_as_list_list.where("#{quoted_position_column_with_table_name} < ?", send(position_column).to_i).increment_all
293
+ end
390
294
 
391
- # This has the effect of moving all the higher items up one.
392
- def decrement_positions_on_higher_items(position)
393
- acts_as_list_list.where("#{quoted_position_column_with_table_name} <= ?", position).decrement_all
394
- end
295
+ # This has the effect of moving all the lower items down one.
296
+ def increment_positions_on_lower_items(position, avoid_id = nil)
297
+ scope = acts_as_list_list
395
298
 
396
- # This has the effect of moving all the lower items up one.
397
- def decrement_positions_on_lower_items(position=nil)
398
- return unless in_list?
399
- position ||= send(position_column).to_i
400
- acts_as_list_list.where("#{quoted_position_column_with_table_name} > ?", position).decrement_all
299
+ if avoid_id
300
+ scope = scope.where("#{quoted_table_name}.#{self.class.primary_key} != ?", self.class.connection.quote(avoid_id))
401
301
  end
402
302
 
403
- # This has the effect of moving all the higher items down one.
404
- def increment_positions_on_higher_items
405
- return unless in_list?
406
- acts_as_list_list.where("#{quoted_position_column_with_table_name} < #{send(position_column).to_i}").increment_all
407
- end
303
+ scope.where("#{quoted_position_column_with_table_name} >= ?", position).increment_all
304
+ end
408
305
 
409
- # This has the effect of moving all the lower items down one.
410
- def increment_positions_on_lower_items(position, avoid_id = nil)
411
- avoid_id_condition = avoid_id ? " AND #{quoted_table_name}.#{self.class.primary_key} != #{self.class.connection.quote(avoid_id)}" : ''
306
+ # This has the effect of moving all the higher items up one.
307
+ def decrement_positions_on_higher_items(position)
308
+ acts_as_list_list.where("#{quoted_position_column_with_table_name} <= ?", position).decrement_all
309
+ end
412
310
 
413
- acts_as_list_list.where("#{quoted_position_column_with_table_name} >= #{position}#{avoid_id_condition}").increment_all
414
- end
311
+ # This has the effect of moving all the lower items up one.
312
+ def decrement_positions_on_lower_items(position=nil)
313
+ return unless in_list?
314
+ position ||= send(position_column).to_i
315
+ acts_as_list_list.where("#{quoted_position_column_with_table_name} > ?", position).decrement_all
316
+ end
415
317
 
416
- # Increments position (<tt>position_column</tt>) of all items in the list.
417
- def increment_positions_on_all_items
418
- acts_as_list_list.increment_all
419
- end
318
+ # Increments position (<tt>position_column</tt>) of all items in the list.
319
+ def increment_positions_on_all_items
320
+ acts_as_list_list.increment_all
321
+ end
420
322
 
421
- # Reorders intermediate items to support moving an item from old_position to new_position.
422
- def shuffle_positions_on_intermediate_items(old_position, new_position, avoid_id = nil)
423
- return if old_position == new_position
424
- avoid_id_condition = avoid_id ? " AND #{quoted_table_name}.#{self.class.primary_key} != #{self.class.connection.quote(avoid_id)}" : ''
425
-
426
- if old_position < new_position
427
- # Decrement position of intermediate items
428
- #
429
- # e.g., if moving an item from 2 to 5,
430
- # move [3, 4, 5] to [2, 3, 4]
431
- acts_as_list_list.where(
432
- "#{quoted_position_column_with_table_name} > ?", old_position
433
- ).where(
434
- "#{quoted_position_column_with_table_name} <= #{new_position}#{avoid_id_condition}"
435
- ).decrement_all
323
+ # Reorders intermediate items to support moving an item from old_position to new_position.
324
+ # unique constraint prevents regular increment_all and forces to do increments one by one
325
+ # http://stackoverflow.com/questions/7703196/sqlite-increment-unique-integer-field
326
+ # both SQLite and PostgreSQL (and most probably MySQL too) has same issue
327
+ # that's why *sequential_updates?* check alters implementation behavior
328
+ def shuffle_positions_on_intermediate_items(old_position, new_position, avoid_id = nil)
329
+ return if old_position == new_position
330
+ scope = acts_as_list_list
331
+
332
+ if avoid_id
333
+ scope = scope.where("#{quoted_table_name}.#{self.class.primary_key} != ?", self.class.connection.quote(avoid_id))
334
+ end
335
+
336
+ if old_position < new_position
337
+ # Decrement position of intermediate items
338
+ #
339
+ # e.g., if moving an item from 2 to 5,
340
+ # move [3, 4, 5] to [2, 3, 4]
341
+ items = scope.where(
342
+ "#{quoted_position_column_with_table_name} > ?", old_position
343
+ ).where(
344
+ "#{quoted_position_column_with_table_name} <= ?", new_position
345
+ )
346
+
347
+ if sequential_updates?
348
+ items.order("#{quoted_position_column_with_table_name} ASC").each do |item|
349
+ item.decrement!(position_column)
350
+ end
436
351
  else
437
- # Increment position of intermediate items
438
- #
439
- # e.g., if moving an item from 5 to 2,
440
- # move [2, 3, 4] to [3, 4, 5]
441
- acts_as_list_list.where(
442
- "#{quoted_position_column_with_table_name} >= ?", new_position
443
- ).where(
444
- "#{quoted_position_column_with_table_name} < #{old_position}#{avoid_id_condition}"
445
- ).increment_all
352
+ items.decrement_all
446
353
  end
447
- end
448
-
449
- def insert_at_position(position)
450
- return set_list_position(position) if new_record?
451
- with_lock do
452
- if in_list?
453
- old_position = send(position_column).to_i
454
- return if position == old_position
455
- shuffle_positions_on_intermediate_items(old_position, position)
456
- else
457
- increment_positions_on_lower_items(position)
354
+ else
355
+ # Increment position of intermediate items
356
+ #
357
+ # e.g., if moving an item from 5 to 2,
358
+ # move [2, 3, 4] to [3, 4, 5]
359
+ items = scope.where(
360
+ "#{quoted_position_column_with_table_name} >= ?", new_position
361
+ ).where(
362
+ "#{quoted_position_column_with_table_name} < ?", old_position
363
+ )
364
+
365
+ if sequential_updates?
366
+ items.order("#{quoted_position_column_with_table_name} DESC").each do |item|
367
+ item.increment!(position_column)
458
368
  end
459
- set_list_position(position)
369
+ else
370
+ items.increment_all
460
371
  end
461
372
  end
462
-
463
- # used by insert_at_position instead of remove_from_list, as postgresql raises error if position_column has non-null constraint
464
- def store_at_0
373
+ end
374
+
375
+ def insert_at_position(position)
376
+ return set_list_position(position) if new_record?
377
+ with_lock do
465
378
  if in_list?
466
379
  old_position = send(position_column).to_i
467
- set_list_position(0)
468
- decrement_positions_on_lower_items(old_position)
380
+ return if position == old_position
381
+ # temporary move after bottom with gap, avoiding duplicate values
382
+ # gap is required to leave room for position increments
383
+ # positive number will be valid with unique not null check (>= 0) db constraint
384
+ temporary_position = acts_as_list_class.maximum(position_column).to_i + 2
385
+ set_list_position(temporary_position)
386
+ shuffle_positions_on_intermediate_items(old_position, position, id)
387
+ else
388
+ increment_positions_on_lower_items(position)
469
389
  end
390
+ set_list_position(position)
470
391
  end
392
+ end
471
393
 
472
- def update_positions
473
- old_position = send("#{position_column}_was") || bottom_position_in_list + 1
474
- new_position = send(position_column).to_i
394
+ def update_positions
395
+ old_position = send("#{position_column}_was") || bottom_position_in_list + 1
396
+ new_position = send(position_column).to_i
475
397
 
476
- return unless acts_as_list_list.where(
477
- "#{quoted_position_column_with_table_name} = #{new_position}"
478
- ).count > 1
479
- shuffle_positions_on_intermediate_items old_position, new_position, id
480
- end
398
+ return unless acts_as_list_list.where(
399
+ "#{quoted_position_column_with_table_name} = #{new_position}"
400
+ ).count > 1
401
+ shuffle_positions_on_intermediate_items old_position, new_position, id
402
+ end
481
403
 
482
- def internal_scope_changed?
483
- return @scope_changed if defined?(@scope_changed)
404
+ def internal_scope_changed?
405
+ return @scope_changed if defined?(@scope_changed)
484
406
 
485
- @scope_changed = scope_changed?
486
- end
407
+ @scope_changed = scope_changed?
408
+ end
487
409
 
488
- def clear_scope_changed
489
- remove_instance_variable(:@scope_changed) if defined?(@scope_changed)
490
- end
410
+ def clear_scope_changed
411
+ remove_instance_variable(:@scope_changed) if defined?(@scope_changed)
412
+ end
491
413
 
492
- def check_scope
493
- if internal_scope_changed?
494
- cached_changes = changes
414
+ def check_scope
415
+ if internal_scope_changed?
416
+ cached_changes = changes
495
417
 
496
- cached_changes.each { |attribute, values| self[attribute] = values[0] }
497
- send('decrement_positions_on_lower_items') if lower_item
498
- cached_changes.each { |attribute, values| self[attribute] = values[1] }
418
+ cached_changes.each { |attribute, values| self[attribute] = values[0] }
419
+ send('decrement_positions_on_lower_items') if lower_item
420
+ cached_changes.each { |attribute, values| self[attribute] = values[1] }
499
421
 
500
- send("add_to_list_#{add_new_at}") if add_new_at.present?
501
- end
422
+ send("add_to_list_#{add_new_at}") if add_new_at.present?
502
423
  end
424
+ end
503
425
 
504
- # This check is skipped if the position is currently the default position from the table
505
- # as modifying the default position on creation is handled elsewhere
506
- def check_top_position
507
- if send(position_column) && !default_position? && send(position_column) < acts_as_list_top
508
- self[position_column] = acts_as_list_top
509
- end
426
+ # This check is skipped if the position is currently the default position from the table
427
+ # as modifying the default position on creation is handled elsewhere
428
+ def check_top_position
429
+ if send(position_column) && !default_position? && send(position_column) < acts_as_list_top
430
+ self[position_column] = acts_as_list_top
510
431
  end
432
+ end
511
433
 
512
- # When using raw column name it must be quoted otherwise it can raise syntax errors with SQL keywords (e.g. order)
513
- def quoted_position_column
514
- @_quoted_position_column ||= self.class.connection.quote_column_name(position_column)
515
- end
434
+ # When using raw column name it must be quoted otherwise it can raise syntax errors with SQL keywords (e.g. order)
435
+ def quoted_position_column
436
+ @_quoted_position_column ||= self.class.connection.quote_column_name(position_column)
437
+ end
516
438
 
517
- # Used in order clauses
518
- def quoted_table_name
519
- @_quoted_table_name ||= acts_as_list_class.quoted_table_name
520
- end
439
+ # Used in order clauses
440
+ def quoted_table_name
441
+ @_quoted_table_name ||= acts_as_list_class.quoted_table_name
442
+ end
521
443
 
522
- def quoted_position_column_with_table_name
523
- @_quoted_position_column_with_table_name ||= "#{quoted_table_name}.#{quoted_position_column}"
524
- end
444
+ def quoted_position_column_with_table_name
445
+ @_quoted_position_column_with_table_name ||= "#{quoted_table_name}.#{quoted_position_column}"
446
+ end
525
447
  end
526
448
  end
527
449
  end
528
- end
450
+ end