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.
- data/CHANGELOG +16 -0
- data/README.rdoc +198 -0
- data/Rakefile +22 -0
- data/init.rb +1 -0
- data/lib/sortable.rb +350 -0
- data/test/sortable_test.rb +258 -0
- 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
|