To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.
root / .svn / pristine / 2f / 2fb6efca6ea9d063a58d2593af2f43422f15665e.svn-base @ 1297:0a574315af3e
History | View | Annotate | Download (9.96 KB)
| 1 | 1296:038ba2d95de8 | Chris | module ActiveRecord |
|---|---|---|---|
| 2 | module Acts #:nodoc: |
||
| 3 | module List #:nodoc: |
||
| 4 | def self.included(base) |
||
| 5 | base.extend(ClassMethods) |
||
| 6 | end |
||
| 7 | |||
| 8 | # This +acts_as+ extension provides the capabilities for sorting and reordering a number of objects in a list. |
||
| 9 | # The class that has this specified needs to have a +position+ column defined as an integer on |
||
| 10 | # the mapped database table. |
||
| 11 | # |
||
| 12 | # Todo list example: |
||
| 13 | # |
||
| 14 | # class TodoList < ActiveRecord::Base |
||
| 15 | # has_many :todo_items, :order => "position" |
||
| 16 | # end |
||
| 17 | # |
||
| 18 | # class TodoItem < ActiveRecord::Base |
||
| 19 | # belongs_to :todo_list |
||
| 20 | # acts_as_list :scope => :todo_list |
||
| 21 | # end |
||
| 22 | # |
||
| 23 | # todo_list.first.move_to_bottom |
||
| 24 | # todo_list.last.move_higher |
||
| 25 | module ClassMethods |
||
| 26 | # Configuration options are: |
||
| 27 | # |
||
| 28 | # * +column+ - specifies the column name to use for keeping the position integer (default: +position+) |
||
| 29 | # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach <tt>_id</tt> |
||
| 30 | # (if it hasn't already been added) and use that as the foreign key restriction. It's also possible |
||
| 31 | # to give it an entire string that is interpolated if you need a tighter scope than just a foreign key. |
||
| 32 | # Example: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
|
||
| 33 | def acts_as_list(options = {})
|
||
| 34 | configuration = { :column => "position", :scope => "1 = 1" }
|
||
| 35 | configuration.update(options) if options.is_a?(Hash) |
||
| 36 | |||
| 37 | configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
|
||
| 38 | |||
| 39 | if configuration[:scope].is_a?(Symbol) |
||
| 40 | scope_condition_method = %( |
||
| 41 | def scope_condition |
||
| 42 | if #{configuration[:scope].to_s}.nil?
|
||
| 43 | "#{configuration[:scope].to_s} IS NULL"
|
||
| 44 | else |
||
| 45 | "#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}"
|
||
| 46 | end |
||
| 47 | end |
||
| 48 | ) |
||
| 49 | else |
||
| 50 | scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end"
|
||
| 51 | end |
||
| 52 | |||
| 53 | class_eval <<-EOV |
||
| 54 | include ActiveRecord::Acts::List::InstanceMethods |
||
| 55 | |||
| 56 | def acts_as_list_class |
||
| 57 | ::#{self.name}
|
||
| 58 | end |
||
| 59 | |||
| 60 | def position_column |
||
| 61 | '#{configuration[:column]}'
|
||
| 62 | end |
||
| 63 | |||
| 64 | #{scope_condition_method}
|
||
| 65 | |||
| 66 | before_destroy :remove_from_list |
||
| 67 | before_create :add_to_list_bottom |
||
| 68 | EOV |
||
| 69 | end |
||
| 70 | end |
||
| 71 | |||
| 72 | # All the methods available to a record that has had <tt>acts_as_list</tt> specified. Each method works |
||
| 73 | # by assuming the object to be the item in the list, so <tt>chapter.move_lower</tt> would move that chapter |
||
| 74 | # lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return +true+ if that chapter is |
||
| 75 | # the first in the list of all chapters. |
||
| 76 | module InstanceMethods |
||
| 77 | # Insert the item at the given position (defaults to the top position of 1). |
||
| 78 | def insert_at(position = 1) |
||
| 79 | insert_at_position(position) |
||
| 80 | end |
||
| 81 | |||
| 82 | # Swap positions with the next lower item, if one exists. |
||
| 83 | def move_lower |
||
| 84 | return unless lower_item |
||
| 85 | |||
| 86 | acts_as_list_class.transaction do |
||
| 87 | lower_item.decrement_position |
||
| 88 | increment_position |
||
| 89 | end |
||
| 90 | end |
||
| 91 | |||
| 92 | # Swap positions with the next higher item, if one exists. |
||
| 93 | def move_higher |
||
| 94 | return unless higher_item |
||
| 95 | |||
| 96 | acts_as_list_class.transaction do |
||
| 97 | higher_item.increment_position |
||
| 98 | decrement_position |
||
| 99 | end |
||
| 100 | end |
||
| 101 | |||
| 102 | # Move to the bottom of the list. If the item is already in the list, the items below it have their |
||
| 103 | # position adjusted accordingly. |
||
| 104 | def move_to_bottom |
||
| 105 | return unless in_list? |
||
| 106 | acts_as_list_class.transaction do |
||
| 107 | decrement_positions_on_lower_items |
||
| 108 | assume_bottom_position |
||
| 109 | end |
||
| 110 | end |
||
| 111 | |||
| 112 | # Move to the top of the list. If the item is already in the list, the items above it have their |
||
| 113 | # position adjusted accordingly. |
||
| 114 | def move_to_top |
||
| 115 | return unless in_list? |
||
| 116 | acts_as_list_class.transaction do |
||
| 117 | increment_positions_on_higher_items |
||
| 118 | assume_top_position |
||
| 119 | end |
||
| 120 | end |
||
| 121 | |||
| 122 | # Move to the given position |
||
| 123 | def move_to=(pos) |
||
| 124 | case pos.to_s |
||
| 125 | when 'highest' |
||
| 126 | move_to_top |
||
| 127 | when 'higher' |
||
| 128 | move_higher |
||
| 129 | when 'lower' |
||
| 130 | move_lower |
||
| 131 | when 'lowest' |
||
| 132 | move_to_bottom |
||
| 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 |
||
| 143 | end |
||
| 144 | |||
| 145 | # Removes the item from the list. |
||
| 146 | def remove_from_list |
||
| 147 | if in_list? |
||
| 148 | decrement_positions_on_lower_items |
||
| 149 | update_attribute position_column, nil |
||
| 150 | end |
||
| 151 | end |
||
| 152 | |||
| 153 | # Increase the position of this item without adjusting the rest of the list. |
||
| 154 | def increment_position |
||
| 155 | return unless in_list? |
||
| 156 | update_attribute position_column, self.send(position_column).to_i + 1 |
||
| 157 | end |
||
| 158 | |||
| 159 | # Decrease the position of this item without adjusting the rest of the list. |
||
| 160 | def decrement_position |
||
| 161 | return unless in_list? |
||
| 162 | update_attribute position_column, self.send(position_column).to_i - 1 |
||
| 163 | end |
||
| 164 | |||
| 165 | # Return +true+ if this object is the first in the list. |
||
| 166 | def first? |
||
| 167 | return false unless in_list? |
||
| 168 | self.send(position_column) == 1 |
||
| 169 | end |
||
| 170 | |||
| 171 | # Return +true+ if this object is the last in the list. |
||
| 172 | def last? |
||
| 173 | return false unless in_list? |
||
| 174 | self.send(position_column) == bottom_position_in_list |
||
| 175 | end |
||
| 176 | |||
| 177 | # Return the next higher item in the list. |
||
| 178 | def higher_item |
||
| 179 | return nil unless in_list? |
||
| 180 | acts_as_list_class.find(:first, :conditions => |
||
| 181 | "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
|
||
| 182 | ) |
||
| 183 | end |
||
| 184 | |||
| 185 | # Return the next lower item in the list. |
||
| 186 | def lower_item |
||
| 187 | return nil unless in_list? |
||
| 188 | acts_as_list_class.find(:first, :conditions => |
||
| 189 | "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
|
||
| 190 | ) |
||
| 191 | end |
||
| 192 | |||
| 193 | # Test if this record is in a list |
||
| 194 | def in_list? |
||
| 195 | !send(position_column).nil? |
||
| 196 | end |
||
| 197 | |||
| 198 | private |
||
| 199 | def add_to_list_top |
||
| 200 | increment_positions_on_all_items |
||
| 201 | end |
||
| 202 | |||
| 203 | def add_to_list_bottom |
||
| 204 | self[position_column] = bottom_position_in_list.to_i + 1 |
||
| 205 | end |
||
| 206 | |||
| 207 | # Overwrite this method to define the scope of the list changes |
||
| 208 | def scope_condition() "1" end |
||
| 209 | |||
| 210 | # Returns the bottom position number in the list. |
||
| 211 | # bottom_position_in_list # => 2 |
||
| 212 | def bottom_position_in_list(except = nil) |
||
| 213 | item = bottom_item(except) |
||
| 214 | item ? item.send(position_column) : 0 |
||
| 215 | end |
||
| 216 | |||
| 217 | # Returns the bottom item |
||
| 218 | def bottom_item(except = nil) |
||
| 219 | conditions = scope_condition |
||
| 220 | conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
|
||
| 221 | acts_as_list_class.where(conditions).reorder("#{position_column} DESC").first
|
||
| 222 | end |
||
| 223 | |||
| 224 | # Forces item to assume the bottom position in the list. |
||
| 225 | def assume_bottom_position |
||
| 226 | update_attribute(position_column, bottom_position_in_list(self).to_i + 1) |
||
| 227 | end |
||
| 228 | |||
| 229 | # Forces item to assume the top position in the list. |
||
| 230 | def assume_top_position |
||
| 231 | update_attribute(position_column, 1) |
||
| 232 | end |
||
| 233 | |||
| 234 | # This has the effect of moving all the higher items up one. |
||
| 235 | def decrement_positions_on_higher_items(position) |
||
| 236 | acts_as_list_class.update_all( |
||
| 237 | "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}"
|
||
| 238 | ) |
||
| 239 | end |
||
| 240 | |||
| 241 | # This has the effect of moving all the lower items up one. |
||
| 242 | def decrement_positions_on_lower_items |
||
| 243 | return unless in_list? |
||
| 244 | acts_as_list_class.update_all( |
||
| 245 | "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}"
|
||
| 246 | ) |
||
| 247 | end |
||
| 248 | |||
| 249 | # This has the effect of moving all the higher items down one. |
||
| 250 | def increment_positions_on_higher_items |
||
| 251 | return unless in_list? |
||
| 252 | acts_as_list_class.update_all( |
||
| 253 | "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}"
|
||
| 254 | ) |
||
| 255 | end |
||
| 256 | |||
| 257 | # This has the effect of moving all the lower items down one. |
||
| 258 | def increment_positions_on_lower_items(position) |
||
| 259 | acts_as_list_class.update_all( |
||
| 260 | "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}"
|
||
| 261 | ) |
||
| 262 | end |
||
| 263 | |||
| 264 | # Increments position (<tt>position_column</tt>) of all items in the list. |
||
| 265 | def increment_positions_on_all_items |
||
| 266 | acts_as_list_class.update_all( |
||
| 267 | "#{position_column} = (#{position_column} + 1)", "#{scope_condition}"
|
||
| 268 | ) |
||
| 269 | end |
||
| 270 | |||
| 271 | def insert_at_position(position) |
||
| 272 | remove_from_list |
||
| 273 | increment_positions_on_lower_items(position) |
||
| 274 | self.update_attribute(position_column, position) |
||
| 275 | end |
||
| 276 | end |
||
| 277 | end |
||
| 278 | end |
||
| 279 | end |