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 / 07 / 074588d42b3ac157ab70417170443c5531685d7e.svn-base @ 912:5e80956cc792
History | View | Annotate | Download (9.6 KB)
| 1 | 909:cbb26bc654de | 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 | end |
||
| 135 | |||
| 136 | # Removes the item from the list. |
||
| 137 | def remove_from_list |
||
| 138 | if in_list? |
||
| 139 | decrement_positions_on_lower_items |
||
| 140 | update_attribute position_column, nil |
||
| 141 | end |
||
| 142 | end |
||
| 143 | |||
| 144 | # Increase the position of this item without adjusting the rest of the list. |
||
| 145 | def increment_position |
||
| 146 | return unless in_list? |
||
| 147 | update_attribute position_column, self.send(position_column).to_i + 1 |
||
| 148 | end |
||
| 149 | |||
| 150 | # Decrease the position of this item without adjusting the rest of the list. |
||
| 151 | def decrement_position |
||
| 152 | return unless in_list? |
||
| 153 | update_attribute position_column, self.send(position_column).to_i - 1 |
||
| 154 | end |
||
| 155 | |||
| 156 | # Return +true+ if this object is the first in the list. |
||
| 157 | def first? |
||
| 158 | return false unless in_list? |
||
| 159 | self.send(position_column) == 1 |
||
| 160 | end |
||
| 161 | |||
| 162 | # Return +true+ if this object is the last in the list. |
||
| 163 | def last? |
||
| 164 | return false unless in_list? |
||
| 165 | self.send(position_column) == bottom_position_in_list |
||
| 166 | end |
||
| 167 | |||
| 168 | # Return the next higher item in the list. |
||
| 169 | def higher_item |
||
| 170 | return nil unless in_list? |
||
| 171 | acts_as_list_class.find(:first, :conditions => |
||
| 172 | "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
|
||
| 173 | ) |
||
| 174 | end |
||
| 175 | |||
| 176 | # Return the next lower item in the list. |
||
| 177 | def lower_item |
||
| 178 | return nil unless in_list? |
||
| 179 | acts_as_list_class.find(:first, :conditions => |
||
| 180 | "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
|
||
| 181 | ) |
||
| 182 | end |
||
| 183 | |||
| 184 | # Test if this record is in a list |
||
| 185 | def in_list? |
||
| 186 | !send(position_column).nil? |
||
| 187 | end |
||
| 188 | |||
| 189 | private |
||
| 190 | def add_to_list_top |
||
| 191 | increment_positions_on_all_items |
||
| 192 | end |
||
| 193 | |||
| 194 | def add_to_list_bottom |
||
| 195 | self[position_column] = bottom_position_in_list.to_i + 1 |
||
| 196 | end |
||
| 197 | |||
| 198 | # Overwrite this method to define the scope of the list changes |
||
| 199 | def scope_condition() "1" end |
||
| 200 | |||
| 201 | # Returns the bottom position number in the list. |
||
| 202 | # bottom_position_in_list # => 2 |
||
| 203 | def bottom_position_in_list(except = nil) |
||
| 204 | item = bottom_item(except) |
||
| 205 | item ? item.send(position_column) : 0 |
||
| 206 | end |
||
| 207 | |||
| 208 | # Returns the bottom item |
||
| 209 | def bottom_item(except = nil) |
||
| 210 | conditions = scope_condition |
||
| 211 | conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
|
||
| 212 | acts_as_list_class.find(:first, :conditions => conditions, :order => "#{position_column} DESC")
|
||
| 213 | end |
||
| 214 | |||
| 215 | # Forces item to assume the bottom position in the list. |
||
| 216 | def assume_bottom_position |
||
| 217 | update_attribute(position_column, bottom_position_in_list(self).to_i + 1) |
||
| 218 | end |
||
| 219 | |||
| 220 | # Forces item to assume the top position in the list. |
||
| 221 | def assume_top_position |
||
| 222 | update_attribute(position_column, 1) |
||
| 223 | end |
||
| 224 | |||
| 225 | # This has the effect of moving all the higher items up one. |
||
| 226 | def decrement_positions_on_higher_items(position) |
||
| 227 | acts_as_list_class.update_all( |
||
| 228 | "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}"
|
||
| 229 | ) |
||
| 230 | end |
||
| 231 | |||
| 232 | # This has the effect of moving all the lower items up one. |
||
| 233 | def decrement_positions_on_lower_items |
||
| 234 | return unless in_list? |
||
| 235 | acts_as_list_class.update_all( |
||
| 236 | "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}"
|
||
| 237 | ) |
||
| 238 | end |
||
| 239 | |||
| 240 | # This has the effect of moving all the higher items down one. |
||
| 241 | def increment_positions_on_higher_items |
||
| 242 | return unless in_list? |
||
| 243 | acts_as_list_class.update_all( |
||
| 244 | "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}"
|
||
| 245 | ) |
||
| 246 | end |
||
| 247 | |||
| 248 | # This has the effect of moving all the lower items down one. |
||
| 249 | def increment_positions_on_lower_items(position) |
||
| 250 | acts_as_list_class.update_all( |
||
| 251 | "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}"
|
||
| 252 | ) |
||
| 253 | end |
||
| 254 | |||
| 255 | # Increments position (<tt>position_column</tt>) of all items in the list. |
||
| 256 | def increment_positions_on_all_items |
||
| 257 | acts_as_list_class.update_all( |
||
| 258 | "#{position_column} = (#{position_column} + 1)", "#{scope_condition}"
|
||
| 259 | ) |
||
| 260 | end |
||
| 261 | |||
| 262 | def insert_at_position(position) |
||
| 263 | remove_from_list |
||
| 264 | increment_positions_on_lower_items(position) |
||
| 265 | self.update_attribute(position_column, position) |
||
| 266 | end |
||
| 267 | end |
||
| 268 | end |
||
| 269 | end |
||
| 270 | end |