annotate .svn/pristine/07/074588d42b3ac157ab70417170443c5531685d7e.svn-base @ 1519:afce8026aaeb redmine-2.4-integration

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