##// END OF EJS Templates
Priorities have the same position and can't be reordered (#11098)....
Jean-Philippe Lang -
r9598:2314e41474b8
parent child
Show More
@@ -1,270 +1,279
1 1 module ActiveRecord
2 2 module Acts #:nodoc:
3 3 module List #:nodoc:
4 4 def self.included(base)
5 5 base.extend(ClassMethods)
6 6 end
7 7
8 8 # This +acts_as+ extension provides the capabilities for sorting and reordering a number of objects in a list.
9 9 # The class that has this specified needs to have a +position+ column defined as an integer on
10 10 # the mapped database table.
11 11 #
12 12 # Todo list example:
13 13 #
14 14 # class TodoList < ActiveRecord::Base
15 15 # has_many :todo_items, :order => "position"
16 16 # end
17 17 #
18 18 # class TodoItem < ActiveRecord::Base
19 19 # belongs_to :todo_list
20 20 # acts_as_list :scope => :todo_list
21 21 # end
22 22 #
23 23 # todo_list.first.move_to_bottom
24 24 # todo_list.last.move_higher
25 25 module ClassMethods
26 26 # Configuration options are:
27 27 #
28 28 # * +column+ - specifies the column name to use for keeping the position integer (default: +position+)
29 29 # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach <tt>_id</tt>
30 30 # (if it hasn't already been added) and use that as the foreign key restriction. It's also possible
31 31 # to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
32 32 # Example: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
33 33 def acts_as_list(options = {})
34 34 configuration = { :column => "position", :scope => "1 = 1" }
35 35 configuration.update(options) if options.is_a?(Hash)
36 36
37 37 configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
38 38
39 39 if configuration[:scope].is_a?(Symbol)
40 40 scope_condition_method = %(
41 41 def scope_condition
42 42 if #{configuration[:scope].to_s}.nil?
43 43 "#{configuration[:scope].to_s} IS NULL"
44 44 else
45 45 "#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}"
46 46 end
47 47 end
48 48 )
49 49 else
50 50 scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end"
51 51 end
52 52
53 53 class_eval <<-EOV
54 54 include ActiveRecord::Acts::List::InstanceMethods
55 55
56 56 def acts_as_list_class
57 57 ::#{self.name}
58 58 end
59 59
60 60 def position_column
61 61 '#{configuration[:column]}'
62 62 end
63 63
64 64 #{scope_condition_method}
65 65
66 66 before_destroy :remove_from_list
67 67 before_create :add_to_list_bottom
68 68 EOV
69 69 end
70 70 end
71 71
72 72 # All the methods available to a record that has had <tt>acts_as_list</tt> specified. Each method works
73 73 # by assuming the object to be the item in the list, so <tt>chapter.move_lower</tt> would move that chapter
74 74 # lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return +true+ if that chapter is
75 75 # the first in the list of all chapters.
76 76 module InstanceMethods
77 77 # Insert the item at the given position (defaults to the top position of 1).
78 78 def insert_at(position = 1)
79 79 insert_at_position(position)
80 80 end
81 81
82 82 # Swap positions with the next lower item, if one exists.
83 83 def move_lower
84 84 return unless lower_item
85 85
86 86 acts_as_list_class.transaction do
87 87 lower_item.decrement_position
88 88 increment_position
89 89 end
90 90 end
91 91
92 92 # Swap positions with the next higher item, if one exists.
93 93 def move_higher
94 94 return unless higher_item
95 95
96 96 acts_as_list_class.transaction do
97 97 higher_item.increment_position
98 98 decrement_position
99 99 end
100 100 end
101 101
102 102 # Move to the bottom of the list. If the item is already in the list, the items below it have their
103 103 # position adjusted accordingly.
104 104 def move_to_bottom
105 105 return unless in_list?
106 106 acts_as_list_class.transaction do
107 107 decrement_positions_on_lower_items
108 108 assume_bottom_position
109 109 end
110 110 end
111 111
112 112 # Move to the top of the list. If the item is already in the list, the items above it have their
113 113 # position adjusted accordingly.
114 114 def move_to_top
115 115 return unless in_list?
116 116 acts_as_list_class.transaction do
117 117 increment_positions_on_higher_items
118 118 assume_top_position
119 119 end
120 120 end
121 121
122 122 # Move to the given position
123 123 def move_to=(pos)
124 124 case pos.to_s
125 125 when 'highest'
126 126 move_to_top
127 127 when 'higher'
128 128 move_higher
129 129 when 'lower'
130 130 move_lower
131 131 when 'lowest'
132 132 move_to_bottom
133 133 end
134 reset_positions_in_list
135 end
136
137 def reset_positions_in_list
138 acts_as_list_class.where(scope_condition).reorder("#{position_column} ASC, id ASC").each_with_index do |item, i|
139 unless item.send(position_column) == (i + 1)
140 acts_as_list_class.update_all({position_column => (i + 1)}, {:id => item.id})
141 end
142 end
134 143 end
135 144
136 145 # Removes the item from the list.
137 146 def remove_from_list
138 147 if in_list?
139 148 decrement_positions_on_lower_items
140 149 update_attribute position_column, nil
141 150 end
142 151 end
143 152
144 153 # Increase the position of this item without adjusting the rest of the list.
145 154 def increment_position
146 155 return unless in_list?
147 156 update_attribute position_column, self.send(position_column).to_i + 1
148 157 end
149 158
150 159 # Decrease the position of this item without adjusting the rest of the list.
151 160 def decrement_position
152 161 return unless in_list?
153 162 update_attribute position_column, self.send(position_column).to_i - 1
154 163 end
155 164
156 165 # Return +true+ if this object is the first in the list.
157 166 def first?
158 167 return false unless in_list?
159 168 self.send(position_column) == 1
160 169 end
161 170
162 171 # Return +true+ if this object is the last in the list.
163 172 def last?
164 173 return false unless in_list?
165 174 self.send(position_column) == bottom_position_in_list
166 175 end
167 176
168 177 # Return the next higher item in the list.
169 178 def higher_item
170 179 return nil unless in_list?
171 180 acts_as_list_class.find(:first, :conditions =>
172 181 "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
173 182 )
174 183 end
175 184
176 185 # Return the next lower item in the list.
177 186 def lower_item
178 187 return nil unless in_list?
179 188 acts_as_list_class.find(:first, :conditions =>
180 189 "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
181 190 )
182 191 end
183 192
184 193 # Test if this record is in a list
185 194 def in_list?
186 195 !send(position_column).nil?
187 196 end
188 197
189 198 private
190 199 def add_to_list_top
191 200 increment_positions_on_all_items
192 201 end
193 202
194 203 def add_to_list_bottom
195 204 self[position_column] = bottom_position_in_list.to_i + 1
196 205 end
197 206
198 207 # Overwrite this method to define the scope of the list changes
199 208 def scope_condition() "1" end
200 209
201 210 # Returns the bottom position number in the list.
202 211 # bottom_position_in_list # => 2
203 212 def bottom_position_in_list(except = nil)
204 213 item = bottom_item(except)
205 214 item ? item.send(position_column) : 0
206 215 end
207 216
208 217 # Returns the bottom item
209 218 def bottom_item(except = nil)
210 219 conditions = scope_condition
211 220 conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
212 acts_as_list_class.find(:first, :conditions => conditions, :order => "#{position_column} DESC")
221 acts_as_list_class.where(conditions).reorder("#{position_column} DESC").first
213 222 end
214 223
215 224 # Forces item to assume the bottom position in the list.
216 225 def assume_bottom_position
217 226 update_attribute(position_column, bottom_position_in_list(self).to_i + 1)
218 227 end
219 228
220 229 # Forces item to assume the top position in the list.
221 230 def assume_top_position
222 231 update_attribute(position_column, 1)
223 232 end
224 233
225 234 # This has the effect of moving all the higher items up one.
226 235 def decrement_positions_on_higher_items(position)
227 236 acts_as_list_class.update_all(
228 237 "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}"
229 238 )
230 239 end
231 240
232 241 # This has the effect of moving all the lower items up one.
233 242 def decrement_positions_on_lower_items
234 243 return unless in_list?
235 244 acts_as_list_class.update_all(
236 245 "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}"
237 246 )
238 247 end
239 248
240 249 # This has the effect of moving all the higher items down one.
241 250 def increment_positions_on_higher_items
242 251 return unless in_list?
243 252 acts_as_list_class.update_all(
244 253 "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}"
245 254 )
246 255 end
247 256
248 257 # This has the effect of moving all the lower items down one.
249 258 def increment_positions_on_lower_items(position)
250 259 acts_as_list_class.update_all(
251 260 "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}"
252 261 )
253 262 end
254 263
255 264 # Increments position (<tt>position_column</tt>) of all items in the list.
256 265 def increment_positions_on_all_items
257 266 acts_as_list_class.update_all(
258 267 "#{position_column} = (#{position_column} + 1)", "#{scope_condition}"
259 268 )
260 269 end
261 270
262 271 def insert_at_position(position)
263 272 remove_from_list
264 273 increment_positions_on_lower_items(position)
265 274 self.update_attribute(position_column, position)
266 275 end
267 276 end
268 277 end
269 278 end
270 279 end
@@ -1,51 +1,76
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class IssuePriorityTest < ActiveSupport::TestCase
21 21 fixtures :enumerations, :issues
22 22
23 23 def test_named_scope
24 24 assert_equal Enumeration.find_by_name('Normal'), Enumeration.named('normal').first
25 25 end
26 26
27 27 def test_default_should_return_the_default_priority
28 28 assert_equal Enumeration.find_by_name('Normal'), IssuePriority.default
29 29 end
30 30
31 31 def test_default_should_return_nil_when_no_default_priority
32 32 IssuePriority.update_all :is_default => false
33 33 assert_nil IssuePriority.default
34 34 end
35 35
36 36 def test_should_be_an_enumeration
37 37 assert IssuePriority.ancestors.include?(Enumeration)
38 38 end
39 39
40 40 def test_objects_count
41 41 # low priority
42 42 assert_equal 6, IssuePriority.find(4).objects_count
43 43 # urgent
44 44 assert_equal 0, IssuePriority.find(7).objects_count
45 45 end
46 46
47 47 def test_option_name
48 48 assert_equal :enumeration_issue_priorities, IssuePriority.new.option_name
49 49 end
50 end
51 50
51 def test_should_be_created_at_last_position
52 IssuePriority.delete_all
53
54 priorities = [1, 2, 3].map {|i| IssuePriority.create!(:name => "P#{i}")}
55 assert_equal [1, 2, 3], priorities.map(&:position)
56 end
57
58 def test_reset_positions_in_list_should_set_sequential_positions
59 IssuePriority.delete_all
60
61 priorities = [1, 2, 3].map {|i| IssuePriority.create!(:name => "P#{i}")}
62 priorities[0].update_attribute :position, 4
63 priorities[1].update_attribute :position, 2
64 priorities[2].update_attribute :position, 7
65 assert_equal [4, 2, 7], priorities.map(&:reload).map(&:position)
66
67 priorities[0].reset_positions_in_list
68 assert_equal [2, 1, 3], priorities.map(&:reload).map(&:position)
69 end
70
71 def test_moving_in_list_should_reset_positions
72 priority = IssuePriority.first
73 priority.expects(:reset_positions_in_list).once
74 priority.move_to = 'higher'
75 end
76 end
General Comments 0
You need to be logged in to leave comments. Login now