Module: Mongoid::Ancestry::ClassMethods

Defined in:
lib/mongoid-ancestry/class_methods.rb

Instance Method Summary collapse

Instance Method Details

#arrange(options = {}) ⇒ Object

Arrangement



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/mongoid-ancestry/class_methods.rb', line 116

def arrange options = {}
  scope =
    if options[:order].nil?
      self.base_class.ordered_by_ancestry
    else
      self.base_class.ordered_by_ancestry_and options.delete(:order)
    end
  # Get all nodes ordered by ancestry and start sorting them into an empty hash
  scope.all(options).inject(ActiveSupport::OrderedHash.new) do |arranged_nodes, node|
    # Find the insertion point for that node by going through its ancestors
    node.ancestor_ids.inject(arranged_nodes) do |insertion_point, ancestor_id|
      insertion_point.each do |parent, children|
        # Change the insertion point to children if node is a descendant of this parent
        insertion_point = children if ancestor_id == parent.id
      end; insertion_point
    end[node] = ActiveSupport::OrderedHash.new; arranged_nodes
  end
end

#build_ancestry_from_parent_ids!(parent_id = nil, ancestry = nil) ⇒ Object

Build ancestry from parent id’s for migration purposes



205
206
207
208
209
210
211
212
213
# File 'lib/mongoid-ancestry/class_methods.rb', line 205

def build_ancestry_from_parent_ids! parent_id = nil, ancestry = nil
  self.base_class.where(:parent_id => parent_id).all.each do |node|
    node.without_ancestry_callbacks do
      node.update_attribute(self.base_class.ancestry_field, ancestry)
    end
    build_ancestry_from_parent_ids! node.id,
      if ancestry.nil? then node.id.to_s else "#{ancestry}/#{node.id}" end
  end
end

#check_ancestry_integrity!(options = {}) ⇒ Object

Integrity checking



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
# File 'lib/mongoid-ancestry/class_methods.rb', line 136

def check_ancestry_integrity! options = {}
  parents = {}
  exceptions = [] if options[:report] == :list
  # For each node ...
  self.base_class.all.each do |node|
    begin
      # ... check validity of ancestry column
      if !node.valid? and !node.errors[node.class.ancestry_field].blank?
        raise IntegrityError.new "Invalid format for ancestry column of node #{node.id}: #{node.read_attribute node.ancestry_field}."
      end
      # ... check that all ancestors exist
      node.ancestor_ids.each do |ancestor_id|
        unless where(:_id => ancestor_id).first
          raise IntegrityError.new "Reference to non-existent node in node #{node.id}: #{ancestor_id}."
        end
      end
      # ... check that all node parents are consistent with values observed earlier
      node.path_ids.zip([nil] + node.path_ids).each do |node_id, parent_id|
        parents[node_id] = parent_id unless parents.has_key? node_id
        unless parents[node_id] == parent_id
          raise IntegrityError.new "Conflicting parent id found in node #{node.id}: #{parent_id || 'nil'} for node #{node_id} while expecting #{parents[node_id] || 'nil'}"
        end
      end
    rescue IntegrityError => integrity_exception
      case options[:report]
      when :list then exceptions << integrity_exception
      when :echo then puts integrity_exception
      else raise integrity_exception
      end
    end
  end
  exceptions if options[:report] == :list
end

#has_ancestry(opts = {}) ⇒ Object



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
# File 'lib/mongoid-ancestry/class_methods.rb', line 7

