aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/acts/list.rb
blob: 5d16f9a0c971bd597f1a121c00d5920cb68e34ce (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
module ActiveRecord
  # Mixins are a way of decorating existing Active Record models with additional behavior. If you for example
  # want to keep a number of Documents in order, you can include Mixins::List, and all of the sudden be able to
  # call <tt>document.move_to_bottom</tt>.
  module Acts
    # This mixin provides the capabilities for sorting and reordering a number of objects in list.
    # The class that has this mixin included needs to have a "position" column defined as an integer on
    # the mapped database table. Further more, you need to implement the <tt>scope_condition</tt> if you want
    # to separate one list from another.
    #
    # Todo list example:
    #
    #   class TodoList < ActiveRecord::Base
    #     has_many :todo_items, :order => "position"
    #   end
    #
    #   class TodoItem < ActiveRecord::Base
    #     include ActiveRecord::Mixins::List
    #     belongs_to :todo_list
    #  
    #     private
    #       def scope_condition
    #         "todo_list_id = #{todo_list_id}"
    #       end
    #   end
    #
    #   todo_list.first.move_to_bottom
    #   todo_list.last.move_higher
    module List
      def self.append_features(base)
        super
        base.extend(ClassMethods)
      end
      
      module ClassMethods
        def acts_as_list(options = {})
          configuration = { :column => "position", :scope => "1" }
          configuration.update(options) if options.is_a?(Hash)

          configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
          
          class_eval <<-EOV
            include InstanceMethods

            def position_column
              '#{configuration[:column]}'
            end

            def scope_condition
              "#{configuration[:scope].is_a?(Symbol) ? configuration[:scope].to_s + " = \#{" + configuration[:scope].to_s + "}" : configuration[:scope]}"
            end
            
            before_destroy :remove_from_list
            after_create   :add_to_list_bottom
          EOV
        end
        
        module InstanceMethods
          def move_lower
            return unless lower_item

            self.class.transaction do
              lower_item.decrement_position
              increment_position
            end
          end

          def move_higher
            return unless higher_item

            self.class.transaction do
              higher_item.increment_position
              decrement_position
            end
          end
    
          def move_to_bottom
            self.class.transaction do
              decrement_positions_on_lower_items
              assume_bottom_position
            end
          end

          def move_to_top
            self.class.transaction do
              increment_positions_on_higher_items
              assume_top_position
            end
          end
    

          # Entering or existing the list
    
          def add_to_list_top
            increment_positions_on_all_items
          end

          def add_to_list_bottom
            assume_bottom_position
          end

          def remove_from_list
            decrement_positions_on_lower_items
          end


          # Changing the position

          def increment_position
            update_attribute position_column, self.send(position_column).to_i + 1
          end
    
          def decrement_position
            update_attribute position_column, self.send(position_column).to_i - 1
          end
    
    
          # Querying the position
    
          def first?
            self.send(position_column) == 1
          end
    
          def last?
            self.send(position_column) == bottom_position_in_list
          end

          private
            # Overwrite this method to define the scope of the list changes
            def scope_condition() "1" end
    
            def higher_item
              self.class.find_first(
                "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
              )
            end
    
            def lower_item
              self.class.find_first(
                "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
              )
            end
    
            def bottom_position_in_list
              item = bottom_item
              item ? item.send(position_column) : 0
            end
    
            def bottom_item
              self.class.find_first(
                "#{scope_condition} ",
                "#{position_column} DESC"
              )
            end

            def assume_bottom_position
              update_attribute position_column, bottom_position_in_list.to_i + 1
            end
      
            def assume_top_position
              update_attribute position_column, 1
            end
      
            def decrement_positions_on_lower_items
              self.class.update_all(
                "#{position_column} = (#{position_column} - 1)",  "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}"
              )
            end
      
            def increment_positions_on_higher_items
              self.class.update_all(
                "#{position_column} = (#{position_column} + 1)",  "#{scope_condition} AND #{position_column} < #{send(position_column)}"
              )
            end

            def increment_positions_on_all_items
              self.class.update_all(
                "#{position_column} = (#{position_column} + 1)",  "#{scope_condition}"
              )
            end
        end     
      end
    end
  end
end