acts_as_positioned 0.0.1 → 0.0.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 83b94938abcb3a029732e108e86d077e46d60674
4
- data.tar.gz: 445a9a8fd77994dca9df179e796f3208397bd1b1
3
+ metadata.gz: 6d3bc41882a5401fb9cba86783a4ec62e0a51028
4
+ data.tar.gz: 7dd0ca43f2a54ccc6697f58f892734f781f8e104
5
5
  SHA512:
6
- metadata.gz: 949f19c613d45803c816678b6b6f3dcc00ddf1f67f47b533c4d9e0443285970c41f5f90330e9fbb1f61c3cd7a546354d3bda2859e58bb8b43858ea5a45d3eaa7
7
- data.tar.gz: 847050cd223261db55862b04d71113975ddc7057f70a8bc0b0e94e5ec4a3482ba9e62995267189a430dcc5f8dd370544c62d1e9926cd94c5bbeaa1535499d560
6
+ metadata.gz: 2a41117d8ce0709ddf8c10448880b75357f9699dde310ae4a40bcf5116d8ff18203bd7d1044d7eb5d8f4ad2b04e75461ea91eec1eebbb5654ec09ea68f79a7f2
7
+ data.tar.gz: fd3424c2a28730f35e77bd08eaf4b5c7fca883e52e941eba8fe74734b53b54817ee7246fbe2bdc0407850dbc83864ba59010a2cfa6e2478cfb622f6296bcc1e9
@@ -7,79 +7,81 @@ module ActsAsPositioned
7
7
  # Class methods that will be added to the base class (the class mixing in this module).
8
8
  module ClassMethods
9
9
  def acts_as_positioned(options = {})
10
- column = options[:column] || :position
11
- scope_columns = Array.wrap(options[:scope])
10
+ column = (options[:column] || :position).to_s
11
+ scope_columns = Array.wrap(options[:scope]).map(&:to_s)
12
12
 
13
13
  after_validation do
14
- acts_as_positioned_validation(column, scope_columns)
14
+ aap_validate_position(column, scope_columns)
15
15
  end
16
16
 
17
17
  before_create do
18
- acts_as_positioned_create(column, scope_columns)
18
+ aap_insert_position(column, scope_columns)
19
19
  end
20
20
 
21
21
  before_destroy do
22
- acts_as_positioned_destroy(column, scope_columns)
22
+ aap_remove_position(column, scope_columns)
23
23
  end
24
24
 
25
25
  before_update do
26
- acts_as_positioned_update(column, scope_columns)
26
+ aap_update_position(column, scope_columns)
27
27
  end
28
28
  end
29
29
  end
30
30
 
31
31
  private
32
32
 
33
- def acts_as_positioned_create(column, scope_columns)
34
- scope = acts_as_positioned_scope(column, scope_columns)
33
+ def aap_insert_position(column, scope_columns)
34
+ scope = aap_scope(column, scope_columns)
35
35
  scope.where(scope.arel_table[column].gteq(send(column))).update_all("#{column} = #{column} + 1")
36
36
  end
37
37
 
38
- def acts_as_positioned_destroy(column, scope_columns)
39
- scope = acts_as_positioned_scope(column, scope_columns, true)
38
+ def aap_remove_position(column, scope_columns)
39
+ scope = aap_scope(column, scope_columns, true)
40
40
  scope.where(scope.arel_table[column].gt(send("#{column}_was"))).update_all("#{column} = #{column} - 1")
41
41
  end
42
42
 
43
- def acts_as_positioned_scope(column, scope_columns, use_old_values = false)
44
- scope_columns.reduce(self.class.base_class.where.not(column => nil)) do |scope, scope_column|
45
- scope.where(scope_column => use_old_values ? send("#{scope_column}_was") : send(scope_column))
46
- end
43
+ def aap_scope(column, scope_columns, use_old_values = false)
44
+ # When using the old values, make sure to overwrite the attribute values with the old values.
45
+ attrs = use_old_values ? attributes.merge(changed_attributes) : attributes
46
+ self.class.base_class.where(attrs.slice(*scope_columns)).where.not(column => nil)
47
47
  end
48
48
 
49
- def acts_as_positioned_update(column, scope_columns)
50
- if scope_columns.any? { |scope_column| send("#{scope_column}_changed?") }
51
- acts_as_positioned_create(column, scope_columns)
52
- acts_as_positioned_destroy(column, scope_columns)
49
+ def aap_switch_positions(column, scope_columns)
50
+ old_value, new_value = changes[column]
51
+ from, to, sign = old_value < new_value ? [old_value + 1, new_value, '-'] : [new_value, old_value - 1, '+']
53
52
 
