sortifiable 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -1,3 +1,3 @@
1
1
  module Sortifiable
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
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
- 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?
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
- # 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)
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
- # Returns the bottom item
159
- def last_item
160
- list_scope.last
161
- end
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
- # 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
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
- # 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
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
- # 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
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
- # 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
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
- # 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
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
- # 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
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
- # 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
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
- # 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
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
- private
217
- def add_to_list_bottom #:nodoc:
218
- send("#{position_column}=".to_sym, last_position + 1)
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
- def base_scope #:nodoc:
222
- self.class.unscoped.where(scope_condition)
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
- def decrement_position_on_lower_items #:nodoc:
226
- lower_scope(current_position).update_all(position_update('- 1'))
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
- def increment_position_on_lower_items(position) #:nodoc:
230
- lower_scope(position).update_all(position_update('+ 1'))
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
- def list_scope #:nodoc:
234
- base_scope.order(position_column).where("#{quoted_position_column} IS NOT NULL")
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
- def lower_scope(position) #:nodoc:
238
- base_scope.where(["#{quoted_position_column} > ?", position])
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
- def offset_scope(offset) #:nodoc:
242
- base_scope.where(position_column => current_position + offset)
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
- def position_column #:nodoc:
246
- acts_as_list_options[:column]
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
- def position_update(direction) #:nodoc:
250
- "#{quoted_position_column} = (#{quoted_position_column} #{direction})"
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
- def quoted_position_column #:nodoc:
254
- connection.quote_column_name(position_column)
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
- def scope_condition #:nodoc:
258
- if acts_as_list_options[:scope].is_a?(String)
259
- instance_eval("\"#{acts_as_list_options[:scope]}\"")
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
- Array.wrap(acts_as_list_options[:scope]).inject({}){ |m,k| m[k] = send(k); m }
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)
@@ -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 < Mixin
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 < Mixin
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 { |i| ((i % 2 == 1) ? ListMixinSub1 : ListMixinSub2).create! :pos => i, :parent_id => 5000 }
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: 27
4
+ hash: 25
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 1
9
- - 0
10
- version: 0.1.0
9
+ - 1
10
+ version: 0.1.1
11
11
  platform: ruby
12
12
  authors:
13
13
  - Andrew White