acts_as_positioned 0.0.1 → 0.0.2

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: 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