diff options
Diffstat (limited to 'app/models/concerns/nested_tree.rb')
| -rw-r--r-- | app/models/concerns/nested_tree.rb | 78 |
1 files changed, 78 insertions, 0 deletions
diff --git a/app/models/concerns/nested_tree.rb b/app/models/concerns/nested_tree.rb new file mode 100644 index 0000000..befcdb3 --- /dev/null +++ b/app/models/concerns/nested_tree.rb | |||
| @@ -0,0 +1,78 @@ | |||
| 1 | # app/models/concerns/nested_tree.rb | ||
| 2 | # | ||
| 3 | # Minimal parent_id-based replacement for the tree-traversal subset of | ||
| 4 | # awesome_nested_set actually used by this app (descendants, ancestors, | ||
| 5 | # level, root, move_to_child_of) | ||
| 6 | module NestedTree | ||
| 7 | extend ActiveSupport::Concern | ||
| 8 | |||
| 9 | included do | ||
| 10 | belongs_to :parent, class_name: name, foreign_key: :parent_id, optional: true, inverse_of: :children | ||
| 11 | has_many :children, -> { order(:id) }, class_name: name, foreign_key: :parent_id, inverse_of: :parent | ||
| 12 | end | ||
| 13 | |||
| 14 | class_methods do | ||
| 15 | def root | ||
| 16 | roots.first | ||
| 17 | end | ||
| 18 | |||
| 19 | def roots | ||
| 20 | where(parent_id: nil) | ||
| 21 | end | ||
| 22 | |||
| 23 | def valid? | ||
| 24 | # Every non-root row's parent_id must point at a real, existing row. | ||
| 25 | where.not(parent_id: nil).where.missing(:parent).none? | ||
| 26 | end | ||
| 27 | end | ||
| 28 | |||
| 29 | def root? | ||
| 30 | parent_id.nil? | ||
| 31 | end | ||
| 32 | |||
| 33 | def ancestors | ||
| 34 | result = [] | ||
| 35 | current = parent | ||
| 36 | while current | ||
| 37 | result << current | ||
| 38 | current = current.parent | ||
| 39 | end | ||
| 40 | result | ||
| 41 | end | ||
| 42 | |||
| 43 | def level | ||
| 44 | ancestors.size | ||
| 45 | end | ||
| 46 | |||
| 47 | def descendants | ||
| 48 | ids = [] | ||
| 49 | queue = self.class.where(parent_id: id).pluck(:id) | ||
| 50 | until queue.empty? | ||
| 51 | ids.concat(queue) | ||
| 52 | queue = self.class.where(parent_id: queue).pluck(:id) | ||
| 53 | end | ||
| 54 | self.class.where(id: ids) | ||
| 55 | end | ||
| 56 | |||
| 57 | def self_and_descendants | ||
| 58 | self.class.where(id: [id] + descendants.pluck(:id)) | ||
| 59 | end | ||
| 60 | |||
| 61 | def self_and_descendants_ordered_with_level | ||
| 62 | nodes = [self] + descendants.to_a | ||
| 63 | children_by_parent = nodes.group_by(&:parent_id) | ||
| 64 | children_by_parent.each_value { |list| list.sort_by!(&:id) } | ||
| 65 | |||
| 66 | result = [] | ||
| 67 | visit = ->(node, level) do | ||
| 68 | result << [node, level] | ||
| 69 | (children_by_parent[node.id] || []).each { |child| visit.call(child, level + 1) } | ||
| 70 | end | ||
| 71 | visit.call(self, 0) | ||
| 72 | result | ||
| 73 | end | ||
| 74 | |||
| 75 | def move_to_child_of(new_parent) | ||
| 76 | update!(parent_id: new_parent.id) | ||
| 77 | end | ||
| 78 | end | ||
