Module: ActiveRecord::Acts::List::ClassMethods

Defined in:
lib/acts_as_list/active_record/acts/list.rb

Overview

This acts_as extension provides the capabilities for sorting and reordering a number of objects in a list. The class that has this specified needs to have a position column defined as an integer on the mapped database table.

Todo list example:

class TodoList < ActiveRecord::Base
  has_many :todo_items, order: "position"
end

class TodoItem < ActiveRecord::Base
  belongs_to :todo_list
  acts_as_list scope: :todo_list
end

todo_list.first.move_to_bottom
todo_list.last.move_higher

Instance Method Summary collapse

Instance Method Details

#acts_as_list(options = {}) ⇒ Object

Configuration options are:

  • column - specifies the column name to use for keeping the position integer (default: position)

  • scope - restricts what is to be considered a list. Given a symbol, it’ll attach _id (if it hasn’t already been added) and use that as the foreign key restriction. It’s also possible to give it an entire string that is interpolated if you need a tighter scope than just a foreign key. Example: acts_as_list scope: 'todo_list_id = #{todo_list_id} AND completed = 0'

  • top_of_list - defines the integer used for the top of the list. Defaults to 1. Use 0 to make the collection act more like an array in its indexing.

  • add_new_at - specifies whether objects get added to the :top or :bottom of the list. (default: bottom)

    `nil` will result in new items not being added to the list on create
    


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
# File 'lib/acts_as_list/active_record/acts/list.rb', line 37

def acts_as_list(options = {})
  configuration = { column: "position", scope: "1 = 1", top_of_list: 1, add_new_at: :bottom}
  configuration.update(options) if options.is_a?(Hash)

  if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
    configuration[:scope] = :"#{configuration[:scope]}_id"
  end

  caller_class = self

  class_eval do
    define_singleton_method :acts_as_list_top do
      configuration[:top_of_list].to_i
    end

    define_method :acts_as_list_top do
      configuration[:top_of_list].to_i
    end

    define_method :acts_as_list_class do
      caller_class
    end

    define_method :position_column do
      configuration[:column]
    end

    define_method :scope_name do
      configuration[:scope]
    end

    define_method :add_new_at do
      configuration[:add_new_at]
    end

    define_method :"#{configuration[:column]}=" do |position|
      write_attribute(configuration[:column], position)
      @position_changed = true
    end

    if configuration[:scope].is_a?(Symbol)
      define_method :scope_condition do
        { configuration[:scope] => send(:"#{configuration[:scope]}") }
      end

      define_method :scope_changed? do
        changed.include?(scope_name.to_s)
      end
    elsif configuration[:scope].is_a?(Array)
      define_method :scope_condition do
        configuration[:scope].inject({}) do |hash, column|
          hash.merge!({ column.to_sym => read_attribute(column.to_sym) })
        end
      end

      define_method :scope_changed? do
        (scope_condition.keys & changed.map(&:to_sym)).any?
      end
    else
      define_method :scope_condition do
        eval "%{#{configuration[:scope]}}"
      end

      define_method :scope_changed? do
        false
      end
    end

    # only add to attr_accessible
    # if the class has some mass_assignment_protection
    if defined?(accessible_attributes) and !accessible_attributes.blank?
      attr_accessible :"#{configuration[:column]}"
    end

    define_singleton_method :quoted_position_column do
      @_quoted_position_column ||= connection.quote_column_name(configuration[:column])
    end

    define_singleton_method :quoted_position_column_with_table_name do
      @_quoted_position_column_with_table_name ||= "#{caller_class.quoted_table_name}.#{quoted_position_column}"
    end

    scope :in_list, lambda { where("#{quoted_position_column_with_table_name} IS NOT NULL") }

    define_singleton_method :decrement_all do
      update_all_with_touch "#{quoted_position_column} = (#{quoted_position_column_with_table_name} - 1)"
    end

    define_singleton_method :increment_all do
      update_all_with_touch "#{quoted_position_column} = (#{quoted_position_column_with_table_name} + 1)"
    end

    define_singleton_method :update_all_with_touch do |updates|
      record = new
      attrs = record.send(:timestamp_attributes_for_update_in_model)
      now = record.send(:current_time_from_proper_timezone)

      query = attrs.map { |attr| "#{connection.quote_column_name(attr)} = :now" }
      query.push updates
      query = query.join(", ")

      update_all([query, now: now])
    end
  end

  attr_reader :position_changed

  before_validation :check_top_position

  before_destroy :lock!
  after_destroy :decrement_positions_on_lower_items

  before_update :check_scope
  after_update :update_positions

  after_commit :clear_scope_changed

  if configuration[:add_new_at].present?
    before_create "add_to_list_#{configuration[:add_new_at]}".to_sym
  end

  include ::ActiveRecord::Acts::List::InstanceMethods
end