aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/mixins/list.rb
blob: bec4b3365132d1a01a92a3c8bb8ddf4f7b13a721 (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
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 Mixins
    # 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.before_destroy :remove_from_list
        base.after_create   :add_to_list_bottom
      end
      
      # can be overriden
      
      def position_column
        "position"
      end

      # Moving around on the list

      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