54
- elsif send(:"#{column}_changed?")
55
- old_value, new_value = send("#{column}_change")
53
+ scope = aap_scope(column, scope_columns)
54
+ statement = "#{column} = #{column} #{sign} 1"
55
+ statement = ", #{locking_column} = #{locking_column} + 1" if scope.locking_enabled?
56
+ scope.where(column => from.eql?(to) ? from : from..to).update_all(statement)
57
+ end
56
58
 
57
- # If the new position becomes nil (and thus the old position wasn't), it should destroy a position.
58
- if new_value.nil?
59
- acts_as_positioned_destroy(column, scope_columns)
59
+ def aap_update_position(column, scope_columns)
60
+ if (changes.keys & scope_columns).present?
61
+ aap_insert_position(column, scope_columns)
62
+ aap_remove_position(column, scope_columns)
60
63
 
61
- # If the old position was nil (and thus the new position isn't), it should insert a position.
62
- elsif old_value.nil?
63
- acts_as_positioned_create(column, scope_columns)
64
+ elsif changes.key?(column)
65
+ # If the position was nil, it should insert a position.
66
+ if changes[column][0].nil?
67
+ aap_insert_position(column, scope_columns)
64
68
 
65
- else
66
- from, to, sign = old_value < new_value ? [old_value + 1, new_value, '-'] : [new_value, old_value - 1, '+']
69
+ # If the position becomes nil, it should remove a position.
70
+ elsif changes[column][1].nil?
71
+ aap_remove_position(column, scope_columns)
67
72
 
68
- acts_as_positioned_scope(column, scope_columns)
69
- .where(column => from.eql?(to) ? from : from..to)
70
- .update_all("#{column} = #{column} #{sign} 1")
73
+ else
74
+ aap_switch_positions(column, scope_columns)
71
75
  end
72
76
  end
73
77
  end
74
78
 
75
- def acts_as_positioned_validation(column, scope_columns)
76
- return if errors[column].any?
77
-
78
- scope = acts_as_positioned_scope(column, scope_columns)
79
- scope = scope.where(scope.arel_table[scope.primary_key].not_eq(id)) unless new_record?
80
- options = { attributes: column, allow_nil: true, only_integer: true, greater_than_or_equal_to: 0,
81
- less_than_or_equal_to: (scope.maximum(column) || -1) + 1 }
79
+ def aap_validate_position(column, scope_columns)
80
+ return if errors[column].present? || (changes.keys & ([column] + scope_columns)).empty?
82
81
 
82
+ scope = aap_scope(column, scope_columns)
83
+ options = { attributes: column, allow_nil: true, greater_than_or_equal_to: 0, only_integer: true,
84
+ less_than_or_equal_to: scope.where.not(scope.primary_key => id).count }
83
85
  ActiveModel::Validations::NumericalityValidator.new(options).validate(self)
84
86
  end
85
87
  end
