sortifiable 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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