def has_ancestry(opts = {})
  defaults = {
    :ancestry_field    => :ancestry,
    :cache_depth       => false,
    :depth_cache_field => :ancestry_depth,
    :orphan_strategy   => :destroy,
    :touchable         => false
  }

  valid_opts = [:ancestry_field, :cache_depth, :depth_cache_field, :orphan_strategy, :touchable]
  unless opts.is_a?(Hash) &&  opts.keys.all? {|opt| valid_opts.include?(opt) }
    raise Error.new("Invalid options for has_ancestry. Only hash is allowed.\n Defaults: #{defaults.inspect}")
  end

  opts.symbolize_keys!

  opts.reverse_merge!(defaults)

  # Create ancestry field accessor and set to option or default
  cattr_accessor :ancestry_field
  self.ancestry_field = opts[:ancestry_field]

  self.field ancestry_field.to_sym, :type => String
  self.index({ ancestry_field.to_s => 1 })

  # Create orphan strategy accessor and set to option or default (writer comes from DynamicClassMethods)
  cattr_reader :orphan_strategy
  self.orphan_strategy = opts[:orphan_strategy]

  # Create touch accessor and set to option or default
  cattr_accessor :ancestry_touchable
  self.ancestry_touchable = opts[:touchable]

  # Validate format of ancestry column value
  primary_key_format = opts[:primary_key_format] || /[a-z0-9]+/
  validates_format_of ancestry_field, :with => /\A#{primary_key_format.source}(\/#{primary_key_format.source})*\Z/, :allow_nil => true

  # Validate that the ancestor ids don't include own id
  validate :ancestry_exclude_self

  # Create ancestry column accessor and set to option or default
  if opts[:cache_depth]
    # Create accessor for column name and set to option or default
    self.cattr_accessor :depth_cache_field
    self.depth_cache_field = opts[:depth_cache_field]

    # Cache depth in depth cache column before save
    before_validation :cache_depth

    # Validate depth column
    validates_numericality_of depth_cache_field, :greater_than_or_equal_to => 0, :only_integer => true, :allow_nil => false
  end

  # Create named scopes for depth
  {:before_depth => 'lt', :to_depth => 'lte', :at_depth => nil, :from_depth => 'gte', :after_depth => 'gt'}.each do |scope_name, operator|
    scope scope_name, ->(depth) {
      raise Error.new("Named scope '#{scope_name}' is only available when depth caching is enabled.") unless opts[:cache_depth]
      where( (operator ? depth_cache_field.send(operator.to_sym) : depth_cache_field) => depth)
    }
  end

  scope :roots, -> { where(ancestry_field => nil) }
  scope :ancestors_of, ->(object) { where(to_node(object).ancestor_conditions) }
  scope :children_of, ->(object) { where(to_node(object).child_conditions) }
  scope :descendants_of, ->(object) { any_of(to_node(object).descendant_conditions) }
  scope :subtree_of, ->(object) { any_of(to_node(object).subtree_conditions) }
  scope :siblings_of, ->(object) { where(to_node(object).sibling_conditions) }
  scope :ordered_by_ancestry, -> { asc(:"#{self.base_class.ancestry_field}") }
  scope :ordered_by_ancestry_and, ->(by) { ordered_by_ancestry.order_by([by]) }

  # Update descendants with new ancestry before save
  before_save :update_descendants_with_new_ancestry

  before_save :touch_parent, if: ->(obj) {
    obj.ancestry_touchable && obj.send(:"#{self.class.ancestry_field}_changed?")
  }

  # Apply orphan strategy before destroy
  before_destroy :apply_orphan_strategy
end

#orphan_strategy=(orphan_strategy) ⇒ Object

Orphan strategy writer



106
107
108
109
110
111
112
113
# File 'lib/mongoid-ancestry/class_methods.rb', line 106

def orphan_strategy= orphan_strategy
  # Check value of orphan strategy, only rootify, restrict or destroy is allowed
  if [:rootify, :restrict, :destroy].include? orphan_strategy
    class_variable_set :@@orphan_strategy, orphan_strategy
  else
    raise Error.new("Invalid orphan strategy, valid ones are :rootify, :restrict and :destroy.")
  end
end

#rebuild_depth_cache!Object

Rebuild depth cache if it got corrupted or if depth caching was just turned on

Raises:



216
217
218
219
220
221
# File 'lib/mongoid-ancestry/class_methods.rb', line 216

def rebuild_depth_cache!
  raise Error.new("Cannot rebuild depth cache for model without depth caching.") unless respond_to? :depth_cache_field
  self.base_class.all.each do |node|
    node.update_attribute depth_cache_field, node.depth
  end
end

#restore_ancestry_integrity!Object

Integrity restoration



171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/mongoid-ancestry/class_methods.rb', line 171

def restore_ancestry_integrity!
  parents = {}
  # For each node ...
  self.base_class.all.each do |node|
    # ... set its ancestry to nil if invalid
    if node.errors[node.class.ancestry_field].blank?
      node.without_ancestry_callbacks do
        node.update_attribute node.ancestry_field, nil
      end
    end
    # ... save parent of this node in parents array if it exists
    parents[node.id] = node.parent_id if where(:_id => node.parent_id).first

    # Reset parent id in array to nil if it introduces a cycle
    parent = parents[node.id]
    until parent.nil? || parent == node.id
      parent = parents[parent]
    end
    parents[node.id] = nil if parent == node.id
  end
  # For each node ...
  self.base_class.all.each do |node|
    # ... rebuild ancestry from parents array
    ancestry, parent = nil, parents[node.id]
    until parent.nil?
      ancestry, parent = if ancestry.nil? then parent else "#{parent}/#{ancestry}" end, parents[parent]
    end
    node.without_ancestry_callbacks do
      node.update_attribute node.ancestry_field, ancestry
    end
  end
end

#scope_depth(depth_options, depth) ⇒ Object

Scope on relative depth options



94
95
96
97
98
99
100
101
102
103
# File 'lib/mongoid-ancestry/class_methods.rb', line 94

def scope_depth depth_options, depth
  depth_options.inject(self.base_class) do |scope, option|
    scope_name, relative_depth = option
    if [:before_depth, :to_depth, :at_depth, :from_depth, :after_depth].include? scope_name
      scope.send scope_name, depth + relative_depth
    else
      raise Error.new("Unknown depth option: #{scope_name}.")
    end
  end
end

#to_node(object) ⇒ Object

Fetch tree node if necessary



89
90
91
# File 'lib/mongoid-ancestry/class_methods.rb', line 89

def to_node object
  object.is_a?(self.base_class) ? object : find(object)
end