@@ -0,0 +1,168 @@
1
+ require('active_record')
2
+ require('minitest')
3
+ require('minitest/autorun')
4
+ require_relative('../lib/acts_as_positioned')
5
+
6
+ # Test by running: ruby test/acts_as_positioned_test.rb
7
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: File.dirname(__FILE__) + '/test.sqlite3')
8
+
9
+ # Create "posts" table.
10
+ ActiveRecord::Schema.define do
11
+ create_table(:posts, force: true) do |t|
12
+ t.string(:text, null: false)
13
+ t.integer(:position)
14
+ t.integer(:author_id)
15
+ end
16
+ end
17
+
18
+ # Create "animals" table.
19
+ ActiveRecord::Schema.define do
20
+ create_table(:animals, force: true) do |t|
21
+ t.string(:type, null: false)
22
+ t.integer(:ordering)
23
+ end
24
+ end
25
+
26
+ class BasicPost < ActiveRecord::Base
27
+ self.table_name = 'posts'
28
+ acts_as_positioned
29
+ end
30
+
31
+ # Basic tests.
32
+ class BasicTests < Minitest::Test
33
+ def setup
34
+ BasicPost.delete_all
35
+ @post1 = BasicPost.create(text: '1st post', position: 0)
36
+ @post2 = BasicPost.create(text: '2nd post', position: 1)
37
+ @post3 = BasicPost.create(text: '3rd post', position: 2)
38
+ end
39
+
40
+ def test_insert_record
41
+ post4 = BasicPost.create(text: '4th post', position: 1)
42
+ assert_equal(post4.position, 1)
43
+ assert_equal(@post1.reload.position, 0)
44
+ assert_equal(@post2.reload.position, 2)
45
+ assert_equal(@post3.reload.position, 3)
46
+ end
47
+
48
+ def test_delete_record
49
+ @post2.destroy
50
+ assert_equal(@post1.reload.position, 0)
51
+ assert_equal(@post3.reload.position, 1)
52
+ end
53
+
54
+ def test_move_record_downwards
55
+ @post1.update(position: 1)
56
+ assert_equal(@post1.reload.position, 1)
57
+ assert_equal(@post2.reload.position, 0)
58
+ assert_equal(@post3.reload.position, 2)
59
+ end
60
+
61
+ def test_move_record_upwards
62
+ @post3.update(position: 0)
63
+ assert_equal(@post1.reload.position, 1)
64
+ assert_equal(@post2.reload.position, 2)
65
+ assert_equal(@post3.reload.position, 0)
66
+ end
67
+
68
+ def test_move_record_downwards_and_upwards
69
+ @post1.update(position: 1)
70
+ @post1.update(position: 0)
71
+ assert_equal(@post1.reload.position, 0)
72
+ assert_equal(@post2.reload.position, 1)
73
+ assert_equal(@post3.reload.position, 2)
74
+ end
75
+
76
+ def test_insert_record_without_position_should_do_nothing
77
+ post4 = BasicPost.create(text: '4th post')
78
+ assert_nil(post4.position)
79
+ assert_equal(@post1.reload.position, 0)
80
+ assert_equal(@post2.reload.position, 1)
81
+ assert_equal(@post3.reload.position, 2)
82
+ end
83
+
84
+ def test_clear_position
85
+ @post1.update(position: nil)
86
+ assert_equal(@post2.reload.position, 0)
87
+ assert_equal(@post3.reload.position, 1)
88
+ end
89
+
90
+ def test_fill_position
91
+ post4 = BasicPost.create(text: '4th post')
92
+ assert_nil(post4.position)
93
+ assert_equal(@post1.reload.position, 0)
94
+ assert_equal(@post2.reload.position, 1)
95
+ assert_equal(@post3.reload.position, 2)
96
+
97
+ post4.update(position: 1)
98
+ assert_equal(post4.position, 1)
99
+ assert_equal(@post1.reload.position, 0)
100
+ assert_equal(@post2.reload.position, 2)
101
+ assert_equal(@post3.reload.position, 3)
102
+ end
103
+
104
+ def test_update_record_without_position_should_do_nothing
105
+ post4 = BasicPost.create(text: 'Initial text', position: 0)
106
+ assert_equal(post4.reload.position, 0)
107
+ post4.update(text: 'Changed text')
108
+ assert_equal(post4.reload.position, 0)
109
+ end
110
+
111
+ # First record that gets created, must have position 0.
112
+ def test_validation
113
+ post4 = BasicPost.new(text: 'Post', position: 4)
114
+ assert_equal(post4.valid?, false)
115
+ end
116
+ end
117
+
118
+ class ScopedPost < ActiveRecord::Base
119
+ self.table_name = 'posts'
120
+ acts_as_positioned(scope: :author_id)
121
+ end
122
+
123
+ # Scoped model tests.
124
+ class ScopedTests < Minitest::Test
125
+ def setup
126
+ ScopedPost.delete_all
127
+ @post1 = ScopedPost.create(text: '1st post', position: 0)
128
+ @post2 = ScopedPost.create(text: '1nd post for author 1', position: 0, author_id: 1)
129
+ @post3 = ScopedPost.create(text: '2nd post for author 1', position: 1, author_id: 1)
130
+ end
131
+
132
+ def test_insert_record_with_scope_column
133
+ post4 = ScopedPost.create(text: '3th post for author 1', position: 1, author_id: 1)
134
+ assert_equal(post4.position, 1)
135
+ assert_equal(@post1.reload.position, 0)
136
+ assert_equal(@post2.reload.position, 0)
137
+ assert_equal(@post3.reload.position, 2)
138
+ end
139
+
140
+ def test_reposition_when_author_changes
141
+ @post2.update(author_id: nil)
142
+ assert_equal(@post2.position, 0)
143
+ assert_equal(@post1.reload.position, 1)
144
+ assert_equal(@post3.reload.position, 0)
145
+ end
146
+ end
147
+
148
+ # Animal base class to test single table inheritance.
149
+ class Animal < ActiveRecord::Base
150
+ acts_as_positioned(column: :ordering)
151
+ end
152
+
153
+ # Cat sub class to test single table inheritance.
154
+ class Cat < Animal
155
+ end
156
+
157
+ # Dog sub class to test single table inheritance.
158
+ class Dog < Animal
159
+ end
160
+
161
+ class SingleTableInheritanceTests < Minitest::Test
162
+ def test_single_table_inheritance
163
+ cat = Cat.create(ordering: 0)
164
+ dog = Dog.create(ordering: 0)
165
+ assert_equal(dog.ordering, 0)
166
+ assert_equal(cat.reload.ordering, 1)
167
+ end
168
+ end
data/test/test.sqlite3 ADDED
Binary file
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: acts_as_positioned
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Walter Horstman
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-05-23 00:00:00.000000000 Z
11
+ date: 2017-06-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -91,7 +91,8 @@ files:
91
91
  - README.md
