Module: Mongoid::Ancestry::ClassMethods
- Defined in:
- lib/mongoid-ancestry/class_methods.rb
Instance Method Summary collapse
-
#arrange(options = {}) ⇒ Object
Arrangement.
-
#build_ancestry_from_parent_ids!(parent_id = nil, ancestry = nil) ⇒ Object
Build ancestry from parent id’s for migration purposes.
-
#check_ancestry_integrity!(options = {}) ⇒ Object
Integrity checking.
- #has_ancestry(opts = {}) ⇒ Object
-
#orphan_strategy=(orphan_strategy) ⇒ Object
Orphan strategy writer.
-
#rebuild_depth_cache! ⇒ Object
Rebuild depth cache if it got corrupted or if depth caching was just turned on.
-
#restore_ancestry_integrity! ⇒ Object
Integrity restoration.
-
#scope_depth(depth_options, depth) ⇒ Object
Scope on relative depth options.
-
#to_node(object) ⇒ Object
Fetch tree node if necessary.
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 = {} scope = if [:order].nil? self.base_class.ordered_by_ancestry else self.base_class.ordered_by_ancestry_and .delete(:order) end # Get all nodes ordered by ancestry and start sorting them into an empty hash scope.all().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! = {} parents = {} exceptions = [] if [: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 [:report] when :list then exceptions << integrity_exception when :echo then puts integrity_exception else raise integrity_exception end end end exceptions if [: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
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 .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 |