sortifiable 0.1.0

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.
data/.gemtest ADDED
File without changes
data/CHANGELOG ADDED
@@ -0,0 +1,3 @@
1
+ *0.1.0 (February 6th, 2011)
2
+
3
+ * First release
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2011 Andrew White
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the "Software"),
5
+ to deal in the Software without restriction, including without limitation
6
+ the rights to use, copy, modify, merge, publish, distribute, sublicense,
7
+ and/or sell copies of the Software, and to permit persons to whom the
8
+ Software is furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be
11
+ included in all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
14
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
15
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
16
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
17
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
18
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
19
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,27 @@
1
+ == Sortifiable
2
+
3
+ This gem provides an acts_as_list compatible capability for sorting
4
+ and reordering a number of objects in a list. The class that has this
5
+ specified needs to have a +position+ column defined as an integer on
6
+ the mapped database table.
7
+
8
+ This gem requires ActiveRecord 3.0 as it has been refactored to use
9
+ the scope methods and query interface introduced with Ruby on Rails 3.0
10
+
11
+
12
+ === Example
13
+
14
+ class TodoList < ActiveRecord::Base
15
+ has_many :todo_items, :order => "position"
16
+ end
17
+
18
+ class TodoItem < ActiveRecord::Base
19
+ belongs_to :todo_list
20
+ acts_as_list :scope => :todo_list
21
+ end
22
+
23
+ todo_list.first.move_to_bottom
24
+ todo_list.last.move_higher
25
+
26
+
27
+ Copyright (c) 2011 Andrew White, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ require 'rake'
2
+ require 'rake/rdoctask'
3
+ require 'rake/testtask'
4
+ require 'bundler'
5
+
6
+ Bundler::GemHelper.install_tasks
7
+
8
+ desc 'Default: run sortifiable unit tests.'
9
+ task :default => :test
10
+
11
+ desc 'Test the sortifiable gem.'
12
+ Rake::TestTask.new(:test) do |t|
13
+ t.libs << 'lib'
14
+ t.pattern = 'test/**/*_test.rb'
15
+ t.verbose = true
16
+ end
17
+
18
+ desc 'Generate documentation for the sortifiable gem.'
19
+ Rake::RDocTask.new(:rdoc) do |rdoc|
20
+ rdoc.rdoc_dir = 'rdoc'
21
+ rdoc.title = 'Sortifiable'
22
+ rdoc.options << '--line-numbers' << '--inline-source'
23
+ rdoc.rdoc_files.include('README')
24
+ rdoc.rdoc_files.include('lib/**/*.rb')
25
+ end
@@ -0,0 +1,3 @@
1
+ module Sortifiable
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,267 @@
1
+ require 'active_support/concern'
2
+ require 'active_support/core_ext/array/wrap'
3
+ require 'active_support/core_ext/class/attribute'
4
+ require 'active_support/core_ext/hash/reverse_merge'
5
+ require 'active_record'
6
+ require 'sortifiable/version'
7
+
8
+ # This +acts_as+ extension provides the capabilities for sorting and
9
+ # reordering a number of objects in a list. The class that has this
10
+ # specified needs to have a +position+ column defined as an integer on
11
+ # the mapped database table.
12
+ #
13
+ # Todo list example:
14
+ #
15
+ # class TodoList < ActiveRecord::Base
16
+ # has_many :todo_items, :order => "position"
17
+ # end
18
+ #
19
+ # class TodoItem < ActiveRecord::Base
20
+ # belongs_to :todo_list
21
+ # acts_as_list :scope => :todo_list
22
+ # end
23
+ #
24
+ # todo_list.first.move_to_bottom
25
+ # todo_list.last.move_higher
26
+ module Sortifiable
27
+ extend ActiveSupport::Concern
28
+
29
+ included do
30
+ class_attribute :acts_as_list_options, :instance_writer => false
31
+ self.acts_as_list_options = {}
32
+
33
+ before_create :add_to_list_bottom
34
+ before_destroy :decrement_position_on_lower_items, :if => :in_list?
35
+ end
36
+
37
+ module ClassMethods
38
+ # Configuration options are:
39
+ #
40
+ # * +column+ - specifies the column name to use for keeping the
41
+ # position integer (default: +position+)
42
+ # * +scope+ - restricts what is to be considered a list. Given a symbol,
43
+ # it'll attach <tt>_id</tt> (if it hasn't already been added) and use
44
+ # that as the foreign key restriction. It's also possible to give it
45
+ # an entire string that is interpolated if you need a tighter scope
46
+ # than just a foreign key. Example:
47
+ #
48
+ # acts_as_list :scope => 'user_id = #{user_id} AND completed = 0'
49
+ #
50
+ # It can also be given an array of symbols or a belongs_to association.
51
+ def acts_as_list(options = {})
52
+ options.reverse_merge!(:scope => [], :column => :position)
53
+
54
+ if options[:scope].is_a?(Symbol) && reflections.key?(options[:scope])
55
+ reflection = reflections[options.delete(:scope)]
56
+
57
+ if reflection.belongs_to?
58
+ if reflection.options[:polymorphic]
59
+ options[:scope] = [
60
+ reflection.association_foreign_key.to_sym,
61
+ reflection.options[:foreign_type].to_sym
62
+ ]
63
+ else
64
+ reflection.association_foreign_key.to_sym
65
+ end
66
+ else
67
+ raise ArgumentError, "Only belongs_to associations can be used as a scope"
68
+ end
69
+ elsif options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
70
+ options[:scope] = "#{options[:scope]}_id".to_sym
71
+ end
72
+
73
+ self.acts_as_list_options = options
74
+ end
75
+ end
76
+
77
+ # All the methods available to a record that has had <tt>acts_as_list</tt>
78
+ # specified. Each method works by assuming the object to be the item in the
79
+ # list, so <tt>chapter.move_lower</tt> would move that chapter lower in the
80
+ # list of all chapters. Likewise, <tt>chapter.first?</tt> would return +true+
81
+ # if that chapter is the first in the list of all chapters.
82
+
83
+ # Add the item to the end of the list
84
+ def add_to_list
85
+ remove_from_list if in_list?
86
+ update_attribute(position_column, last_position + 1)
87
+ end
88
+
89
+ # Returns the current position
90
+ def current_position
91
+ send(position_column).to_i
92
+ end
93
+
94
+ # Decrease the position of this item without adjusting the rest of the list.
95
+ def decrement_position
96
+ in_list? && update_attribute(position_column, current_position - 1)
97
+ end
98
+
99
+ # Return +true+ if this object is the first in the list.
100
+ def first?
101
+ in_list? && current_position == 1
102
+ end
103
+ alias_method :top?, :first?
104
+
105
+ # Returns the first item in the list
106
+ def first_item
107
+ list_scope.first
108
+ end
109
+ alias_method :top_item, :first_item
110
+
111
+ # Return the next higher item in the list.
112
+ def higher_item
113
+ item_at_offset(-1)
114
+ end
115
+ alias_method :previous_item, :higher_item
116
+
117
+ # Return items lower than this item or an empty array if it is the last item
118
+ def higher_items
119
+ list_scope.where(["#{quoted_position_column} < ?", current_position]).all
120
+ end
121
+
122
+ # Test if this record is in a list
123
+ def in_list?
124
+ !new_record? && !send(position_column).nil?
125
+ end
126
+
127
+ # Increase the position of this item without adjusting the rest of the list.
128
+ def increment_position
129
+ in_list? && update_attribute(position_column, current_position + 1)
130
+ end
131
+
132
+ # Insert the item at the given position (defaults to the top position of 1).
133
+ def insert_at(position = 1)
134
+ if position > 0
135
+ remove_from_list
136
+ if position > last_position
137
+ add_to_list
138
+ else
139
+ increment_position_on_lower_items(position - 1)
140
+ update_attribute(position_column, position)
141
+ end
142
+ else
143
+ false
144
+ end
145
+ end
146
+
147
+ # Return the item at the offset specified from the current position
148
+ def item_at_offset(offset)
149
+ in_list? ? offset_scope(offset).first : nil
150
+ end
151
+
152
+ # Return +true+ if this object is the last in the list.
153
+ def last?
154
+ in_list? && current_position == last_position
155
+ end
156
+ alias_method :bottom?, :last?
157
+
158
+ # Returns the bottom item
159
+ def last_item
160
+ list_scope.last
161
+ end
162
+ alias_method :bottom_item, :last_item
163
+
164
+ # Returns the bottom position in the list.
165
+ def last_position
166
+ item = last_item
167
+ item ? item.current_position : 0
168
+ end
169
+ alias_method :bottom_position, :last_position
170
+
171
+ # Return the next lower item in the list.
172
+ def lower_item
173
+ item_at_offset(1)
174
+ end
175
+ alias_method :next_item, :lower_item
176
+
177
+ # Return items lower than this item or an empty array if it is the last item
178
+ def lower_items
179
+ list_scope.where(["#{quoted_position_column} > ?", current_position]).all
180
+ end
181
+
182
+ # Swap positions with the next higher item, if one exists.
183
+ def move_higher
184
+ in_list? && (first? || insert_at(current_position - 1))
185
+ end
186
+ alias_method :move_up, :move_higher
187
+
188
+ # Swap positions with the next lower item, if one exists.
189
+ def move_lower
190
+ in_list? && (last? || insert_at(current_position + 1))
191
+ end
192
+ alias_method :move_down, :move_lower
193
+
194
+ # Move to the bottom of the list. If the item is already in the list,
195
+ # the items below it have their position adjusted accordingly.
196
+ def move_to_bottom
197
+ in_list? && (last? || add_to_list)
198
+ end
199
+
200
+ # Move to the top of the list. If the item is already in the list,
201
+ # the items above it have their position adjusted accordingly.
202
+ def move_to_top
203
+ in_list? && (first? || insert_at(1))
204
+ end
205
+
206
+ # Removes the item from the list.
207
+ def remove_from_list
208
+ if in_list?
209
+ decrement_position_on_lower_items
210
+ update_attribute(position_column, nil)
211
+ else
212
+ false
213
+ end
214
+ end
215
+
216
+ private
217
+ def add_to_list_bottom #:nodoc:
218
+ send("#{position_column}=".to_sym, last_position + 1)
219
+ end
220
+
221
+ def base_scope #:nodoc:
222
+ self.class.unscoped.where(scope_condition)
223
+ end
224
+
225
+ def decrement_position_on_lower_items #:nodoc:
226
+ lower_scope(current_position).update_all(position_update('- 1'))
227
+ end
228
+
229
+ def increment_position_on_lower_items(position) #:nodoc:
230
+ lower_scope(position).update_all(position_update('+ 1'))
231
+ end
232
+
233
+ def list_scope #:nodoc:
234
+ base_scope.order(position_column).where("#{quoted_position_column} IS NOT NULL")
235
+ end
236
+
237
+ def lower_scope(position) #:nodoc:
238
+ base_scope.where(["#{quoted_position_column} > ?", position])
239
+ end
240
+
241
+ def offset_scope(offset) #:nodoc:
242
+ base_scope.where(position_column => current_position + offset)
243
+ end
244
+
245
+ def position_column #:nodoc:
246
+ acts_as_list_options[:column]
247
+ end
248
+
249
+ def position_update(direction) #:nodoc:
250
+ "#{quoted_position_column} = (#{quoted_position_column} #{direction})"
251
+ end
252
+
253
+ def quoted_position_column #:nodoc:
254
+ connection.quote_column_name(position_column)
255
+ end
256
+
257
+ def scope_condition #:nodoc:
258
+ if acts_as_list_options[:scope].is_a?(String)
259
+ instance_eval("\"#{acts_as_list_options[:scope]}\"")
260
+ else
261
+ Array.wrap(acts_as_list_options[:scope]).inject({}){ |m,k| m[k] = send(k); m }
262
+ end
263
+ end
264
+
265
+ end
266
+
267
+ ActiveRecord::Base.send(:include, Sortifiable)
@@ -0,0 +1,42 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "sortifiable/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "sortifiable"
7
+ s.version = Sortifiable::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Andrew White"]
10
+ s.email = ["andyw@pixeltrix.co.uk"]
11
+ s.homepage = %q{http://github.com/pixeltrix/sortifiable/}
12
+ s.summary = %q{Sort your models}
13
+ s.description = <<-EOF
14
+ This gem provides an acts_as_list compatible capability for sorting
15
+ and reordering a number of objects in a list. The class that has this
16
+ specified needs to have a +position+ column defined as an integer on
17
+ the mapped database table.
18
+
19
+ This gem requires ActiveRecord 3.0 as it has been refactored to use
20
+ the scope methods and query interface introduced with Ruby on Rails 3.0
21
+ EOF
22
+
23
+ s.files = [
24
+ ".gemtest",
25
+ "CHANGELOG",
26
+ "LICENSE",
27
+ "README",
28
+ "Rakefile",
29
+ "lib/sortifiable.rb",
30
+ "lib/sortifiable/version.rb",
31
+ "sortifiable.gemspec",
32
+ "test/sortifiable_test.rb"
33
+ ]
34
+
35
+ s.test_files = ["test/sortifiable_test.rb"]
36
+ s.require_paths = ["lib"]
37
+
38
+ s.add_dependency "activesupport", "~> 3.0.3"
39
+ s.add_dependency "activerecord", "~> 3.0.3"
40
+ s.add_development_dependency "bundler", "~> 1.0.10"
41
+ s.add_development_dependency "sqlite3", "~> 1.3.3"
42
+ end
@@ -0,0 +1,566 @@
1
+ require 'test/unit'
2
+ require 'rubygems'
3
+ require 'active_record'
4
+ require 'active_support/core_ext/kernel/reporting'
5
+ require 'sortifiable'
6
+
7
+ ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
8
+
9
+ def setup_db
10
+ silence_stream(STDOUT) do
11
+ ActiveRecord::Schema.define(:version => 1) do
12
+ create_table :mixins do |t|
13
+ t.column :pos, :integer
14
+ t.column :parent_id, :integer
15
+ t.column :parent_type, :string
16
+ t.column :created_at, :datetime
17
+ t.column :updated_at, :datetime
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ def teardown_db
24
+ ActiveRecord::Base.connection.tables.each do |table|
25
+ ActiveRecord::Base.connection.drop_table(table)
26
+ end
27
+ end
28
+
29
+ setup_db
30
+
31
+ class Mixin < ActiveRecord::Base
32
+ end
33
+
34
+ class ListMixin < Mixin
35
+ acts_as_list :column => "pos", :scope => :parent
36
+ set_table_name "mixins"
37
+ default_scope order(:pos)
38
+ end
39
+
40
+ class ListMixinSub1 < ListMixin
41
+ end
42
+
43
+ class ListMixinSub2 < ListMixin
44
+ end
45
+
46
+ class ListWithStringScopeMixin < ActiveRecord::Base
47
+ acts_as_list :column => "pos", :scope => 'parent_id = #{parent_id}'
48
+ set_table_name "mixins"
49
+ default_scope order(:pos)
50
+ end
51
+
52
+ class ArrayScopeListMixin < Mixin
53
+ acts_as_list :column => "pos", :scope => [:parent_id, :parent_type]
54
+ set_table_name "mixins"
55
+ default_scope order(:pos)
56
+ end
57
+
58
+ teardown_db
59
+
60
+ class ListTest < Test::Unit::TestCase
61
+
62
+ def setup
63
+ setup_db
64
+ (1..4).each { |counter| ListMixin.create! :pos => counter, :parent_id => 5 }
65
+ end
66
+
67
+ def teardown
68
+ teardown_db
69
+ end
70
+
71
+ def test_reordering
72
+ assert_equal [1, 2, 3, 4], ListMixin.where('parent_id = 5').map(&:id)
73
+
74
+ ListMixin.find(2).move_lower
75
+ assert_equal [1, 3, 2, 4], ListMixin.where('parent_id = 5').map(&:id)
76
+
77
+ ListMixin.find(2).move_higher
78
+ assert_equal [1, 2, 3, 4], ListMixin.where('parent_id = 5').map(&:id)
79
+
80
+ ListMixin.find(1).move_to_bottom
81
+ assert_equal [2, 3, 4, 1], ListMixin.where('parent_id = 5').map(&:id)
82
+
83
+ ListMixin.find(1).move_to_top
84
+ assert_equal [1, 2, 3, 4], ListMixin.where('parent_id = 5').map(&:id)
85
+
86
+ ListMixin.find(2).move_to_bottom
87
+ assert_equal [1, 3, 4, 2], ListMixin.where('parent_id = 5').map(&:id)
88
+
89
+ ListMixin.find(4).move_to_top
90
+ assert_equal [4, 1, 3, 2], ListMixin.where('parent_id = 5').map(&:id)
91
+ end
92
+
93
+ def test_move_to_bottom_with_next_to_last_item
94
+ assert_equal [1, 2, 3, 4], ListMixin.where('parent_id = 5').map(&:id)
95
+ ListMixin.find(3).move_to_bottom
96
+ assert_equal [1, 2, 4, 3], ListMixin.where('parent_id = 5').map(&:id)
97
+ end
98
+
99
+ def test_next_prev
100
+ assert_equal ListMixin.find(2), ListMixin.find(1).lower_item
101
+ assert_nil ListMixin.find(1).higher_item
102
+ assert_equal ListMixin.find(3), ListMixin.find(4).higher_item
103
+ assert_nil ListMixin.find(4).lower_item
104
+ end
105
+
106
+ def test_injection
107
+ item = ListMixin.new(:parent_id => 1)
108
+ assert_equal({ :parent_id => 1 }, item.send(:scope_condition))
109
+ assert_equal("pos", item.send(:position_column))
110
+ end
111
+
112
+ def test_insert
113
+ new = ListMixin.create(:parent_id => 20)
114
+ assert_equal 1, new.pos
115
+ assert new.first?
116
+ assert new.last?
117
+
118
+ new = ListMixin.create(:parent_id => 20)
119
+ assert_equal 2, new.pos
120
+ assert !new.first?
121
+ assert new.last?
122
+
123
+ new = ListMixin.create(:parent_id => 20)
124
+ assert_equal 3, new.pos
125
+ assert !new.first?
126
+ assert new.last?
127
+
128
+ new = ListMixin.create(:parent_id => 0)
129
+ assert_equal 1, new.pos
130
+ assert new.first?
131
+ assert new.last?
132
+ end
133
+
134
+ def test_insert_at
135
+ new = ListMixin.create(:parent_id => 20)
136
+ assert_equal 1, new.pos
137
+
138
+ new = ListMixin.create(:parent_id => 20)
139
+ assert_equal 2, new.pos
140
+
141
+ new = ListMixin.create(:parent_id => 20)
142
+ assert_equal 3, new.pos
143
+
144
+ new4 = ListMixin.create(:parent_id => 20)
145
+ assert_equal 4, new4.pos
146
+
147
+ new4.insert_at(3)
148
+ assert_equal 3, new4.pos
149
+
150
+ new.reload
151
+ assert_equal 4, new.pos
152
+
153
+ new.insert_at(2)
154
+ assert_equal 2, new.pos
155
+
156
+ new4.reload
157
+ assert_equal 4, new4.pos
158
+
159
+ new5 = ListMixin.create(:parent_id => 20)
160
+ assert_equal 5, new5.pos
161
+
162
+ new5.insert_at(1)
163
+ assert_equal 1, new5.pos
164
+
165
+ new4.reload
166
+ assert_equal 5, new4.pos
167
+ end
168
+
169
+ def test_delete_middle
170
+ assert_equal [1, 2, 3, 4], ListMixin.where('parent_id = 5').map(&:id)
171
+
172
+ ListMixin.find(2).destroy
173
+
174
+ assert_equal [1, 3, 4], ListMixin.where('parent_id = 5').map(&:id)
175
+
176
+ assert_equal 1, ListMixin.find(1).pos
177
+ assert_equal 2, ListMixin.find(3).pos
178
+ assert_equal 3, ListMixin.find(4).pos
179
+
180
+ ListMixin.find(1).destroy
181
+
182
+ assert_equal [3, 4], ListMixin.where('parent_id = 5').map(&:id)
183
+
184
+ assert_equal 1, ListMixin.find(3).pos
185
+ assert_equal 2, ListMixin.find(4).pos
186
+ end
187
+
188
+ def test_with_string_based_scope
189
+ new = ListWithStringScopeMixin.create(:parent_id => 500)
190
+ assert_equal 1, new.pos
191
+ assert new.first?
192
+ assert new.last?
193
+ end
194
+
195
+ def test_nil_scope
196
+ new1, new2, new3 = ListMixin.create, ListMixin.create, ListMixin.create
197
+ new2.move_higher
198
+ assert_equal [new2, new1, new3], ListMixin.where('parent_id IS NULL')
199
+ end
200
+
201
+ def test_remove_from_list_should_then_fail_in_list?
202
+ assert_equal true, ListMixin.find(1).in_list?
203
+ ListMixin.find(1).remove_from_list
204
+ assert_equal false, ListMixin.find(1).in_list?
205
+ end
206
+
207
+ def test_remove_from_list_should_set_position_to_nil
208
+ assert_equal [1, 2, 3, 4], ListMixin.where('parent_id = 5').map(&:id)
209
+
210
+ ListMixin.find(2).remove_from_list
211
+
212
+ assert_equal [2, 1, 3, 4], ListMixin.where('parent_id = 5').map(&:id)
213
+
214
+ assert_equal 1, ListMixin.find(1).pos
215
+ assert_equal nil, ListMixin.find(2).pos
216
+ assert_equal 2, ListMixin.find(3).pos
217
+ assert_equal 3, ListMixin.find(4).pos
218
+ end
219
+
220
+ def test_remove_before_destroy_does_not_shift_lower_items_twice
221
+ assert_equal [1, 2, 3, 4], ListMixin.where('parent_id = 5').map(&:id)
222
+
223
+ ListMixin.find(2).remove_from_list
224
+ ListMixin.find(2).destroy
225
+
226
+ assert_equal [1, 3, 4], ListMixin.where('parent_id = 5').map(&:id)
227
+
228
+ assert_equal 1, ListMixin.find(1).pos
229
+ assert_equal 2, ListMixin.find(3).pos
230
+ assert_equal 3, ListMixin.find(4).pos
231
+ end
232
+
233
+ def test_before_destroy_callbacks_do_not_update_position_to_nil_before_deleting_the_record
234
+ assert_equal [1, 2, 3, 4], ListMixin.where('parent_id = 5').map(&:id)
235
+
236
+ # We need to trigger all the before_destroy callbacks without actually
237
+ # destroying the record so we can see the affect the callbacks have on
238
+ # the record.
239
+ list = ListMixin.find(2)
240
+ if list.respond_to?(:run_callbacks)
241
+ list.run_callbacks(:destroy)
242
+ else
243
+ list.send(:callback, :before_destroy)
244
+ end
245
+
246
+ assert_equal [1, 2, 3, 4], ListMixin.where('parent_id = 5').map(&:id)
247
+
248
+ assert_equal 1, ListMixin.find(1).pos
249
+ assert_equal 2, ListMixin.find(2).pos
250
+ assert_equal 2, ListMixin.find(3).pos
251
+ assert_equal 3, ListMixin.find(4).pos
252
+ end
253
+
254
+ def test_higher_items
255
+ assert_equal [1, 2], ListMixin.find(3).higher_items.map(&:id)
256
+ assert_equal [], ListMixin.find(1).higher_items.map(&:id)
257
+ end
258
+
259
+ def test_lower_items
260
+ assert_equal [3, 4], ListMixin.find(2).lower_items.map(&:id)
261
+ assert_equal [], ListMixin.find(4).lower_items.map(&:id)
262
+ end
263
+
264
+ end
265
+
266
+ class ListSubTest < Test::Unit::TestCase
267
+
268
+ def setup
269
+ setup_db
270
+ (1..4).each { |i| ((i % 2 == 1) ? ListMixinSub1 : ListMixinSub2).create! :pos => i, :parent_id => 5000 }
271
+ end
272
+
273
+ def teardown
274
+ teardown_db
275
+ end
276
+
277
+ def test_reordering
278
+ assert_equal [1, 2, 3, 4], ListMixin.where('parent_id = 5000').map(&:id)
279
+
280
+ ListMixin.find(2).move_lower
281
+ assert_equal [1, 3, 2, 4], ListMixin.where('parent_id = 5000').map(&:id)
282
+
283
+ ListMixin.find(2).move_higher
284
+ assert_equal [1, 2, 3, 4], ListMixin.where('parent_id = 5000').map(&:id)
285
+
286
+ ListMixin.find(1).move_to_bottom
287
+ assert_equal [2, 3, 4, 1], ListMixin.where('parent_id = 5000').map(&:id)
288
+
289
+ ListMixin.find(1).move_to_top
290
+ assert_equal [1, 2, 3, 4], ListMixin.where('parent_id = 5000').map(&:id)
291
+
292
+ ListMixin.find(2).move_to_bottom
293
+ assert_equal [1, 3, 4, 2], ListMixin.where('parent_id = 5000').map(&:id)
294
+
295
+ ListMixin.find(4).move_to_top
296
+ assert_equal [4, 1, 3, 2], ListMixin.where('parent_id = 5000').map(&:id)
297
+ end
298
+
299
+ def test_move_to_bottom_with_next_to_last_item
300
+ assert_equal [1, 2, 3, 4], ListMixin.where('parent_id = 5000').map(&:id)
301
+ ListMixin.find(3).move_to_bottom
302
+ assert_equal [1, 2, 4, 3], ListMixin.where('parent_id = 5000').map(&:id)
303
+ end
304
+
305
+ def test_next_prev
306
+ assert_equal ListMixin.find(2), ListMixin.find(1).lower_item
307
+ assert_nil ListMixin.find(1).higher_item
308
+ assert_equal ListMixin.find(3), ListMixin.find(4).higher_item
309
+ assert_nil ListMixin.find(4).lower_item
310
+ end
311
+
312
+ def test_injection
313
+ item = ListMixin.new("parent_id"=>1)
314
+ assert_equal({ :parent_id => 1 }, item.send(:scope_condition))
315
+ assert_equal("pos", item.send(:position_column))
316
+ end
317
+
318
+ def test_insert_at
319
+ new = ListMixin.create("parent_id" => 20)
320
+ assert_equal 1, new.pos
321
+
322
+ new = ListMixinSub1.create("parent_id" => 20)
323
+ assert_equal 2, new.pos
324
+
325
+ new = ListMixinSub2.create("parent_id" => 20)
326
+ assert_equal 3, new.pos
327
+
328
+ new4 = ListMixin.create("parent_id" => 20)
329
+ assert_equal 4, new4.pos
330
+
331
+ new4.insert_at(3)
332
+ assert_equal 3, new4.pos
333
+
334
+ new.reload
335
+ assert_equal 4, new.pos
336
+
337
+ new.insert_at(2)
338
+ assert_equal 2, new.pos
339
+
340
+ new4.reload
341
+ assert_equal 4, new4.pos
342
+
343
+ new5 = ListMixinSub1.create("parent_id" => 20)
344
+ assert_equal 5, new5.pos
345
+
346
+ new5.insert_at(1)
347
+ assert_equal 1, new5.pos
348
+
349
+ new4.reload
350
+ assert_equal 5, new4.pos
351
+ end
352
+
353
+ def test_delete_middle
354
+ assert_equal [1, 2, 3, 4], ListMixin.where('parent_id = 5000').map(&:id)
355
+
356
+ ListMixin.find(2).destroy
357
+
358
+ assert_equal [1, 3, 4], ListMixin.where('parent_id = 5000').map(&:id)
359
+
360
+ assert_equal 1, ListMixin.find(1).pos
361
+ assert_equal 2, ListMixin.find(3).pos
362
+ assert_equal 3, ListMixin.find(4).pos
363
+
364
+ ListMixin.find(1).destroy
365
+
366
+ assert_equal [3, 4], ListMixin.where('parent_id = 5000').map(&:id)
367
+
368
+ assert_equal 1, ListMixin.find(3).pos
369
+ assert_equal 2, ListMixin.find(4).pos
370
+ end
371
+
372
+ def test_higher_items
373
+ ListMixin.find(2).remove_from_list
374
+ assert_equal [1], ListMixin.find(3).higher_items.map(&:id)
375
+ assert_equal [], ListMixin.find(1).higher_items.map(&:id)
376
+ end
377
+
378
+ def test_lower_items
379
+ ListMixin.find(3).remove_from_list
380
+ assert_equal [4], ListMixin.find(2).lower_items.map(&:id)
381
+ assert_equal [], ListMixin.find(4).lower_items.map(&:id)
382
+ end
383
+
384
+ end
385
+
386
+ class ArrayScopeListTest < Test::Unit::TestCase
387
+
388
+ def setup
389
+ setup_db
390
+ (1..4).each do |counter|
391
+ ArrayScopeListMixin.create!(
392
+ :pos => counter,
393
+ :parent_id => 5,
394
+ :parent_type => 'ParentClass'
395
+ )
396
+ end
397
+ end
398
+
399
+ def teardown
400
+ teardown_db
401
+ end
402
+
403
+ def conditions(options = {})
404
+ { :parent_id => 5, :parent_type => 'ParentClass' }.merge(options)
405
+ end
406
+
407
+ def test_reordering
408
+ assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(conditions).map(&:id)
409
+
410
+ ArrayScopeListMixin.find(2).move_lower
411
+ assert_equal [1, 3, 2, 4], ArrayScopeListMixin.where(conditions).map(&:id)
412
+
413
+ ArrayScopeListMixin.find(2).move_higher
414
+ assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(conditions).map(&:id)
415
+
416
+ ArrayScopeListMixin.find(1).move_to_bottom
417
+ assert_equal [2, 3, 4, 1], ArrayScopeListMixin.where(conditions).map(&:id)
418
+
419
+ ArrayScopeListMixin.find(1).move_to_top
420
+ assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(conditions).map(&:id)
421
+
422
+ ArrayScopeListMixin.find(2).move_to_bottom
423
+ assert_equal [1, 3, 4, 2], ArrayScopeListMixin.where(conditions).map(&:id)
424
+
425
+ ArrayScopeListMixin.find(4).move_to_top
426
+ assert_equal [4, 1, 3, 2], ArrayScopeListMixin.where(conditions).map(&:id)
427
+ end
428
+
429
+ def test_move_to_bottom_with_next_to_last_item
430
+ assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(conditions).map(&:id)
431
+ ArrayScopeListMixin.find(3).move_to_bottom
432
+ assert_equal [1, 2, 4, 3], ArrayScopeListMixin.where(conditions).map(&:id)
433
+ end
434
+
435
+ def test_next_prev
436
+ assert_equal ArrayScopeListMixin.find(2), ArrayScopeListMixin.find(1).lower_item
437
+ assert_nil ArrayScopeListMixin.find(1).higher_item
438
+ assert_equal ArrayScopeListMixin.find(3), ArrayScopeListMixin.find(4).higher_item
439
+ assert_nil ArrayScopeListMixin.find(4).lower_item
440
+ end
441
+
442
+ def test_injection
443
+ item = ArrayScopeListMixin.new(conditions)
444
+ assert_equal(conditions, item.send(:scope_condition))
445
+ assert_equal("pos", item.send(:position_column))
446
+ end
447
+
448
+ def test_insert
449
+ new = ArrayScopeListMixin.create(conditions(:parent_id => 20))
450
+ assert_equal 1, new.pos
451
+ assert new.first?
452
+ assert new.last?
453
+
454
+ new = ArrayScopeListMixin.create(conditions(:parent_id => 20))
455
+ assert_equal 2, new.pos
456
+ assert !new.first?
457
+ assert new.last?
458
+
459
+ new = ArrayScopeListMixin.create(conditions(:parent_id => 20))
460
+ assert_equal 3, new.pos
461
+ assert !new.first?
462
+ assert new.last?
463
+
464
+ new = ArrayScopeListMixin.create(conditions(:parent_id => 0))
465
+ assert_equal 1, new.pos
466
+ assert new.first?
467
+ assert new.last?
468
+ end
469
+
470
+ def test_insert_at
471
+ new = ArrayScopeListMixin.create(conditions(:parent_id => 20))
472
+ assert_equal 1, new.pos
473
+
474
+ new = ArrayScopeListMixin.create(conditions(:parent_id => 20))
475
+ assert_equal 2, new.pos
476
+
477
+ new = ArrayScopeListMixin.create(conditions(:parent_id => 20))
478
+ assert_equal 3, new.pos
479
+
480
+ new4 = ArrayScopeListMixin.create(conditions(:parent_id => 20))
481
+ assert_equal 4, new4.pos
482
+
483
+ new4.insert_at(3)
484
+ assert_equal 3, new4.pos
485
+
486
+ new.reload
487
+ assert_equal 4, new.pos
488
+
489
+ new.insert_at(2)
490
+ assert_equal 2, new.pos
491
+
492
+ new4.reload
493
+ assert_equal 4, new4.pos
494
+
495
+ new5 = ArrayScopeListMixin.create(conditions(:parent_id => 20))
496
+ assert_equal 5, new5.pos
497
+
498
+ new5.insert_at(1)
499
+ assert_equal 1, new5.pos
500
+
501
+ new4.reload
502
+ assert_equal 5, new4.pos
503
+ end
504
+
505
+ def test_delete_middle
506
+ assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(conditions).map(&:id)
507
+
508
+ ArrayScopeListMixin.find(2).destroy
509
+
510
+ assert_equal [1, 3, 4], ArrayScopeListMixin.where(conditions).map(&:id)
511
+
512
+ assert_equal 1, ArrayScopeListMixin.find(1).pos
513
+ assert_equal 2, ArrayScopeListMixin.find(3).pos
514
+ assert_equal 3, ArrayScopeListMixin.find(4).pos
515
+
516
+ ArrayScopeListMixin.find(1).destroy
517
+
518
+ assert_equal [3, 4], ArrayScopeListMixin.where(conditions).map(&:id)
519
+
520
+ assert_equal 1, ArrayScopeListMixin.find(3).pos
521
+ assert_equal 2, ArrayScopeListMixin.find(4).pos
522
+ end
523
+
524
+ def test_remove_from_list_should_then_fail_in_list?
525
+ assert_equal true, ArrayScopeListMixin.find(1).in_list?
526
+ ArrayScopeListMixin.find(1).remove_from_list
527
+ assert_equal false, ArrayScopeListMixin.find(1).in_list?
528
+ end
529
+
530
+ def test_remove_from_list_should_set_position_to_nil
531
+ assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(conditions).map(&:id)
532
+
533
+ ArrayScopeListMixin.find(2).remove_from_list
534
+
535
+ assert_equal [2, 1, 3, 4], ArrayScopeListMixin.where(conditions).map(&:id)
536
+
537
+ assert_equal 1, ArrayScopeListMixin.find(1).pos
538
+ assert_equal nil, ArrayScopeListMixin.find(2).pos
539
+ assert_equal 2, ArrayScopeListMixin.find(3).pos
540
+ assert_equal 3, ArrayScopeListMixin.find(4).pos
541
+ end
542
+
543
+ def test_remove_before_destroy_does_not_shift_lower_items_twice
544
+ assert_equal [1, 2, 3, 4], ArrayScopeListMixin.where(conditions).map(&:id)
545
+
546
+ ArrayScopeListMixin.find(2).remove_from_list
547
+ ArrayScopeListMixin.find(2).destroy
548
+
549
+ assert_equal [1, 3, 4], ArrayScopeListMixin.where(conditions).map(&:id)
550
+
551
+ assert_equal 1, ArrayScopeListMixin.find(1).pos
552
+ assert_equal 2, ArrayScopeListMixin.find(3).pos
553
+ assert_equal 3, ArrayScopeListMixin.find(4).pos
554
+ end
555
+
556
+ def test_higher_items
557
+ assert_equal [1, 2], ArrayScopeListMixin.find(3).higher_items.map(&:id)
558
+ assert_equal [], ArrayScopeListMixin.find(1).higher_items.map(&:id)
559
+ end
560
+
561
+ def test_lower_items
562
+ assert_equal [3, 4], ArrayScopeListMixin.find(2).lower_items.map(&:id)
563
+ assert_equal [], ArrayScopeListMixin.find(4).lower_items.map(&:id)
564
+ end
565
+
566
+ end
metadata ADDED
@@ -0,0 +1,147 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sortifiable
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Andrew White
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-02-07 00:00:00 +00:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: activesupport
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ hash: 1
30
+ segments:
31
+ - 3
32
+ - 0
33
+ - 3
34
+ version: 3.0.3
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: activerecord
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ hash: 1
46
+ segments:
47
+ - 3
48
+ - 0
49
+ - 3
50
+ version: 3.0.3
51
+ type: :runtime
52
+ version_requirements: *id002
53
+ - !ruby/object:Gem::Dependency
54
+ name: bundler
55
+ prerelease: false
56
+ requirement: &id003 !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ hash: 3
62
+ segments:
63
+ - 1
64
+ - 0
65
+ - 10
66
+ version: 1.0.10
67
+ type: :development
68
+ version_requirements: *id003
69
+ - !ruby/object:Gem::Dependency
70
+ name: sqlite3
71
+ prerelease: false
72
+ requirement: &id004 !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ hash: 29
78
+ segments:
79
+ - 1
80
+ - 3
81
+ - 3
82
+ version: 1.3.3
83
+ type: :development
84
+ version_requirements: *id004
85
+ description: |
86
+ This gem provides an acts_as_list compatible capability for sorting
87
+ and reordering a number of objects in a list. The class that has this
88
+ specified needs to have a +position+ column defined as an integer on
89
+ the mapped database table.
90
+
91
+ This gem requires ActiveRecord 3.0 as it has been refactored to use
92
+ the scope methods and query interface introduced with Ruby on Rails 3.0
93
+
94
+ email:
95
+ - andyw@pixeltrix.co.uk
96
+ executables: []
97
+
98
+ extensions: []
99
+
100
+ extra_rdoc_files: []
101
+
102
+ files:
103
+ - .gemtest
104
+ - CHANGELOG
105
+ - LICENSE
106
+ - README
107
+ - Rakefile
108
+ - lib/sortifiable.rb
109
+ - lib/sortifiable/version.rb
110
+ - sortifiable.gemspec
111
+ - test/sortifiable_test.rb
112
+ has_rdoc: true
113
+ homepage: http://github.com/pixeltrix/sortifiable/
114
+ licenses: []
115
+
116
+ post_install_message:
117
+ rdoc_options: []
118
+
119
+ require_paths:
120
+ - lib
121
+ required_ruby_version: !ruby/object:Gem::Requirement
122
+ none: false
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ hash: 3
127
+ segments:
128
+ - 0
129
+ version: "0"
130
+ required_rubygems_version: !ruby/object:Gem::Requirement
131
+ none: false
132
+ requirements:
133
+ - - ">="
134
+ - !ruby/object:Gem::Version
135
+ hash: 3
136
+ segments:
137
+ - 0
138
+ version: "0"
139
+ requirements: []
140
+
141
+ rubyforge_project:
142
+ rubygems_version: 1.4.2
143
+ signing_key:
144
+ specification_version: 3
145
+ summary: Sort your models
146
+ test_files:
147
+ - test/sortifiable_test.rb