sortifiable 0.1.0 → 0.1.1
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/CHANGELOG +12 -1
- data/lib/sortifiable/version.rb +1 -1
- data/lib/sortifiable.rb +168 -155
- data/test/sortifiable_test.rb +43 -4
- metadata +3 -3
data/CHANGELOG
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
*0.1.1 (February 7th, 2011)
|
2
|
+
|
3
|
+
* Use the class in which act_as_list is called
|
4
|
+
|
5
|
+
* Wrap list updates inside a transaction block
|
6
|
+
|
7
|
+
* Don't include instance methods in all models
|
8
|
+
|
9
|
+
* Don't add acts_as_list callbacks to all models
|
10
|
+
|
11
|
+
|
1
12
|
*0.1.0 (February 6th, 2011)
|
2
13
|
|
3
|
-
* First release
|
14
|
+
* First release
|
data/lib/sortifiable/version.rb
CHANGED
data/lib/sortifiable.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'active_support/concern'
|
2
1
|
require 'active_support/core_ext/array/wrap'
|
3
2
|
require 'active_support/core_ext/class/attribute'
|
4
3
|
require 'active_support/core_ext/hash/reverse_merge'
|
@@ -24,14 +23,12 @@ require 'sortifiable/version'
|
|
24
23
|
# todo_list.first.move_to_bottom
|
25
24
|
# todo_list.last.move_higher
|
26
25
|
module Sortifiable
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
before_create :add_to_list_bottom
|
34
|
-
before_destroy :decrement_position_on_lower_items, :if => :in_list?
|
26
|
+
def self.included(base) #:nodoc:
|
27
|
+
base.extend(ClassMethods)
|
28
|
+
base.class_eval do
|
29
|
+
class_attribute :acts_as_list_options, :instance_writer => false
|
30
|
+
self.acts_as_list_options = {}
|
31
|
+
end
|
35
32
|
end
|
36
33
|
|
37
34
|
module ClassMethods
|
@@ -70,6 +67,12 @@ module Sortifiable
|
|
70
67
|
options[:scope] = "#{options[:scope]}_id".to_sym
|
71
68
|
end
|
72
69
|
|
70
|
+
options[:class] = self
|
71
|
+
|
72
|
+
include InstanceMethods
|
73
|
+
before_create :add_to_list_bottom
|
74
|
+
before_destroy :decrement_position_on_lower_items, :if => :in_list?
|
75
|
+
|
73
76
|
self.acts_as_list_options = options
|
74
77
|
end
|
75
78
|
end
|
@@ -79,189 +82,199 @@ module Sortifiable
|
|
79
82
|
# list, so <tt>chapter.move_lower</tt> would move that chapter lower in the
|
80
83
|
# list of all chapters. Likewise, <tt>chapter.first?</tt> would return +true+
|
81
84
|
# if that chapter is the first in the list of all chapters.
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
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)
|
85
|
+
module InstanceMethods
|
86
|
+
# Add the item to the end of the list
|
87
|
+
def add_to_list
|
88
|
+
list_class.transaction do
|
89
|
+
remove_from_list if in_list?
|
90
|
+
update_attribute(position_column, last_position + 1)
|
141
91
|
end
|
142
|
-
else
|
143
|
-
false
|
144
92
|
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
93
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
alias_method :bottom_item, :last_item
|
94
|
+
# Returns the current position
|
95
|
+
def current_position
|
96
|
+
send(position_column).to_i
|
97
|
+
end
|
163
98
|
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
end
|
169
|
-
alias_method :bottom_position, :last_position
|
99
|
+
# Decrease the position of this item without adjusting the rest of the list.
|
100
|
+
def decrement_position
|
101
|
+
in_list? && update_attribute(position_column, current_position - 1)
|
102
|
+
end
|
170
103
|
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
104
|
+
# Return +true+ if this object is the first in the list.
|
105
|
+
def first?
|
106
|
+
in_list? && current_position == 1
|
107
|
+
end
|
108
|
+
alias_method :top?, :first?
|
176
109
|
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
110
|
+
# Returns the first item in the list
|
111
|
+
def first_item
|
112
|
+
list_scope.first
|
113
|
+
end
|
114
|
+
alias_method :top_item, :first_item
|
181
115
|
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
116
|
+
# Return the next higher item in the list.
|
117
|
+
def higher_item
|
118
|
+
item_at_offset(-1)
|
119
|
+
end
|
120
|
+
alias_method :previous_item, :higher_item
|
187
121
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
alias_method :move_down, :move_lower
|
122
|
+
# Return items lower than this item or an empty array if it is the last item
|
123
|
+
def higher_items
|
124
|
+
list_scope.where(["#{quoted_position_column} < ?", current_position]).all
|
125
|
+
end
|
193
126
|
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
end
|
127
|
+
# Test if this record is in a list
|
128
|
+
def in_list?
|
129
|
+
!new_record? && !send(position_column).nil?
|
130
|
+
end
|
199
131
|
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
end
|
132
|
+
# Increase the position of this item without adjusting the rest of the list.
|
133
|
+
def increment_position
|
134
|
+
in_list? && update_attribute(position_column, current_position + 1)
|
135
|
+
end
|
205
136
|
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
137
|
+
# Insert the item at the given position (defaults to the top position of 1).
|
138
|
+
def insert_at(position = 1)
|
139
|
+
if position > 0
|
140
|
+
list_class.transaction do
|
141
|
+
remove_from_list
|
142
|
+
if position > last_position
|
143
|
+
add_to_list
|
144
|
+
else
|
145
|
+
increment_position_on_lower_items(position - 1)
|
146
|
+
update_attribute(position_column, position)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
else
|
150
|
+
false
|
151
|
+
end
|
213
152
|
end
|
214
|
-
end
|
215
153
|
|
216
|
-
|
217
|
-
def
|
218
|
-
|
154
|
+
# Return the item at the offset specified from the current position
|
155
|
+
def item_at_offset(offset)
|
156
|
+
in_list? ? offset_scope(offset).first : nil
|
219
157
|
end
|
220
158
|
|
221
|
-
|
222
|
-
|
159
|
+
# Return +true+ if this object is the last in the list.
|
160
|
+
def last?
|
161
|
+
in_list? && current_position == last_position
|
223
162
|
end
|
163
|
+
alias_method :bottom?, :last?
|
224
164
|
|
225
|
-
|
226
|
-
|
165
|
+
# Returns the bottom item
|
166
|
+
def last_item
|
167
|
+
list_scope.last
|
227
168
|
end
|
169
|
+
alias_method :bottom_item, :last_item
|
228
170
|
|
229
|
-
|
230
|
-
|
171
|
+
# Returns the bottom position in the list.
|
172
|
+
def last_position
|
173
|
+
item = last_item
|
174
|
+
item ? item.current_position : 0
|
231
175
|
end
|
176
|
+
alias_method :bottom_position, :last_position
|
232
177
|
|
233
|
-
|
234
|
-
|
178
|
+
# Return the next lower item in the list.
|
179
|
+
def lower_item
|
180
|
+
item_at_offset(1)
|
235
181
|
end
|
182
|
+
alias_method :next_item, :lower_item
|
236
183
|
|
237
|
-
|
238
|
-
|
184
|
+
# Return items lower than this item or an empty array if it is the last item
|
185
|
+
def lower_items
|
186
|
+
list_scope.where(["#{quoted_position_column} > ?", current_position]).all
|
239
187
|
end
|
240
188
|
|
241
|
-
|
242
|
-
|
189
|
+
# Swap positions with the next higher item, if one exists.
|
190
|
+
def move_higher
|
191
|
+
in_list? && (first? || insert_at(current_position - 1))
|
243
192
|
end
|
193
|
+
alias_method :move_up, :move_higher
|
244
194
|
|
245
|
-
|
246
|
-
|
195
|
+
# Swap positions with the next lower item, if one exists.
|
196
|
+
def move_lower
|
197
|
+
in_list? && (last? || insert_at(current_position + 1))
|
247
198
|
end
|
199
|
+
alias_method :move_down, :move_lower
|
248
200
|
|
249
|
-
|
250
|
-
|
201
|
+
# Move to the bottom of the list. If the item is already in the list,
|
202
|
+
# the items below it have their position adjusted accordingly.
|
203
|
+
def move_to_bottom
|
204
|
+
in_list? && (last? || add_to_list)
|
251
205
|
end
|
252
206
|
|
253
|
-
|
254
|
-
|
207
|
+
# Move to the top of the list. If the item is already in the list,
|
208
|
+
# the items above it have their position adjusted accordingly.
|
209
|
+
def move_to_top
|
210
|
+
in_list? && (first? || insert_at(1))
|
255
211
|
end
|
256
212
|
|
257
|
-
|
258
|
-
|
259
|
-
|
213
|
+
# Removes the item from the list.
|
214
|
+
def remove_from_list
|
215
|
+
if in_list?
|
216
|
+
list_class.transaction do
|
217
|
+
decrement_position_on_lower_items
|
218
|
+
update_attribute(position_column, nil)
|
219
|
+
end
|
260
220
|
else
|
261
|
-
|
221
|
+
false
|
262
222
|
end
|
263
223
|
end
|
264
224
|
|
225
|
+
private
|
226
|
+
def add_to_list_bottom #:nodoc:
|
227
|
+
send("#{position_column}=".to_sym, last_position + 1)
|
228
|
+
end
|
229
|
+
|
230
|
+
def base_scope #:nodoc:
|
231
|
+
list_class.unscoped.where(scope_condition)
|
232
|
+
end
|
233
|
+
|
234
|
+
def decrement_position_on_lower_items #:nodoc:
|
235
|
+
lower_scope(current_position).update_all(position_update('- 1'))
|
236
|
+
end
|
237
|
+
|
238
|
+
def increment_position_on_lower_items(position) #:nodoc:
|
239
|
+
lower_scope(position).update_all(position_update('+ 1'))
|
240
|
+
end
|
241
|
+
|
242
|
+
def list_class #:nodoc:
|
243
|
+
acts_as_list_options[:class]
|
244
|
+
end
|
245
|
+
|
246
|
+
def list_scope #:nodoc:
|
247
|
+
base_scope.order(position_column).where("#{quoted_position_column} IS NOT NULL")
|
248
|
+
end
|
249
|
+
|
250
|
+
def lower_scope(position) #:nodoc:
|
251
|
+
base_scope.where(["#{quoted_position_column} > ?", position])
|
252
|
+
end
|
253
|
+
|
254
|
+
def offset_scope(offset) #:nodoc:
|
255
|
+
base_scope.where(position_column => current_position + offset)
|
256
|
+
end
|
257
|
+
|
258
|
+
def position_column #:nodoc:
|
259
|
+
acts_as_list_options[:column]
|
260
|
+
end
|
261
|
+
|
262
|
+
def position_update(direction) #:nodoc:
|
263
|
+
"#{quoted_position_column} = (#{quoted_position_column} #{direction})"
|
264
|
+
end
|
265
|
+
|
266
|
+
def quoted_position_column #:nodoc:
|
267
|
+
connection.quote_column_name(position_column)
|
268
|
+
end
|
269
|
+
|
270
|
+
def scope_condition #:nodoc:
|
271
|
+
if acts_as_list_options[:scope].is_a?(String)
|
272
|
+
instance_eval("\"#{acts_as_list_options[:scope]}\"")
|
273
|
+
else
|
274
|
+
Array.wrap(acts_as_list_options[:scope]).inject({}){ |m,k| m[k] = send(k); m }
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
265
278
|
end
|
266
279
|
|
267
280
|
ActiveRecord::Base.send(:include, Sortifiable)
|
data/test/sortifiable_test.rb
CHANGED
@@ -10,6 +10,7 @@ def setup_db
|
|
10
10
|
silence_stream(STDOUT) do
|
11
11
|
ActiveRecord::Schema.define(:version => 1) do
|
12
12
|
create_table :mixins do |t|
|
13
|
+
t.column :type, :string
|
13
14
|
t.column :pos, :integer
|
14
15
|
t.column :parent_id, :integer
|
15
16
|
t.column :parent_type, :string
|
@@ -31,7 +32,7 @@ setup_db
|
|
31
32
|
class Mixin < ActiveRecord::Base
|
32
33
|
end
|
33
34
|
|
34
|
-
class ListMixin <
|
35
|
+
class ListMixin < ActiveRecord::Base
|
35
36
|
acts_as_list :column => "pos", :scope => :parent
|
36
37
|
set_table_name "mixins"
|
37
38
|
default_scope order(:pos)
|
@@ -46,10 +47,9 @@ end
|
|
46
47
|
class ListWithStringScopeMixin < ActiveRecord::Base
|
47
48
|
acts_as_list :column => "pos", :scope => 'parent_id = #{parent_id}'
|
48
49
|
set_table_name "mixins"
|
49
|
-
default_scope order(:pos)
|
50
50
|
end
|
51
51
|
|
52
|
-
class ArrayScopeListMixin <
|
52
|
+
class ArrayScopeListMixin < ActiveRecord::Base
|
53
53
|
acts_as_list :column => "pos", :scope => [:parent_id, :parent_type]
|
54
54
|
set_table_name "mixins"
|
55
55
|
default_scope order(:pos)
|
@@ -57,6 +57,31 @@ end
|
|
57
57
|
|
58
58
|
teardown_db
|
59
59
|
|
60
|
+
class NonListTest < Test::Unit::TestCase
|
61
|
+
|
62
|
+
def setup
|
63
|
+
setup_db
|
64
|
+
end
|
65
|
+
|
66
|
+
def teardown
|
67
|
+
teardown_db
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_callbacks_are_not_added_to_all_models
|
71
|
+
Mixin.create! :pos => 1, :parent_id => 5
|
72
|
+
assert_equal 1, Mixin.first.id
|
73
|
+
|
74
|
+
Mixin.find(1).destroy
|
75
|
+
assert_equal [], Mixin.all
|
76
|
+
end
|
77
|
+
|
78
|
+
def test_instance_methods_are_not_included_in_all_models
|
79
|
+
Mixin.create! :pos => 1, :parent_id => 5
|
80
|
+
assert_equal false, Mixin.first.respond_to?(:in_list?)
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
60
85
|
class ListTest < Test::Unit::TestCase
|
61
86
|
|
62
87
|
def setup
|
@@ -267,13 +292,23 @@ class ListSubTest < Test::Unit::TestCase
|
|
267
292
|
|
268
293
|
def setup
|
269
294
|
setup_db
|
270
|
-
(1..4).each
|
295
|
+
(1..4).each do |i|
|
296
|
+
klass = ((i % 2 == 1) ? ListMixinSub1 : ListMixinSub2)
|
297
|
+
klass.create! :pos => i, :parent_id => 5000
|
298
|
+
end
|
271
299
|
end
|
272
300
|
|
273
301
|
def teardown
|
274
302
|
teardown_db
|
275
303
|
end
|
276
304
|
|
305
|
+
def test_sti_class
|
306
|
+
assert_instance_of ListMixinSub1, ListMixin.find(1)
|
307
|
+
assert_instance_of ListMixinSub2, ListMixin.find(2)
|
308
|
+
assert_instance_of ListMixinSub1, ListMixin.find(3)
|
309
|
+
assert_instance_of ListMixinSub2, ListMixin.find(4)
|
310
|
+
end
|
311
|
+
|
277
312
|
def test_reordering
|
278
313
|
assert_equal [1, 2, 3, 4], ListMixin.where('parent_id = 5000').map(&:id)
|
279
314
|
|
@@ -381,6 +416,10 @@ class ListSubTest < Test::Unit::TestCase
|
|
381
416
|
assert_equal [], ListMixin.find(4).lower_items.map(&:id)
|
382
417
|
end
|
383
418
|
|
419
|
+
def test_list_class
|
420
|
+
assert_equal [1, 2, 3, 4], ListMixin.all.map(&:pos)
|
421
|
+
end
|
422
|
+
|
384
423
|
end
|
385
424
|
|
386
425
|
class ArrayScopeListTest < Test::Unit::TestCase
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sortifiable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 25
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 1
|
9
|
-
-
|
10
|
-
version: 0.1.
|
9
|
+
- 1
|
10
|
+
version: 0.1.1
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Andrew White
|