Module: Hierarchable::InstanceMethods
- Defined in:
- lib/hierarchable/hierarchable.rb
Overview
Instance methods to include
Instance Method Summary collapse
-
#hierarchy_ancestor_models(include_self: false) ⇒ Object
Get all of the ancestors models.
-
#hierarchy_ancestors(include_self: false, models: :all) ⇒ Object
Get ancestors of the same type for an object.
-
#hierarchy_children(include_self: false, models: :all, compact: false) ⇒ Object
Get the children of an object.
-
#hierarchy_children_models(include_self: false) ⇒ Object
Get all of the models of the children that this object could have.
-
#hierarchy_descendant_associations ⇒ Object
Return all of the ‘has_many` association names this class class has as a list of symbols.
-
#hierarchy_descendant_models(include_self: false) ⇒ Object
Get all of the descendant models for objects that are descendants of the current one.
-
#hierarchy_descendants(include_self: false, models: :all, compact: false) ⇒ Object
Get descendants for an object.
-
#hierarchy_full_path ⇒ Object
Return the full hierarchy path from the root to this object.
-
#hierarchy_full_path_reified ⇒ Object
Return the full hierarchy path from the root to this object as objects.
- #hierarchy_parent(raw: false) ⇒ Object
-
#hierarchy_parent_source ⇒ Object
Return the attribute name that links this object to its parent.
-
#hierarchy_path_for(objects) ⇒ Object
Return hierarchy path for given list of objects.
- #hierarchy_root? ⇒ Boolean
-
#hierarchy_sibling_models(include_self: false) ⇒ Object
Get all of the sibling models.
-
#hierarchy_siblings(include_self: false, models: :all, compact: false) ⇒ Object
Get siblings of an object.
-
#to_hierarchy_ancestors_path_format ⇒ Object
Return the string representation of the current object in the format when used as part of a hierarchy.
- #to_hierarchy_format(object) ⇒ Object
Instance Method Details
#hierarchy_ancestor_models(include_self: false) ⇒ Object
Get all of the ancestors models
The ‘include_self` parameter can be set to decide where to start the the ancestry search. If set to `false` (default), then it will return all models found starting with the parent of this object. If set to `true`, then it will start with the currect object.
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 |
# File 'lib/hierarchable/hierarchable.rb', line 172 def hierarchy_ancestor_models(include_self: false) return [] unless respond_to?(:hierarchy_ancestors_path) return include_self ? [self.class] : [] if hierarchy_ancestors_path.blank? models = hierarchy_ancestors_path.split( hierarchable_config[:path_separator] ).map do |ancestor| ancestor_class, = ancestor.split(hierarchable_config[:record_separator]) ancestor_class.safe_constantize end.uniq models << self.class if include_self models.uniq end |
#hierarchy_ancestors(include_self: false, models: :all) ⇒ Object
Get ancestors of the same type for an object.
Using the ‘hierarchy_ancestors_path`, this will iteratively get all ancestor objects and return them as a list.
If the ‘models` parameter is `:all` (default), then the result will contain objects of different types. E.g. if we have a Project, Task, and a Comment, the siblings of a Task may include both Tasks and Comments. If you only need this one particular model’s data, then set ‘models` to `:this`. If you want to specify a specific list of models then that can be passed as a list (e.g. [MyModel1, MyModel2]) rubocop:disable Metrics/CyclomaticComplexity rubocop:disable Metrics/PerceivedComplexity
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 |
# File 'lib/hierarchable/hierarchable.rb', line 201 def hierarchy_ancestors(include_self: false, models: :all) return [] unless respond_to?(:hierarchy_ancestors_path) return include_self ? [self] : [] if hierarchy_ancestors_path.blank? ancestors = hierarchy_ancestors_path.split( hierarchable_config[:path_separator] ).map do |ancestor| ancestor_class, ancestor_id = ancestor.split( hierarchable_config[:record_separator] ) next if ancestor_class != self.class.name && models != :all next if models.is_a?(Array) && models.exclude?(ancestor_class) ancestor_class.safe_constantize.find(ancestor_id) end ancestors.compact ancestors << self if include_self ancestors end |
#hierarchy_children(include_self: false, models: :all, compact: false) ⇒ Object
Get the children of an object.
For a given object type, return all siblings as a hash such that the key is the model and the value is the list of siblings of that model.
If the ‘models` parameter is `:all` (default), then the result will contain objects of different types. E.g. if we have a Project, Task, and a Comment, the siblings of a Task may include both Tasks and Comments. If you only need this one particular model’s data, then set ‘models` to `:this`. If you want to specify a specific list of models then that can be passed as a list (e.g. [MyModel1, MyModel2])
The ‘include_self` parameter can be set to decide where to start the the children search. If set to `false` (default), then it will return all models found starting with the for all children. If set to `true`, then it will include the current object’s class. Note, this parameter is added here for consistency, but in the case of children, it is unlikely that ‘include_self` would be set to `true` rubocop:disable Metrics/AbcSize rubocop:disable Metrics/CyclomaticComplexity rubocop:disable Metrics/PerceivedComplexity
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 |
# File 'lib/hierarchable/hierarchable.rb', line 271 def hierarchy_children(include_self: false, models: :all, compact: false) return {} unless respond_to?(:hierarchy_parent_id) # Convert all of the models to actual classes if they are passed as # stings. if models.is_a?(Array) models = models.map do |model| model.is_a?(String) ? model.safe_constantize : model end end result = {} hierarchy_descendant_associations.each do |association| model = class_for_association(association) next unless models == :all || (models.is_a?(Array) && models.include?(model)) || (models == :this && instance_of?(model)) result[model.to_s] = public_send(association) end # If we want to include self, we need to do some extra work if include_self if result.key?(self.class.to_s) result[self.class.to_s] = result[self.class.to_s].or(self.class.where(id:)) elsif models == :all || models == :this || (models.is_a?(Array) && models.include?(self.class)) result[self.class.to_s] = [self] end end # Compact the results if necessary# Compact the results if necessary _, result = result.first if result.size == 1 && compact result end |
#hierarchy_children_models(include_self: false) ⇒ Object
Get all of the models of the children that this object could have
This is based on the models identified in the ‘hierarchy_descendant_associations` association
The ‘include_self` parameter can be set to decide where to start the the children search. If set to `false` (default), then it will return all models found starting with the for all children. If set to `true`, then it will include the current object’s class. Note, this parameter is added here for consistency, but in the case of children models, it is unlikely that ‘include_self` would be set to `true`
236 237 238 239 240 241 242 243 244 245 246 247 248 |
# File 'lib/hierarchable/hierarchable.rb', line 236 def hierarchy_children_models(include_self: false) return [] unless respond_to?(:hierarchy_descendant_associations) if hierarchy_descendant_associations.blank? return include_self ? [self.class] : [] end models = hierarchy_descendant_associations.map do |association| class_for_association(association) end models << self.class if include_self models.uniq end |
#hierarchy_descendant_associations ⇒ Object
Return all of the ‘has_many` association names this class class has as a list of symbols.
In order to be safe and not return potential duplicate associations, the only associations that are automatically detected are the ones that are the pluralized form of the model name. For example, if a model as the association ‘has_many :tasks`, there will need to be a Task model for this association to be kept.
If there are some associations that need to be manually added, one simply needs to specify them when setting up the model.
The most common case is if we want to specify additional associations. This will take all of the associations that can be auto-detected and also add in the one provided.
class A
include Hierarchable
hierarched parent_source: :parent,
additional_descendant_associations: [:some_association]
end
There may also be a case when we want exact control over what associations that should be used. In that case, we can specify it like this:
class A
include Hierarchable
hierarched parent_source: :parent,
descendant_associations: [:some_association]
end
531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 |
# File 'lib/hierarchable/hierarchable.rb', line 531 def hierarchy_descendant_associations if hierarchable_config[:descendant_associations].present? return hierarchable_config[:descendant_associations] end associations = self.class .reflect_on_all_associations(:has_many) .reject do |a| a.name.to_s.singularize.camelcase.safe_constantize.nil? end .reject(&:through_reflection?) .map(&:name) associations += hierarchable_config[:additional_descendant_associations] associations.uniq end |
#hierarchy_descendant_models(include_self: false) ⇒ Object
Get all of the descendant models for objects that are descendants of the current one.
This will make use of the ‘hierarchy_descendant_associations` to find all models.
Unlike ‘hierarchy_children_models` that only looks at the immediate children of an object, this method will look at all descenants of the current object and find the models. In other words, this will follow all relationships of all children, and those children’s children to get all models that could potentially be descendants of the current model.
The ‘include_self` parameter can be set to decide where to start the the descentant search. If set to `false` (default), then it will return all models found starting with the children of this object. If set to `true`, then it will start with the currect object. rubocop:disable Metrics/CyclomaticComplexity rubocop:disable Metrics/PerceivedComplexity
387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 |
# File 'lib/hierarchable/hierarchable.rb', line 387 def hierarchy_descendant_models(include_self: false) return [] unless respond_to?(:hierarchy_descendant_associations) if hierarchy_descendant_associations.blank? return include_self ? [self.class] : [] end models = [] models_to_analyze = [self.class] until models_to_analyze.empty? klass = models_to_analyze.pop next unless klass next if models.include?(klass) obj = klass.new next unless obj.respond_to?(:hierarchy_descendant_associations) models_to_analyze += obj.hierarchy_children_models(include_self: false) next if klass == self.class && !include_self models << klass end models.uniq end |
#hierarchy_descendants(include_self: false, models: :all, compact: false) ⇒ Object
Get descendants for an object.
The ‘include_self` parameter can be set to decide where to start the the descentant search. If set to `false` (default), then it will return all models found starting with the children of this object. If set to `true`, then it will start with the currect object.
If the ‘models` parameter is `:all` (default), then the result will contain objects of different types. E.g. if we have a Project, Task, and a Comment, the siblings of a Task may include both Tasks and Comments. If you only need this one particular model’s data, then set ‘models` to `:this`. If you want to specify a specific list of models then that can be passed as a list (e.g. [MyModel1, MyModel2]) rubocop:disable Metrics/AbcSize rubocop:disable Metrics/CyclomaticComplexity rubocop:disable Metrics/PerceivedComplexity
432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 |
# File 'lib/hierarchable/hierarchable.rb', line 432 def hierarchy_descendants(include_self: false, models: :all, compact: false) return {} unless respond_to?(:hierarchy_ancestors_path) models = case models when Array models when :all hierarchy_descendant_models(include_self: true) else [self.class] end result = {} models.each do |model| model = model.safe_constantize if model.is_a?(String) query = if hierarchy_root? # If it's the root, we need to base the query based on the # hierarchy_root attribute since the ancestor_path will be # empty for a root node. See the README for the explanation # as to why the root node has values set to nil and the # path as the empty string. model.where( hierarchy_root_type: self.class.name, hierarchy_root_id: id ) else path = public_send(:hierarchy_full_path) model.where( 'hierarchy_ancestors_path LIKE ?', "#{model.sanitize_sql_like(path)}%" ) end # Make sure to include/exlude the current object depending on what the # user wants if model == self.class query = if include_self query.or(model.where(id:)) else query.where.not(id:) end end result[model.to_s] = query end # Compact the results if necessary _, result = result.first if result.size == 1 && compact result end |
#hierarchy_full_path ⇒ Object
Return the full hierarchy path from the root to this object.
Unlike the hierarchy_ancestors_path which DOES NOT include the current object in the path, this path contains both the ancestors path AND the current object.
564 565 566 567 568 569 570 571 572 573 574 575 576 |
# File 'lib/hierarchable/hierarchable.rb', line 564 def hierarchy_full_path return '' if new_record? || !respond_to?(:hierarchy_ancestors_path) if hierarchy_ancestors_path.present? format('%<path>s%<sep>s%<current>s', path: hierarchy_ancestors_path, sep: hierarchable_config[:path_separator], current: to_hierarchy_ancestors_path_format) else to_hierarchy_ancestors_path_format end end |
#hierarchy_full_path_reified ⇒ Object
Return the full hierarchy path from the root to this object as objects.
Unlike the hierarchy_full_path that returns a string of the path, this returns a list of items. The pattern of the returned list will be
[Class, Object, Class, Object, ...]
Where the Class is the class of the object coming right after it. This representation is useful when creating a breadcrumb and we want to have both all the ancestors (like in the ancestors method), but also the collections (classes), so that we can build up a nice path with links.
603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 |
# File 'lib/hierarchable/hierarchable.rb', line 603 def hierarchy_full_path_reified return '' if new_record? || !respond_to?(:hierarchy_ancestors_path) path = [] hierarchy_full_path.split(hierarchable_config[:path_separator]) .each do |record| ancestor_class_name, ancestor_id = record.split( hierarchable_config[:record_separator] ) ancestor_class = ancestor_class_name.safe_constantize path << ancestor_class path << ancestor_class.find(ancestor_id) end path end |
#hierarchy_parent(raw: false) ⇒ Object
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/hierarchable/hierarchable.rb', line 142 def hierarchy_parent(raw: false) return hierarchy_parent_relationship if raw # Depending on whether or not the object has been saved or not, we need # to be smart as to how we try to get the parent. If it's saved, then # the `hierarchy_parent` attribute in the model will be set and so we # can use the `belongs_to` relationship to get the parent. However, # if the parent has changed or the object has yet to be saved, we can't # use the relationship to get the parent as the value will not have been # set properly yet in the model (since it's a `before_save` hook). use_relationship = if persisted? !hierarchy_parent_changed? else false end if use_relationship hierarchy_parent_relationship else source = hierarchy_parent_source source.nil? ? nil : send(source) end end |
#hierarchy_parent_source ⇒ Object
Return the attribute name that links this object to its parent.
This should return the name of the attribute/relation/etc either as a string or symbol.
For example, if this is a Task, then the hierarchy_parent_source is likely the attribute that references the Project this task belongs to. If the method returns nil (the default behavior), the assumption is that this object is the root of the hierarchy.
494 495 496 497 498 499 |
# File 'lib/hierarchable/hierarchable.rb', line 494 def hierarchy_parent_source source = hierarchable_config[:parent_source] return nil unless source source.respond_to?(:call) ? source.call(self) : source end |
#hierarchy_path_for(objects) ⇒ Object
Return hierarchy path for given list of objects
579 580 581 582 583 584 585 |
# File 'lib/hierarchable/hierarchable.rb', line 579 def hierarchy_path_for(objects) return '' if objects.blank? objects.map do |obj| to_hierarchy_format(obj) end.join(hierarchable_config[:path_separator]) end |
#hierarchy_root? ⇒ Boolean
138 139 140 |
# File 'lib/hierarchable/hierarchable.rb', line 138 def hierarchy_root? hierarchy_root.nil? end |
#hierarchy_sibling_models(include_self: false) ⇒ Object
Get all of the sibling models
The ‘include_self` parameter can be set to decide what to include in the sibling models search. If set to `false` (default), then it will return all models other models that are siblings of the current object. If set to `true`, then it will also include the current object’s class.
319 320 321 322 323 324 325 326 |
# File 'lib/hierarchable/hierarchable.rb', line 319 def hierarchy_sibling_models(include_self: false) return [] unless respond_to?(:hierarchy_parent) return include_self ? [self.class] : [] if hierarchy_parent.blank? models = hierarchy_parent.hierarchy_children_models(include_self: false) models << self.class if include_self models.uniq end |
#hierarchy_siblings(include_self: false, models: :all, compact: false) ⇒ Object
Get siblings of an object.
If the ‘models` parameter is `:all` (default), then the result will contain objects of different types. E.g. if we have a Project, Task, and a Comment, the siblings of a Task may include both Tasks and Comments. If you only need this one particular model’s data, then set ‘models` to `:this`. If you want to specify a specific list of models then that can be passed as a list (e.g. [MyModel1, MyModel2]) rubocop:disable Metrics/CyclomaticComplexity rubocop:disable Metrics/PerceivedComplexity
338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 |
# File 'lib/hierarchable/hierarchable.rb', line 338 def hierarchy_siblings(include_self: false, models: :all, compact: false) return {} unless respond_to?(:hierarchy_parent_id) models = case models when Array models when :all hierarchy_sibling_models(include_self: true) else [self.class] end result = {} models.each do |model| model = model.safe_constantize if model.is_a?(String) query = model.where( hierarchy_parent_type: public_send(:hierarchy_parent_type), hierarchy_parent_id: public_send(:hierarchy_parent_id) ) query = query.where.not(id:) if model == self.class && !include_self result[model.to_s] = query end # Compact the results if necessary _, result = result.first if result.size == 1 && compact result end |
#to_hierarchy_ancestors_path_format ⇒ Object
Return the string representation of the current object in the format when used as part of a hierarchy.
If this is a new record (i.e. not saved yet), this will return “”, and will return the string representation of the format once it is saved.
553 554 555 556 557 |
# File 'lib/hierarchable/hierarchable.rb', line 553 def to_hierarchy_ancestors_path_format return '' if new_record? to_hierarchy_format(self) end |
#to_hierarchy_format(object) ⇒ Object
587 588 589 |
# File 'lib/hierarchable/hierarchable.rb', line 587 def to_hierarchy_format(object) "#{object.class}#{hierarchable_config[:record_separator]}#{object.id}" end |