Module: Sortability::ActiveRecord::Base::ClassMethods

Defined in:
lib/sortability/active_record/base.rb

Instance Method Summary collapse

Instance Method Details

#sortable_belongs_to(container, scope_or_options = nil, options_with_scope = {}, &extension) ⇒ Object

Defines a sortable belongs_to relation on the child records



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/sortability/active_record/base.rb', line 140

def sortable_belongs_to(container, scope_or_options = nil,
                        options_with_scope = {}, &extension)
  scope, options = extract_association_params(scope_or_options, options_with_scope)
  on = options[:on] || :sort_position

  class_exec do
    belongs_to container, scope, options.except(:on, :scope), &extension

    reflection = reflect_on_association(container)
    options[:scope] ||= reflection.polymorphic? ? \
                          [reflection.foreign_type,
                           reflection.foreign_key] : \
                          reflection.foreign_key
    options[:inverse_of] ||= reflection.inverse_of.try(:name)

    validates on, presence: true,
                  numericality: { only_integer: true,
                                  greater_than: 0 },
                  uniqueness: { scope: options[:scope] }
  end

  options[:container] = container
  sortable_methods(options)
end

#sortable_class(options = {}) ⇒ Object

Defines a sortable class without a container



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/sortability/active_record/base.rb', line 166

def sortable_class(options = {})
  on = options[:on] || :sort_position
  scope = options[:scope]

  class_exec do
    default_scope { order(on) }

    validates on, presence: true,
                  numericality: { only_integer: true,
                                  greater_than: 0 },
                  uniqueness: (scope.nil? ? true : { scope: scope })
  end

  sortable_methods(options)
end

#sortable_has_many(records, scope_or_options = nil, options_with_scope = {}, &extension) ⇒ Object

Defines a sortable has_many relation on the container



129
130
131
132
133
134
135
136
137
# File 'lib/sortability/active_record/base.rb', line 129

def sortable_has_many(records, scope_or_options = nil, options_with_scope = {}, &extension)
  scope, options = extract_association_params(scope_or_options, options_with_scope)
  if scope.nil?
    on = options[:on] || :sort_position
    scope = -> { order(on) }
  end

  class_exec { has_many records, scope, options.except(:on), &extension }
end

#sortable_methods(options = {}) ⇒ Object

Defines methods that are used to sort records Use via sortable_belongs_to or sortable_class



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
# File 'lib/sortability/active_record/base.rb', line 11

def sortable_methods(options = {})
  on = options[:on] || :sort_position
  container = options[:container]
  inverse_of = options[:inverse_of]
  scope_array = [options[:scope]].flatten.compact
  onname = on.to_s
  setter_mname = "#{onname}="
  peers_mname = "#{onname}_peers"
  before_validation_mname = "#{onname}_before_validation"
  next_by_mname = "next_by_#{onname}"
  prev_by_mname = "previous_by_#{onname}"
  compact_peers_mname = "compact_#{onname}_peers!"

  class_exec do
    before_validation before_validation_mname.to_sym

    # Returns all the sort peers for this record, including self
    define_method peers_mname do |force_scope_load = false|
      unless force_scope_load || container.nil? || inverse_of.nil?
        cont = send(container)
        return cont.send(inverse_of) unless cont.nil?
      end

      relation = self.class.unscoped
      scope_array.each do |s|
        relation = relation.where(s => send(s))
      end
      relation
    end

    # Assigns the "on" field's value if needed
    # Adds 1 to any conflicting fields
    define_method before_validation_mname do
      val = send(on)
      scope_changed = scope_array.any? { |s|
                        !changed_attributes[s].nil? }

      return unless val.nil? || scope_changed || changes[on]

      peers = send(peers_mname, scope_changed)
      if val.nil?
        # Assign the next available number to the record
        max_val = (peers.loaded? ? \
                    peers.to_a.max_by{|r| r.send(on) || 0}.try(on) : \
                    peers.maximum(on)) || 0
        send(setter_mname, max_val + 1)
      elsif peers.to_a.any? { |p| p != self && p.send(on) == val }
        # Make a gap for the record
        at = self.class.arel_table
        peers.where(at[on].gteq(val))
             .reorder(nil)
             .update_all("#{onname} = - (#{onname} + 1)")
        peers.where(at[on].lt(0))
             .reorder(nil)
             .update_all("#{onname} = - #{onname}")

        # Cause peers to load from the DB the next time they are used
        peers.reset
      end
    end

    # Gets the next record among the peers
    define_method next_by_mname do
      val = send(on)
      peers = send(peers_mname)
      peers.loaded? ? \
        peers.to_a.detect { |p| p.send(on) > val } : \
        peers.where(peers.arel_table[on].gt(val)).first
    end

    # Gets the previous record among the peers
    define_method prev_by_mname do
      val = send(on)
      peers = send(peers_mname)
      peers.loaded? ? \
        peers.to_a.reverse.detect { |p| p.send(on) < val } : \
        peers.where(peers.arel_table[on].lt(val)).last
    end

    # Renumbers the peers so that their numbers are sequential,
    # starting at 1
    define_method compact_peers_mname do
      needs_update = false
      peers = send(peers_mname)
      cases = peers.to_a.collect.with_index do |p, i|
        old_val = p.send(on)
        new_val = i + 1
        needs_update = true if old_val != new_val

        # Make sure "on" field in self is up to date
        send(setter_mname, new_val) if p == self

        "WHEN #{old_val} THEN #{- new_val}"
      end.join(' ')

      return peers unless needs_update

      mysql = \
        defined?(ActiveRecord::ConnectionAdapters::MysqlAdapter) && \
        ActiveRecord::Base.connection.instance_of?(
          ActiveRecord::ConnectionAdapters::MysqlAdapter)
      cend = mysql ? 'END CASE' : 'END'

      self.class.transaction do
        peers.reorder(nil)
             .update_all("#{onname} = CASE #{onname} #{cases} #{cend}")
        peers.reorder(nil).update_all("#{onname} = - #{onname}")
      end

      # Mark self as not dirty
      changes_applied
      # Force peers to load from the DB the next time they are used
      peers.reset
    end
  end
end