92
92
  - Rakefile
93
93
  - lib/acts_as_positioned.rb
94
- - test/lib/acts_as_positioned_test.rb
94
+ - test/acts_as_positioned_test.rb
95
+ - test/test.sqlite3
95
96
  homepage: http://github.com/walterhorstman/acts_as_positioned
96
97
  licenses: []
97
98
  metadata: {}
@@ -116,4 +117,5 @@ signing_key:
116
117
  specification_version: 4
117
118
  summary: Lightweight ordering of models in ActiveRecord 3 or higher
118
119
  test_files:
119
- - test/lib/acts_as_positioned_test.rb
120
+ - test/acts_as_positioned_test.rb
121
+ - test/test.sqlite3
@@ -1,179 +0,0 @@
1
- require('active_record')
2
- require('minitest')
3
- require('minitest/autorun')
4
- require('acts_as_positioned')
5
-
6
- # Test by running: ruby -Ilib test/lib/acts_as_positioned_test.rb
7
- ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: File.dirname(__FILE__) + '/../test.sqlite3')
8
-
9
- # Create "posts" table.
10
- ActiveRecord::Schema.define do
11
- create_table(:posts, force: true) do |t|
12
- t.string(:text, null: false)
13
- t.integer(:position)
14
- t.integer(:author_id)
15
- end
16
- end
17
-
18
- # Create "animals" table.
19
- ActiveRecord::Schema.define do
20
- create_table(:animals, force: true) do |t|
21
- t.string(:type, null: false)
22
- t.string(:sound, null: false)
23
- t.integer(:ordering)
24
- end
25
- end
26
-
27
- class Post < ActiveRecord::Base
28
- acts_as_positioned
29
- end
30
-
31
- class PostWithScope < ActiveRecord::Base
32
- self.table_name = 'posts'
33
- acts_as_positioned(scope: :author_id)
34
- end
35
-
36
- class Animal < ActiveRecord::Base
37
- acts_as_positioned(column: :ordering)
38
- end
39
-
40
- class Cat < Animal
41
- end
42
-
43
- class Dog < Animal
44
- end
45
-
46
- class ActsAsPositionedTest < Minitest::Test
47
- def setup
48
- Post.delete_all
49
- end
50
-
51
- def test_insert_record
52
- post1 = Post.create(text: '1st post', position: 0)
53
- post2 = Post.create(text: '2nd post', position: 1)
54
- post3 = Post.create(text: '3rd post', position: 2)
55
- post4 = Post.create(text: '4th post', position: 1)
56
- assert_equal(post4.position, 1)
57
- assert_equal(post1.reload.position, 0)
58
- assert_equal(post2.reload.position, 2)
59
- assert_equal(post3.reload.position, 3)
60
- end
61
-
62
- def test_delete_record
63
- post1 = Post.create(text: '1st post', position: 0)
64
- post2 = Post.create(text: '2nd post', position: 1)
65
- post3 = Post.create(text: '3rd post', position: 2)
66
- post2.destroy
67
- assert_equal(post1.reload.position, 0)
68
- assert_equal(post3.reload.position, 1)
69
- end
70
-
71
- def test_move_record_downwards
72
- post1 = Post.create(text: '1st post', position: 0)
73
- post2 = Post.create(text: '2nd post', position: 1)
74
- post3 = Post.create(text: '3rd post', position: 2)
75
- post1.update(position: 1)
76
- assert_equal(post1.reload.position, 1)
77
- assert_equal(post2.reload.position, 0)
78
- assert_equal(post3.reload.position, 2)
79
- end
80
-
81
- def test_move_record_upwards
82
- post1 = Post.create(text: '1st post', position: 0)
83
- post2 = Post.create(text: '2nd post', position: 1)
84
- post3 = Post.create(text: '3rd post', position: 2)
85
- post3.update(position: 0)
86
- assert_equal(post1.reload.position, 1)
87
- assert_equal(post2.reload.position, 2)
88
- assert_equal(post3.reload.position, 0)
89
- end
90
-
91
- def test_move_record_downwards_and_upwards
92
- post1 = Post.create(text: '1st post', position: 0)
93
- post2 = Post.create(text: '2nd post', position: 1)
94
- post3 = Post.create(text: '3rd post', position: 2)
95
- post1.update(position: 1)
96
- post1.update(position: 0)
97
- assert_equal(post1.reload.position, 0)
98
- assert_equal(post2.reload.position, 1)
99
- assert_equal(post3.reload.position, 2)
100
- end
101
-
102
- def test_insert_record_without_position_should_do_nothing
103
- post1 = Post.create(text: '1st post', position: 0)
104
- post2 = Post.create(text: '2nd post', position: 1)
105
- post3 = Post.create(text: '3rd post', position: 2)
106
- post4 = Post.create(text: '4th post')
107
- assert_nil(post4.position)
108
- assert_equal(post1.reload.position, 0)
109
- assert_equal(post2.reload.position, 1)
110
- assert_equal(post3.reload.position, 2)
111
- end
112
-
113
- def test_clear_position
114
- post1 = Post.create(text: '1st post', position: 0)
115
- post2 = Post.create(text: '2nd post', position: 1)
116
- post3 = Post.create(text: '3rd post', position: 2)
117
- post1.update(position: nil)
118
- assert_equal(post2.reload.position, 0)
119
- assert_equal(post3.reload.position, 1)
120
- end
121
-
122
- def test_fill_position
123
- post1 = Post.create(text: '1st post', position: 0)
124
- post2 = Post.create(text: '2nd post', position: 1)
125
- post3 = Post.create(text: '3rd post', position: 2)
126
- post4 = Post.create(text: '4th post')
127
- assert_nil(post4.position)
128
- assert_equal(post1.reload.position, 0)
129
- assert_equal(post2.reload.position, 1)
130
- assert_equal(post3.reload.position, 2)
131
-
132
- post4.update(position: 1)
133
- assert_equal(post4.position, 1)
134
- assert_equal(post1.reload.position, 0)
135
- assert_equal(post2.reload.position, 2)
136
- assert_equal(post3.reload.position, 3)
137
- end
138
-
139
- def test_update_record_without_position_should_do_nothing
140
- post = Post.create(text: 'Initial text', position: 0)
141
- assert_equal(post.reload.position, 0)
142
- post.update(text: 'Changed text')
143
- assert_equal(post.reload.position, 0)
144
- end
145
-
146
- # First record that gets created, must have position 0.
147
- def test_validation
148
- post = Post.new(text: 'Post', position: 4)
149
- assert_equal(post.valid?, false)
150
- end
151
-
152
- def test_insert_record_with_scope_column
153
- post1 = PostWithScope.create(text: '1st post', position: 0)
154
- post2 = PostWithScope.create(text: '1nd post for author 1', position: 0, author_id: 1)
155
- post3 = PostWithScope.create(text: '2nd post for author 1', position: 1, author_id: 1)
156
- post4 = PostWithScope.create(text: '3th post for author 1', position: 1, author_id: 1)
157
- assert_equal(post4.position, 1)
158
- assert_equal(post1.reload.position, 0)
159
- assert_equal(post2.reload.position, 0)
160
- assert_equal(post3.reload.position, 2)
161
- end
162
-
163
- def test_reposition_when_author_changes
164
- post1 = PostWithScope.create(text: '1st post', position: 0)
165
- post2 = PostWithScope.create(text: '1nd post for author 1', position: 0, author_id: 1)
166
- post3 = PostWithScope.create(text: '2nd post for author 1', position: 1, author_id: 1)
167
- post2.update(author_id: nil)
168
- assert_equal(post2.position, 0)
169
- assert_equal(post1.reload.position, 1)
170
- assert_equal(post3.reload.position, 0)
171
- end
172
-
173
- def test_single_table_inheritance
174
- cat = Cat.create(sound: 'Miaow', ordering: 0)
175
- dog = Dog.create(sound: 'Bark', ordering: 0)
176
- assert_equal(dog.ordering, 0)
177
- assert_equal(cat.reload.ordering, 1)
178
- end
179
- end