shuber-sortable 1.0.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.
Files changed (7) hide show
  1. data/CHANGELOG +16 -0
  2. data/README.rdoc +198 -0
  3. data/Rakefile +22 -0
  4. data/init.rb +1 -0
  5. data/lib/sortable.rb +350 -0
  6. data/test/sortable_test.rb +258 -0
  7. metadata +61 -0
data/CHANGELOG ADDED
@@ -0,0 +1,16 @@
1
+ 2009-01-17 - Sean Huber (shuber@huberry.com)
2
+ * Update code documentation
3
+ * Make the sortable_scope_changes instance method public
4
+ * Add more tests
5
+ * Update README
6
+ * Add gemspec
7
+
8
+ 2009-01-16 - Sean Huber (shuber@huberry.com)
9
+ * Update logic and add some tests
10
+ * Add more tests
11
+ * Rename README.markdown to README.rdoc
12
+ * Symbolize arguments for calls to the "send" method
13
+
14
+ 2009-01-15 - Sean Huber (shuber@huberry.com)
15
+ * Initial commit
16
+ * Add logic
data/README.rdoc ADDED
@@ -0,0 +1,198 @@
1
+ = sortable
2
+
3
+ Allows you to sort ActiveRecord items similar to http://github.com/rails/acts_as_list but with support for multiple scopes and lists
4
+
5
+
6
+ == Installation
7
+
8
+ gem install shuber-sortable --source http://gems.github.com
9
+ OR
10
+ script/plugin install git://github.com/shuber/sortable.git
11
+
12
+
13
+ == Examples
14
+
15
+ === Simple
16
+
17
+ Works just like http://github.com/rails/acts_as_list
18
+
19
+ class Todo < ActiveRecord::Base
20
+ # schema
21
+ # id :integer
22
+ # project_id :integer
23
+ # description :string
24
+ # position :integer
25
+ sortable :scope => :project_id
26
+ end
27
+
28
+ @todo = Todo.create(:description => 'do something', :project_id => 1)
29
+ @todo_2 = Todo.create(:description => 'do something else', :project_id => 1)
30
+ @todo_3 = Todo.create(:description => 'some other task', :project_id => 2)
31
+
32
+ @todo.position # 1
33
+ @todo_2.position # 2
34
+ @todo_3.position # 1
35
+
36
+ @todo.move_down!
37
+ @todo_2.reload
38
+
39
+ @todo.position # 2
40
+ @todo_2.position # 1
41
+ @todo_3.position # 1
42
+
43
+
44
+ === Multiple scopes
45
+
46
+ Stories may or may not be in a sprint, but if we scoped just by :sprint_id, all stories with a nil :sprint_id
47
+ would be sorted in one giant list instead of being sorted in each of their respective projects. Specifying an
48
+ array of scopes fixes this problem.
49
+
50
+ class Story < ActiveRecord::Base
51
+ # schema
52
+ # id :integer
53
+ # project_id :integer
54
+ # sprint_id :integer
55
+ # description :string
56
+ # position :integer
57
+ sortable :scope => [:project_id, :sprint_id]
58
+ end
59
+
60
+
61
+ === Multiple lists
62
+
63
+ Your project management software needs to allow both clients and developers to prioritize todo items separately
64
+ so that they can be discussed and reviewed during their next meeting. Multiple lists solves this problem.
65
+
66
+ class Todo < ActiveRecord::Base
67
+ # schema
68
+ # id :integer
69
+ # project_id :integer
70
+ # description :string
71
+ # client_priority :integer
72
+ # developer_priority :integer
73
+ sortable :scope => :project_id, :column => :client_priority, :list_name => :client
74
+ sortable :scope => :project_id, :column => :developer_priority, :list_name => :developer
75
+ end
76
+
77
+ @todo = Todo.create(:description => 'do something', :project_id => 1)
78
+ @todo_2 = Todo.create(:description => 'do something else', :project_id => 1)
79
+
80
+ @todo.client_priority # 1
81
+ @todo.developer_priority # 1
82
+ @todo_2.client_priority # 2
83
+ @todo_2.developer_priority # 2
84
+
85
+ @todo.move_down!(:client)
86
+ @todo_2.reload
87
+
88
+ @todo.client_priority # 2
89
+ @todo.developer_priority # 1
90
+ @todo_2.client_priority # 1
91
+ @todo_2.developer_priority # 2
92
+
93
+
94
+ === Switching scope
95
+
96
+ Any attributes specified as a :scope that are changed on an item cause the item to automatically switch lists when it is saved
97
+
98
+ class Todo < ActiveRecord::Base
99
+ # schema
100
+ # id :integer
101
+ # project_id :integer
102
+ # description :string
103
+ # position :integer
104
+ sortable :scope => :project_id
105
+ end
106
+
107
+ @todo = Todo.create(:description => 'do something', :project_id => 1)
108
+ @todo_2 = Todo.create(:description => 'do something else', :project_id => 1)
109
+
110
+ @todo.position # 1
111
+ @todo_2.position # 2
112
+
113
+ @todo.project_id = 2
114
+ @todo.save
115
+ @todo_2.reload
116
+
117
+ @todo.position # 1
118
+ @todo_2.position # 1
119
+
120
+
121
+ == Instance methods
122
+
123
+ # Adds the current item to the end of the specified list and saves
124
+ #
125
+ # If the current item is already in the list, it will remove it before adding it
126
+ add_to_list!(list_name = nil)
127
+
128
+ # Returns the first item in a list associated with the current item
129
+ first_item(list_name = nil)
130
+
131
+ # Returns a boolean after determining if the current item is the first item in the specified list
132
+ first_item?(list_name = nil)
133
+
134
+ # Returns a boolean after determining if the current item is in the specified list
135
+ in_list?(list_name = nil)
136
+
137
+ # Inserts the current item at a certain position in the specified list and saves
138
+ #
139
+ # If the current item is already in the list, it will remove it before adding it
140
+ #
141
+ # Aliased as insert_at_position!
142
+ insert_at!(position = 1, list_name = nil)
143
+
144
+ # Returns the item with a position at a certain offset to the current item's position in the specified list
145
+ #
146
+ # Example
147
+ #
148
+ # @todo = Todo.create
149
+ # @todo_2 = Todo.create
150
+ # @todo.item_at_offset(1) # returns @todo_2
151
+ #
152
+ # Returns nil if an item at the specified offset could not be found
153
+ item_at_offset(offset, list_name = nil)
154
+
155
+ # Returns the last item in a list associated with the current item
156
+ last_item(list_name = nil)
157
+
158
+ # Returns a boolean after determining if the current item is the last item in the specified list
159
+ last_item?(list_name = nil)
160
+
161
+ # Returns the position of the last item in a specified list
162
+ #
163
+ # Returns 0 if there are no items in the specified list
164
+ last_position(list_name = nil)
165
+
166
+ # Moves the current item down one position in the specified list and saves
167
+ move_down!(list_name = nil)
168
+
169
+ # Moves the current item up one position in the specified list and saves
170
+ move_up!(list_name = nil)
171
+
172
+ # Moves the current item down to the bottom of the specified list and saves
173
+ move_to_bottom!(list_name = nil)
174
+
175
+ # Moves the current item up to the top of the specified list and saves
176
+ move_to_top!(list_name = nil)
177
+
178
+ # Returns the next lower item in the specified list
179
+ next_item(list_name = nil)
180
+
181
+ # Returns the previous higher item in the specified list
182
+ previous_item(list_name = nil)
183
+
184
+ # Removes the current item from the specified list and saves
185
+ #
186
+ # This will set the :position to nil
187
+ remove_from_list!(list_name = nil)
188
+
189
+ # Returns a boolean after determining if this item has changed any attributes specified in the :scope options
190
+ sortable_scope_changed?
191
+
192
+ # Stores an array of attributes specified as a :scope that have been changed
193
+ sortable_scope_changes
194
+
195
+
196
+ == Contact
197
+
198
+ Problems, comments, and suggestions all welcome: shuber@huberry.com
data/Rakefile ADDED
@@ -0,0 +1,22 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run the sortable tests'
6
+ task :default => :test
7
+
8
+ desc 'Test the sortable gem/plugin.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.pattern = 'test/*_test.rb'
12
+ t.verbose = true
13
+ end
14
+
15
+ desc 'Generate documentation for the sortable gem/plugin.'
16
+ Rake::RDocTask.new(:rdoc) do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = 'sortable'
19
+ rdoc.options << '--line-numbers' << '--inline-source'
20
+ rdoc.rdoc_files.include('README.rdoc')
21
+ rdoc.rdoc_files.include('lib/**/*.rb')
22
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'sortable'
data/lib/sortable.rb ADDED
@@ -0,0 +1,350 @@
1
+ module Huberry
2
+ module Sortable
3
+ class InvalidSortableList < StandardError; end
4
+
5
+ # Raises InvalidSortableList if <tt>list_name</tt> is not a valid sortable list
6
+ def assert_sortable_list_exists!(list_name)
7
+ raise ::Huberry::Sortable::InvalidSortableList.new("sortable list '#{list_name}' does not exist") unless sortable_lists.has_key?(list_name.to_s)
8
+ end
9
+
10
+ # Allows you to sort items similar to http://github.com/rails/acts_as_list by with added support for multiple scopes and lists
11
+ #
12
+ # Accepts four options:
13
+ #
14
+ # :column => The name of the column that will be used to store an item's position in the list. Defaults to :position
15
+ # :conditions => Any extra constraints to use if you need to specify a tighter scope than just a foreign key. Defaults to '1 = 1'
16
+ # :list_name => The name of the list (this is used when calling all sortable related instance methods). Defaults to nil
17
+ # :scope => A foreign key or an array of foreign keys to use as list constraints. Defaults to []
18
+ #
19
+ #
20
+ # Simple example (works just like rails/acts_as_list)
21
+ #
22
+ # class Todo < ActiveRecord::Base
23
+ # # schema
24
+ # # id :integer
25
+ # # project_id :integer
26
+ # # description :string
27
+ # # position :integer
28
+ # sortable :scope => :project_id
29
+ # end
30
+ #
31
+ # @todo = Todo.create(:description => 'do something', :project_id => 1)
32
+ # @todo_2 = Todo.create(:description => 'do something else', :project_id => 1)
33
+ # @todo_3 = Todo.create(:description => 'some other task', :project_id => 2)
34
+ #
35
+ # @todo.position # 1
36
+ # @todo_2.position # 2
37
+ # @todo_3.position # 1
38
+ #
39
+ # @todo.move_down!
40
+ # @todo_2.reload
41
+ #
42
+ # @todo.position # 2
43
+ # @todo_2.position # 1
44
+ # @todo_3.position # 1
45
+ #
46
+ #
47
+ # Example with multiple scopes - Stories may or may not be in a sprint, but if we scoped just by :sprint_id, all stories with a nil :sprint_id
48
+ # would be sorted in one giant list instead of being sorted in each of their respective projects. Specifying an
49
+ # array of scopes fixes this problem.
50
+ #
51
+ # class Story < ActiveRecord::Base
52
+ # # schema
53
+ # # id :integer
54
+ # # project_id :integer
55
+ # # sprint_id :integer
56
+ # # description :string
57
+ # # position :integer
58
+ # sortable :scope => [:project_id, :sprint_id]
59
+ # end
60
+ #
61
+ #
62
+ # Example with multiple lists - Your project management software needs to allow both clients and developers to prioritize todo items separately
63
+ # so that they can be discussed and reviewed during their next meeting. Multiple lists solves this problem.
64
+ #
65
+ # class Todo < ActiveRecord::Base
66
+ # # schema
67
+ # # id :integer
68
+ # # project_id :integer
69
+ # # description :string
70
+ # # client_priority :integer
71
+ # # developer_priority :integer
72
+ # sortable :scope => :project_id, :column => :client_priority, :list_name => :client
73
+ # sortable :scope => :project_id, :column => :developer_priority, :list_name => :developer
74
+ # end
75
+ #
76
+ # @todo = Todo.create(:description => 'do something', :project_id => 1)
77
+ # @todo_2 = Todo.create(:description => 'do something else', :project_id => 1)
78
+ #
79
+ # @todo.client_priority # 1
80
+ # @todo.developer_priority # 1
81
+ # @todo_2.client_priority # 2
82
+ # @todo_2.developer_priority # 2
83
+ #
84
+ # @todo.move_down!(:client)
85
+ # @todo_2.reload
86
+ #
87
+ # @todo.client_priority # 2
88
+ # @todo.developer_priority # 1
89
+ # @todo_2.client_priority # 1
90
+ # @todo_2.developer_priority # 2
91
+ #
92
+ #
93
+ # Any attributes specified as a <tt>:scope</tt> that are changed on an item cause the item to automatically switch lists when it is saved
94
+ #
95
+ # Example
96
+ #
97
+ # class Todo < ActiveRecord::Base
98
+ # # schema
99
+ # # id :integer
100
+ # # project_id :integer
101
+ # # description :string
102
+ # # position :integer
103
+ # sortable :scope => :project_id
104
+ # end
105
+ #
106
+ # @todo = Todo.create(:description => 'do something', :project_id => 1)
107
+ # @todo_2 = Todo.create(:description => 'do something else', :project_id => 1)
108
+ #
109
+ # @todo.position # 1
110
+ # @todo_2.position # 2
111
+ #
112
+ # @todo.project_id = 2
113
+ # @todo.save
114
+ # @todo_2.reload
115
+ #
116
+ # @todo.position # 1
117
+ # @todo_2.position # 1
118
+ def sortable(options = {})
119
+ include InstanceMethods unless include?(InstanceMethods)
120
+
121
+ cattr_accessor :sortable_lists unless respond_to?(:sortable_lists)
122
+ self.sortable_lists ||= {}
123
+
124
+ define_attribute_methods
125
+
126
+ options = { :column => :position, :conditions => '1 = 1', :list_name => nil, :scope => [] }.merge(options)
127
+ options[:scope] = [options[:scope]] unless options[:scope].is_a?(Array)
128
+
129
+ options[:scope].each do |scope|
130
+ (options[:conditions].is_a?(Array) ? options[:conditions].first : options[:conditions]) << " AND (#{table_name}.#{scope} = ?) "
131
+
132
+ unless instance_methods.include?("#{scope}_with_sortable=")
133
+ define_method "#{scope}_with_sortable=" do |value|
134
+ sortable_scope_changes << scope unless sortable_scope_changes.include?(scope) || new_record? || value == send(scope) || !self.class.sortable_lists.any? { |list_name, configuration| configuration[:scope].include?(scope) }
135
+ send("#{scope}_without_sortable=".to_sym, value)
136
+ end
137
+ alias_method_chain "#{scope}=".to_sym, :sortable
138
+ end
139
+ end
140
+
141
+ self.sortable_lists[options.delete(:list_name).to_s] = options
142
+ end
143
+
144
+ module InstanceMethods
145
+ def self.included(base)
146
+ base.class_eval do
147
+ before_create :add_to_lists
148
+ before_destroy :remove_from_lists
149
+ before_update :update_lists, :if => :sortable_scope_changed?
150
+ alias_method_chain :reload, :sortable
151
+ end
152
+ end
153
+
154
+ # Adds the current item to the end of the specified list and saves
155
+ #
156
+ # If the current item is already in the list, it will remove it before adding it
157
+ def add_to_list!(list_name = nil)
158
+ remove_from_list!(list_name) if in_list?(list_name)
159
+ add_to_list(list_name)
160
+ save
161
+ end
162
+
163
+ # Returns the first item in a list associated with the current item
164
+ def first_item(list_name = nil)
165
+ options = evaluate_sortable_options(list_name)
166
+ self.class.send("find_by_#{options[:column]}".to_sym, 1, :conditions => options[:conditions])
167
+ end
168
+
169
+ # Returns a boolean after determining if the current item is the first item in the specified list
170
+ def first_item?(list_name = nil)
171
+ self == first_item(list_name)
172
+ end
173
+
174
+ # Returns a boolean after determining if the current item is in the specified list
175
+ def in_list?(list_name = nil)
176
+ !new_record? && !send(evaluate_sortable_options(list_name)[:column]).nil?
177
+ end
178
+
179
+ # Inserts the current item at a certain <tt>position</tt> in the specified list and saves
180
+ #
181
+ # If the current item is already in the list, it will remove it before adding it
182
+ #
183
+ # Also aliased as <tt>insert_at_position!</tt>
184
+ def insert_at!(position = 1, list_name = nil)
185
+ remove_from_list!(list_name)
186
+ if position > last_position(list_name)
187
+ add_to_list!(list_name)
188
+ else
189
+ move_lower_items(:down, position - 1, list_name)
190
+ send("#{evaluate_sortable_options(list_name)[:column]}=".to_sym, position)
191
+ save
192
+ end
193
+ end
194
+ alias_method :insert_at_position!, :insert_at!
195
+
196
+ # Returns the item with a <tt>position</tt> at a certain offset to the current item's <tt>position</tt> in the specified list
197
+ #
198
+ # Example
199
+ #
200
+ # @todo = Todo.create
201
+ # @todo_2 = Todo.create
202
+ # @todo.item_at_offset(1) # returns @todo_2
203
+ #
204
+ # Returns nil if an item at the specified offset could not be found
205
+ def item_at_offset(offset, list_name = nil)
206
+ options = evaluate_sortable_options(list_name)
207
+ in_list?(list_name) ? self.class.send("find_by_#{options[:column]}".to_sym, send(options[:column]) + offset) : nil
208
+ end
209
+
210
+ # Returns the last item in a list associated with the current item
211
+ def last_item(list_name = nil)
212
+ options = evaluate_sortable_options(list_name)
213
+ (options[:conditions].is_a?(Array) ? options[:conditions].first : options[:conditions]) << " AND #{self.class.table_name}.#{options[:column]} IS NOT NULL "
214
+ self.class.find(:last, :conditions => options[:conditions], :order => options[:column].to_s)
215
+ end
216
+
217
+ # Returns a boolean after determining if the current item is the last item in the specified list
218
+ def last_item?(list_name = nil)
219
+ self == last_item(list_name)
220
+ end
221
+
222
+ # Returns the position of the last item in a specified list
223
+ #
224
+ # Returns 0 if there are no items in the specified list
225
+ def last_position(list_name = nil)
226
+ item = last_item(list_name)
227
+ item.nil? ? 0 : item.send(evaluate_sortable_options(list_name)[:column])
228
+ end
229
+
230
+ # Moves the current item down one position in the specified list and saves
231
+ def move_down!(list_name = nil)
232
+ in_list?(list_name) && (last_item?(list_name) || insert_at!(send(evaluate_sortable_options(list_name)[:column]) + 1, list_name))
233
+ end
234
+
235
+ # Moves the current item up one position in the specified list and saves
236
+ def move_up!(list_name = nil)
237
+ in_list?(list_name) && (first_item?(list_name) || insert_at!(send(evaluate_sortable_options(list_name)[:column]) - 1, list_name))
238
+ end
239
+
240
+ # Moves the current item down to the bottom of the specified list and saves
241
+ def move_to_bottom!(list_name = nil)
242
+ in_list?(list_name) && (last_item?(list_name) || add_to_list!(list_name))
243
+ end
244
+
245
+ # Moves the current item up to the top of the specified list and saves
246
+ def move_to_top!(list_name = nil)
247
+ in_list?(list_name) && (first_item?(list_name) || insert_at!(1, list_name))
248
+ end
249
+
250
+ # Returns the next lower item in the specified list
251
+ def next_item(list_name = nil)
252
+ item_at_offset(1, list_name)
253
+ end
254
+
255
+ # Returns the previous higher item in the specified list
256
+ def previous_item(list_name = nil)
257
+ item_at_offset(-1, list_name)
258
+ end
259
+
260
+ # Clears any <tt>sortable_scope_changes</tt> and reloads normally
261
+ def reload_with_sortable
262
+ @sortable_scope_changes = nil
263
+ reload_without_sortable
264
+ end
265
+
266
+ # Removes the current item from the specified list and saves
267
+ #
268
+ # This will set the <tt>position</tt> to nil
269
+ def remove_from_list!(list_name = nil)
270
+ if in_list?(list_name)
271
+ remove_from_list(list_name)
272
+ save
273
+ else
274
+ false
275
+ end
276
+ end
277
+
278
+ # Returns a boolean after determining if this item has changed any attributes specified in the <tt>:scope</tt> options
279
+ def sortable_scope_changed?
280
+ !sortable_scope_changes.empty?
281
+ end
282
+
283
+ # Stores an array of attributes specified as a <tt>:scope</tt> that have been changed
284
+ def sortable_scope_changes
285
+ @sortable_scope_changes ||= []
286
+ end
287
+
288
+ protected
289
+
290
+ # Adds the current item to the specified list
291
+ def add_to_list(list_name = nil)
292
+ send("#{evaluate_sortable_options(list_name)[:column]}=".to_sym, last_position(list_name) + 1)
293
+ end
294
+
295
+ # Adds the current item to all sortable lists
296
+ def add_to_lists
297
+ self.class.sortable_lists.each { |list_name, options| add_to_list(list_name) }
298
+ end
299
+
300
+ # Evaluates <tt>:scope</tt> option and appends those constraints to the <tt>:conditions</tt> option
301
+ #
302
+ # Returns the evaluated options
303
+ def evaluate_sortable_options(list_name = nil)
304
+ self.class.assert_sortable_list_exists!(list_name)
305
+ options = self.class.sortable_lists[list_name.to_s].inject({}) { |hash, pair| hash[pair.first] = pair.last.nil? || pair.last.is_a?(Symbol) ? pair.last : pair.last.dup; hash }
306
+ options[:scope].each do |scope|
307
+ value = send(scope)
308
+ if value.nil?
309
+ (options[:conditions].is_a?(Array) ? options[:conditions].first : options[:conditions]).gsub!(/#{scope} \= \?/, "#{scope} IS NULL")
310
+ else
311
+ options[:conditions] = [options[:conditions]] unless options[:conditions].is_a?(Array)
312
+ options[:conditions] << value
313
+ end
314
+ end
315
+ options
316
+ end
317
+
318
+ # Moves items with a position lower than a certain <tt>position</tt> by an offset of 1 in the specified
319
+ # <tt>direction</tt> (:up or :down) for the specified list
320
+ def move_lower_items(direction, position, list_name = nil)
321
+ options = evaluate_sortable_options(list_name)
322
+ (options[:conditions].is_a?(Array) ? options[:conditions].first : options[:conditions]) << " AND #{self.class.table_name}.#{options[:column]} > '#{position}' AND #{self.class.table_name}.#{options[:column]} IS NOT NULL "
323
+ self.class.update_all "#{options[:column]} = #{options[:column]} #{direction == :up ? '-' : '+'} 1", options[:conditions]
324
+ end
325
+
326
+ # Removes the current item from the specified list
327
+ def remove_from_list(list_name = nil)
328
+ options = evaluate_sortable_options(list_name)
329
+ move_lower_items(:up, send(options[:column]), list_name)
330
+ send("#{options[:column]}=".to_sym, nil)
331
+ end
332
+
333
+ # Removes the current item from all sortable lists
334
+ def remove_from_lists
335
+ self.class.sortable_lists.each { |list_name, options| remove_from_list(list_name) }
336
+ end
337
+
338
+ # Removes the current item from its old lists and adds it to new lists if any attributes specified as a <tt>:scope</tt> have been changed
339
+ def update_lists
340
+ new_values = sortable_scope_changes.inject({}) { |hash, scope| value = send(scope); hash[scope] = value.nil? ? nil : value.dup; hash }
341
+ sortable_scope_changes.each { |scope| send("#{scope}=".to_sym, send("#{scope}_was".to_sym)) }
342
+ remove_from_lists
343
+ new_values.each { |scope, value| send("#{scope}=".to_sym, value) }
344
+ add_to_lists
345
+ end
346
+ end
347
+ end
348
+ end
349
+
350
+ ActiveRecord::Base.extend Huberry::Sortable
@@ -0,0 +1,258 @@
1
+ require 'test/unit'
2
+ require 'rubygems'
3
+ gem 'activerecord'
4
+ require 'active_record'
5
+ require File.dirname(__FILE__) + '/../lib/sortable'
6
+
7
+ ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => ':memory:'
8
+
9
+ def create_todos_table
10
+ silence_stream(STDOUT) do
11
+ ActiveRecord::Schema.define(:version => 1) do
12
+ create_table :todos do |t|
13
+ t.integer :project_id
14
+ t.string :action
15
+ t.integer :client_priority
16
+ t.integer :developer_priority
17
+ t.integer :position
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ # The table needs to exist before defining the class
24
+ create_todos_table
25
+
26
+ class Todo < ActiveRecord::Base
27
+ sortable :scope => :project_id, :conditions => 'todos.action IS NOT NULL'
28
+ sortable :scope => :project_id, :column => :client_priority, :list_name => :client
29
+ sortable :scope => :project_id, :column => :developer_priority, :list_name => :developer
30
+ end
31
+
32
+ class SortableTest < Test::Unit::TestCase
33
+
34
+ def setup
35
+ ActiveRecord::Base.connection.tables.each { |table| ActiveRecord::Base.connection.drop_table(table) }
36
+ create_todos_table
37
+ end
38
+
39
+ def test_should_add_to_lists
40
+ @todo = Todo.create
41
+ assert_equal 1, @todo.client_priority
42
+ assert_equal 1, @todo.developer_priority
43
+ end
44
+
45
+ def test_should_increment_lists
46
+ Todo.create
47
+ @todo = Todo.create
48
+ assert_equal 2, @todo.client_priority
49
+ assert_equal 2, @todo.developer_priority
50
+ end
51
+
52
+ def test_should_scope_lists
53
+ Todo.create
54
+ @todo = Todo.create :project_id => 1
55
+ assert_equal 1, @todo.client_priority
56
+ assert_equal 1, @todo.developer_priority
57
+ end
58
+
59
+ def test_should_remove_from_lists_on_destroy
60
+ Todo.create
61
+ @todo = Todo.create
62
+ @todo_2 = Todo.create
63
+ assert_equal 3, @todo_2.client_priority
64
+ assert_equal 3, @todo_2.developer_priority
65
+ @todo.destroy
66
+ @todo_2.reload
67
+ assert_equal 2, @todo_2.client_priority
68
+ assert_equal 2, @todo_2.developer_priority
69
+ end
70
+
71
+ def test_should_return_first_item
72
+ @todo = Todo.create
73
+ @todo_2 = Todo.create
74
+ assert_equal @todo, @todo_2.first_item(:client)
75
+ assert_equal @todo, @todo_2.first_item(:developer)
76
+ end
77
+
78
+ def test_should_return_boolean_for_first_item?
79
+ @todo = Todo.create
80
+ @todo_2 = Todo.create
81
+ assert @todo.first_item?(:client)
82
+ assert !@todo_2.first_item?(:client)
83
+ end
84
+
85
+ def test_should_return_boolean_for_in_list?
86
+ @todo = Todo.new
87
+ assert !@todo.in_list?(:client)
88
+ assert @todo.save
89
+ assert @todo.in_list?(:client)
90
+ @todo.remove_from_list!(:client)
91
+ assert !@todo.in_list?(:client)
92
+ end
93
+
94
+ def test_should_insert_at!
95
+ @todo = Todo.create
96
+ @todo_2 = Todo.create
97
+ @todo_3 = Todo.create
98
+ @todo.insert_at!(2, :client)
99
+ @todo_2.reload
100
+ @todo_3.reload
101
+ assert_equal 1, @todo_2.client_priority
102
+ assert_equal 2, @todo.client_priority
103
+ assert_equal 3, @todo_3.client_priority
104
+ end
105
+
106
+ def test_item_at_offset_should_return_previous_item
107
+ @todo = Todo.create
108
+ @todo_2 = Todo.create
109
+ assert_equal @todo, @todo_2.item_at_offset(-1, :client)
110
+ end
111
+
112
+ def test_item_at_offset_should_return_next_item
113
+ @todo = Todo.create
114
+ @todo_2 = Todo.create
115
+ assert_equal @todo_2, @todo.item_at_offset(1, :client)
116
+ end
117
+
118
+ def test_item_at_offset_should_return_nil_for_non_existent_offset
119
+ @todo = Todo.create
120
+ @todo_2 = Todo.create
121
+ assert_nil @todo.item_at_offset(-1, :client)
122
+ assert_nil @todo.item_at_offset(2, :client)
123
+ end
124
+
125
+ def test_should_return_last_item
126
+ @todo = Todo.create
127
+ @todo_2 = Todo.create
128
+ assert_equal @todo_2, @todo.last_item(:client)
129
+ assert_equal @todo_2, @todo.last_item(:developer)
130
+ end
131
+
132
+ def test_should_return_boolean_for_last_item?
133
+ @todo = Todo.create
134
+ @todo_2 = Todo.create
135
+ assert @todo_2.last_item?(:client)
136
+ assert !@todo.last_item?(:client)
137
+ end
138
+
139
+ def test_should_return_last_position
140
+ assert_equal 0, Todo.new.last_position(:client)
141
+ @todo = Todo.create
142
+ assert_equal 1, @todo.last_position(:client)
143
+ Todo.create
144
+ assert_equal 2, @todo.last_position(:client)
145
+ end
146
+
147
+ def test_should_move_down
148
+ @todo = Todo.create
149
+ Todo.create
150
+ assert_equal 1, @todo.client_priority
151
+ @todo.move_down!(:client)
152
+ assert_equal 2, @todo.client_priority
153
+ end
154
+
155
+ def test_should_move_up
156
+ Todo.create
157
+ @todo = Todo.create
158
+ assert_equal 2, @todo.client_priority
159
+ @todo.move_up!(:client)
160
+ assert_equal 1, @todo.client_priority
161
+ end
162
+
163
+ def test_should_move_to_bottom
164
+ @todo = Todo.create
165
+ Todo.create
166
+ Todo.create
167
+ assert_equal 1, @todo.client_priority
168
+ @todo.move_to_bottom!(:client)
169
+ assert_equal 3, @todo.client_priority
170
+ end
171
+
172
+ def test_should_move_to_top
173
+ Todo.create
174
+ Todo.create
175
+ @todo = Todo.create
176
+ assert_equal 3, @todo.client_priority
177
+ @todo.move_to_top!(:client)
178
+ assert_equal 1, @todo.client_priority
179
+ end
180
+
181
+ def test_should_return_next_item
182
+ @todo = Todo.create
183
+ @todo_2 = Todo.create
184
+ assert_equal @todo_2, @todo.next_item(:client)
185
+ assert_nil @todo_2.next_item(:client)
186
+ end
187
+
188
+ def test_should_return_previous_item
189
+ @todo = Todo.create
190
+ @todo_2 = Todo.create
191
+ assert_equal @todo, @todo_2.previous_item(:client)
192
+ assert_nil @todo.previous_item(:client)
193
+ end
194
+
195
+ def test_should_clear_sortable_scope_changes_when_reloading
196
+ @todo = Todo.create
197
+ @todo.project_id = 1
198
+ assert @todo.sortable_scope_changed?
199
+ @todo.reload
200
+ assert !@todo.sortable_scope_changed?
201
+ end
202
+
203
+ def test_should_remove_from_list
204
+ @todo = Todo.create
205
+ @todo_2 = Todo.create
206
+ assert_equal 1, @todo.client_priority
207
+ assert_equal 2, @todo_2.client_priority
208
+ @todo.remove_from_list!(:client)
209
+ @todo_2.reload
210
+ assert_nil @todo.client_priority
211
+ assert_equal 1, @todo_2.client_priority
212
+ end
213
+
214
+ def test_should_return_boolean_for_sortable_scope_changed?
215
+ @todo = Todo.new
216
+ assert !@todo.sortable_scope_changed?
217
+ @todo.project_id = 1
218
+ assert !@todo.sortable_scope_changed?
219
+ assert @todo.save
220
+ @todo.reload
221
+ @todo.project_id = 2
222
+ assert @todo.sortable_scope_changed?
223
+ end
224
+
225
+ def test_should_list_attrs_in_sortable_scope_changes
226
+ @todo = Todo.new
227
+ assert_equal [], @todo.sortable_scope_changes
228
+ @todo.project_id = 1
229
+ assert_equal [], @todo.sortable_scope_changes
230
+ assert @todo.save
231
+ @todo.reload
232
+ @todo.project_id = 2
233
+ assert [:project_id], @todo.sortable_scope_changes
234
+ end
235
+
236
+ def test_should_raise_invalid_sortable_list_error_if_list_does_not_exist
237
+ @todo = Todo.create
238
+ assert_raises ::Huberry::Sortable::InvalidSortableList do
239
+ @todo.move_up!(:invalid)
240
+ end
241
+ end
242
+
243
+ def test_should_use_conditions
244
+ @todo = Todo.create
245
+ @todo_2 = Todo.create :action => 'test'
246
+ @todo_3 = Todo.create
247
+ @todo_4 = Todo.create :action => 'test again'
248
+ @todo_5 = Todo.create
249
+ @todo_6 = Todo.create
250
+ assert_equal 1, @todo.position
251
+ assert_equal 1, @todo_2.position
252
+ assert_equal 2, @todo_3.position
253
+ assert_equal 2, @todo_4.position
254
+ assert_equal 3, @todo_5.position
255
+ assert_equal 3, @todo_6.position
256
+ end
257
+
258
+ end
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shuber-sortable
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Sean Huber
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-01-17 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Allows you to sort ActiveRecord items in multiple lists with multiple scopes
17
+ email: shuber@huberry.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - CHANGELOG
26
+ - init.rb
27
+ - lib/sortable.rb
28
+ - MIT-LICENSE
29
+ - Rakefile
30
+ - README.rdoc
31
+ has_rdoc: false
32
+ homepage: http://github.com/shuber/sortable
33
+ post_install_message:
34
+ rdoc_options:
35
+ - --line-numbers
36
+ - --inline-source
37
+ - --main
38
+ - README.rdoc
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: "0"
46
+ version:
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: "0"
52
+ version:
53
+ requirements: []
54
+
55
+ rubyforge_project:
56
+ rubygems_version: 1.2.0
57
+ signing_key:
58
+ specification_version: 2
59
+ summary: Allows you to sort ActiveRecord items in multiple lists with multiple scopes
60
+ test_files:
61
+ - test/sortable_test.rb