dagnabit 2.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +5 -0
- data/.document +5 -0
- data/.gitignore +6 -0
- data/LICENSE +20 -0
- data/README.rdoc +186 -0
- data/Rakefile +69 -0
- data/VERSION.yml +5 -0
- data/bin/dagnabit-test +53 -0
- data/dagnabit.gemspec +124 -0
- data/init.rb +1 -0
- data/lib/dagnabit.rb +17 -0
- data/lib/dagnabit/activation.rb +60 -0
- data/lib/dagnabit/link/associations.rb +18 -0
- data/lib/dagnabit/link/class_methods.rb +43 -0
- data/lib/dagnabit/link/configuration.rb +40 -0
- data/lib/dagnabit/link/cycle_prevention.rb +31 -0
- data/lib/dagnabit/link/named_scopes.rb +65 -0
- data/lib/dagnabit/link/transitive_closure_link_model.rb +86 -0
- data/lib/dagnabit/link/transitive_closure_recalculation.rb +17 -0
- data/lib/dagnabit/link/transitive_closure_recalculation/on_create.rb +104 -0
- data/lib/dagnabit/link/transitive_closure_recalculation/on_destroy.rb +125 -0
- data/lib/dagnabit/link/transitive_closure_recalculation/on_update.rb +13 -0
- data/lib/dagnabit/link/transitive_closure_recalculation/utilities.rb +56 -0
- data/lib/dagnabit/link/validations.rb +26 -0
- data/lib/dagnabit/node/associations.rb +84 -0
- data/lib/dagnabit/node/class_methods.rb +74 -0
- data/lib/dagnabit/node/configuration.rb +26 -0
- data/lib/dagnabit/node/neighbors.rb +73 -0
- data/test/connections/native_postgresql/connection.rb +17 -0
- data/test/connections/native_sqlite3/connection.rb +24 -0
- data/test/dagnabit/link/test_associations.rb +61 -0
- data/test/dagnabit/link/test_class_methods.rb +102 -0
- data/test/dagnabit/link/test_configuration.rb +38 -0
- data/test/dagnabit/link/test_cycle_prevention.rb +64 -0
- data/test/dagnabit/link/test_named_scopes.rb +32 -0
- data/test/dagnabit/link/test_transitive_closure_link_model.rb +69 -0
- data/test/dagnabit/link/test_transitive_closure_recalculation.rb +139 -0
- data/test/dagnabit/link/test_validations.rb +39 -0
- data/test/dagnabit/node/test_associations.rb +147 -0
- data/test/dagnabit/node/test_class_methods.rb +49 -0
- data/test/dagnabit/node/test_configuration.rb +29 -0
- data/test/dagnabit/node/test_neighbors.rb +91 -0
- data/test/helper.rb +27 -0
- data/test/models/beta_node.rb +3 -0
- data/test/models/custom_data_link.rb +4 -0
- data/test/models/customized_link.rb +7 -0
- data/test/models/customized_link_node.rb +4 -0
- data/test/models/link.rb +4 -0
- data/test/models/node.rb +3 -0
- data/test/schema/schema.rb +51 -0
- metadata +165 -0
@@ -0,0 +1,125 @@
|
|
1
|
+
module Dagnabit
|
2
|
+
module Link
|
3
|
+
module TransitiveClosureRecalculation
|
4
|
+
module OnDestroy
|
5
|
+
def after_destroy
|
6
|
+
super
|
7
|
+
update_transitive_closure_for_destroy(*quoted_dag_link_values)
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def update_transitive_closure_for_destroy(aid, did, atype, dtype)
|
13
|
+
my_table = self.class.quoted_table_name
|
14
|
+
my_aid, my_did, my_atype, my_dtype = quoted_dag_link_column_names
|
15
|
+
tc = self.class.transitive_closure_table_name
|
16
|
+
tc_aid, tc_did, tc_atype, tc_dtype = quoted_dag_link_column_names
|
17
|
+
|
18
|
+
with_temporary_edge_tables('suspect', 'trusty', 'new') do |suspect, trusty, new|
|
19
|
+
connection.execute <<-END
|
20
|
+
INSERT INTO #{suspect}
|
21
|
+
SELECT * FROM (
|
22
|
+
SELECT
|
23
|
+
TC1.#{tc_aid}, TC2.#{tc_did}, TC1.#{tc_atype}, TC2.#{tc_dtype}
|
24
|
+
FROM
|
25
|
+
#{tc} AS TC1, #{tc} AS TC2
|
26
|
+
WHERE
|
27
|
+
TC1.#{tc_did} = #{aid} AND TC2.#{tc_aid} = #{did}
|
28
|
+
AND
|
29
|
+
TC1.#{tc_dtype} = #{atype} AND TC2.#{tc_atype} = #{dtype}
|
30
|
+
UNION
|
31
|
+
SELECT
|
32
|
+
TC.#{tc_aid}, #{did}, TC.#{tc_atype}, #{dtype}
|
33
|
+
FROM
|
34
|
+
#{tc} AS TC
|
35
|
+
WHERE
|
36
|
+
TC.#{tc_did} = #{aid} AND TC.#{tc_dtype} = #{atype}
|
37
|
+
UNION
|
38
|
+
SELECT
|
39
|
+
#{aid}, TC.#{tc_did}, #{atype}, TC.#{tc_dtype}
|
40
|
+
FROM
|
41
|
+
#{tc} AS TC
|
42
|
+
WHERE
|
43
|
+
TC.#{tc_aid} = #{did} AND TC.#{tc_atype} = #{dtype}
|
44
|
+
UNION
|
45
|
+
SELECT
|
46
|
+
#{aid}, #{did}, #{atype}, #{dtype}
|
47
|
+
FROM
|
48
|
+
#{tc} AS TC
|
49
|
+
WHERE
|
50
|
+
TC.#{tc_aid} = #{aid} AND TC.#{tc_did} = #{did}
|
51
|
+
AND
|
52
|
+
TC.#{tc_atype} = #{atype} AND TC.#{tc_dtype} = #{dtype}
|
53
|
+
) AS tmp0
|
54
|
+
END
|
55
|
+
|
56
|
+
connection.execute <<-END
|
57
|
+
INSERT INTO #{trusty}
|
58
|
+
SELECT
|
59
|
+
#{tc_aid}, #{tc_did}, #{tc_atype}, #{tc_dtype}
|
60
|
+
FROM (
|
61
|
+
SELECT
|
62
|
+
#{tc_aid}, #{tc_did}, #{tc_atype}, #{tc_dtype}
|
63
|
+
FROM
|
64
|
+
#{tc} AS TC
|
65
|
+
WHERE NOT EXISTS (
|
66
|
+
SELECT *
|
67
|
+
FROM
|
68
|
+
#{suspect} AS SUSPECT
|
69
|
+
WHERE
|
70
|
+
SUSPECT.#{tc_aid} = TC.#{tc_aid} AND SUSPECT.#{tc_did} = TC.#{tc_did}
|
71
|
+
AND
|
72
|
+
SUSPECT.#{tc_atype} = TC.#{tc_atype} AND SUSPECT.#{tc_dtype} = TC.#{tc_dtype}
|
73
|
+
)
|
74
|
+
UNION
|
75
|
+
SELECT
|
76
|
+
#{my_aid}, #{my_did}, #{my_atype}, #{my_dtype}
|
77
|
+
FROM
|
78
|
+
#{my_table} AS G
|
79
|
+
WHERE
|
80
|
+
NOT (G.#{my_aid} = #{aid} AND g.#{my_atype} = #{atype}
|
81
|
+
AND
|
82
|
+
G.#{my_did} = #{did} AND g.#{my_dtype} = #{dtype})
|
83
|
+
) AS tmp0
|
84
|
+
END
|
85
|
+
|
86
|
+
connection.execute <<-END
|
87
|
+
INSERT INTO #{new}
|
88
|
+
SELECT * FROM (
|
89
|
+
SELECT * FROM #{trusty}
|
90
|
+
UNION
|
91
|
+
SELECT
|
92
|
+
T1.#{tc_aid}, T2.#{tc_aid}, T1.#{tc_atype}, T2.#{tc_dtype}
|
93
|
+
FROM
|
94
|
+
#{trusty} T1, #{trusty} T2
|
95
|
+
WHERE
|
96
|
+
T1.#{tc_aid} = T2.#{tc_aid} AND T1.#{tc_atype} = T2.#{tc_dtype}
|
97
|
+
UNION
|
98
|
+
SELECT
|
99
|
+
T1.#{tc_aid}, T3.#{tc_aid}, T1.#{tc_atype}, T3.#{tc_dtype}
|
100
|
+
FROM
|
101
|
+
#{trusty} T1, #{trusty} T2, #{trusty} T3
|
102
|
+
WHERE
|
103
|
+
T1.#{tc_aid} = T2.#{tc_aid} AND T2.#{tc_aid} = T3.#{tc_aid}
|
104
|
+
AND
|
105
|
+
T1.#{tc_dtype} = T2.#{tc_atype} AND T2.#{tc_dtype} = T3.#{tc_atype}
|
106
|
+
) AS tmp0
|
107
|
+
END
|
108
|
+
|
109
|
+
connection.execute <<-END
|
110
|
+
DELETE FROM #{tc} WHERE NOT EXISTS (
|
111
|
+
SELECT *
|
112
|
+
FROM
|
113
|
+
#{new} T
|
114
|
+
WHERE
|
115
|
+
T.#{tc_aid} = #{tc}.#{tc_aid} AND T.#{tc_did} = #{tc}.#{tc_did}
|
116
|
+
AND
|
117
|
+
T.#{tc_atype} = #{tc}.#{tc_atype} AND T.#{tc_dtype} = #{tc}.#{tc_dtype}
|
118
|
+
)
|
119
|
+
END
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Dagnabit
|
2
|
+
module Link
|
3
|
+
module TransitiveClosureRecalculation
|
4
|
+
module OnUpdate
|
5
|
+
def after_update
|
6
|
+
old_values = dag_link_column_names.map { |n| connection.quote(changes[n].try(:first) || send(n)) }
|
7
|
+
update_transitive_closure_for_destroy(*old_values)
|
8
|
+
update_transitive_closure_for_create
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Dagnabit
|
2
|
+
module Link
|
3
|
+
module TransitiveClosureRecalculation
|
4
|
+
module Utilities
|
5
|
+
private
|
6
|
+
|
7
|
+
def quoted_dag_link_values
|
8
|
+
dag_link_column_names.map { |n| connection.quote(send(n)) }
|
9
|
+
end
|
10
|
+
|
11
|
+
def quoted_dag_link_column_names
|
12
|
+
dag_link_column_names.map { |n| connection.quote_column_name(n) }
|
13
|
+
end
|
14
|
+
|
15
|
+
def dag_link_column_names
|
16
|
+
[ self.class.ancestor_id_column,
|
17
|
+
self.class.descendant_id_column,
|
18
|
+
self.class.ancestor_type_column,
|
19
|
+
self.class.descendant_type_column ]
|
20
|
+
end
|
21
|
+
|
22
|
+
def all_quoted_column_values
|
23
|
+
all_column_names.map { |n| connection.quote(send(n)) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def all_quoted_column_names
|
27
|
+
all_column_names.map { |n| connection.quote_column_name(n) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def all_column_names
|
31
|
+
all_columns.map { |c| c.name }
|
32
|
+
end
|
33
|
+
|
34
|
+
def with_temporary_edge_tables(*tables, &block)
|
35
|
+
tables.each do |table|
|
36
|
+
connection.create_table(table, :temporary => true, :id => false) do |t|
|
37
|
+
all_columns.each do |c|
|
38
|
+
t.send(c.type, c.name)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
yield(tables.map { |t| connection.quote_table_name(t) })
|
44
|
+
|
45
|
+
tables.each do |table|
|
46
|
+
connection.drop_table table
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def all_columns
|
51
|
+
self.class.columns.reject { |c| c.name == 'id' || c.name == 'type' }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Dagnabit
|
2
|
+
module Link
|
3
|
+
#
|
4
|
+
# Basic validations on link models.
|
5
|
+
#
|
6
|
+
# This module only installs ancestor and descendant presence validations;
|
7
|
+
# the only basic requirement for a link is that it have a valid start point
|
8
|
+
# and a valid end point. We validate the presence of the +ancestor+ and
|
9
|
+
# +descendant+, instead of +ancestor_id+ and +descendant_id+, in order to
|
10
|
+
# permit scenarios like this:
|
11
|
+
#
|
12
|
+
# n1 = Node.new
|
13
|
+
# n2 = Node.new
|
14
|
+
# l = Link.new(:ancestor => n1, :descendant => n2)
|
15
|
+
# l.save # will save l, n1, and n2
|
16
|
+
#
|
17
|
+
module Validations
|
18
|
+
def self.extended(base)
|
19
|
+
base.send(:validates_presence_of, :ancestor)
|
20
|
+
base.send(:validates_presence_of, :descendant)
|
21
|
+
base.send(:validates_associated, :ancestor)
|
22
|
+
base.send(:validates_associated, :descendant)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module Dagnabit
|
2
|
+
module Node
|
3
|
+
#
|
4
|
+
# Association macros added to node in a dagnabit dag.
|
5
|
+
#
|
6
|
+
# == Added associations
|
7
|
+
#
|
8
|
+
# * +links_as_parent+: Links for which this node is a parent.
|
9
|
+
# * +links_as_child+: Links for which this node is a child.
|
10
|
+
# * +links_as_ancestor+: Links for which this node is an ancestor.
|
11
|
+
# * +links_as_descendant+: Links for which this node is a descendant.
|
12
|
+
#
|
13
|
+
# == Illustration
|
14
|
+
#
|
15
|
+
# Suppose we have the following graph:
|
16
|
+
#
|
17
|
+
# n1
|
18
|
+
# |
|
19
|
+
# / \
|
20
|
+
# n2 n3
|
21
|
+
# \ /
|
22
|
+
# n4
|
23
|
+
#
|
24
|
+
# Here are some example queries and outputs:
|
25
|
+
#
|
26
|
+
# n1.links_as_parent # => [#<Link ancestor=n1, descendant=n2>, #<Link ancestor=n1, descendant=n3>]
|
27
|
+
# n4.links_as_child # => [#<Link ancestor=n2, descendant=n4>, #<Link ancestor=n3, descendant=n4>]
|
28
|
+
# n1.links_as_ancestor # => [#<Link ancestor=n1, descendant=n2>, #<Link ancestor=n1, descendant=n3>, #<Link ancestor=n1, descendant=n4>]
|
29
|
+
# n4.links_as_descendant # => [#<Link ancestor=n2, descendant=n4>, #<Link ancestor=n3, descendant=n4>, #<Link ancestor=n1, descendant=n4>]
|
30
|
+
#
|
31
|
+
# In this example, we used +Link+ as the class for all links. This isn't
|
32
|
+
# actually what you get back (see Dagnabit::Link for details), but the
|
33
|
+
# objects you get back _will_ have ancestor and descendant accessors.
|
34
|
+
#
|
35
|
+
module Associations
|
36
|
+
#
|
37
|
+
# Installs associations on the node model.
|
38
|
+
#
|
39
|
+
def self.extended(base)
|
40
|
+
base.install_associations
|
41
|
+
end
|
42
|
+
|
43
|
+
def install_associations
|
44
|
+
klass = self
|
45
|
+
link_class = klass.link_class_name.constantize
|
46
|
+
|
47
|
+
klass.send(:has_many,
|
48
|
+
:links_as_parent,
|
49
|
+
:class_name => klass.link_class_name,
|
50
|
+
:foreign_key => link_class.ancestor_id_column,
|
51
|
+
:conditions => { link_class.ancestor_type_column => klass.name },
|
52
|
+
:dependent => :destroy)
|
53
|
+
|
54
|
+
klass.send(:has_many,
|
55
|
+
:links_as_child,
|
56
|
+
:class_name => klass.link_class_name,
|
57
|
+
:foreign_key => link_class.descendant_id_column,
|
58
|
+
:conditions => { link_class.descendant_type_column => klass.name },
|
59
|
+
:dependent => :destroy)
|
60
|
+
|
61
|
+
klass.send(:has_many,
|
62
|
+
:links_as_ancestor,
|
63
|
+
:class_name => link_class.transitive_closure_class.name,
|
64
|
+
:foreign_key => link_class.ancestor_id_column,
|
65
|
+
:conditions => { link_class.ancestor_type_column => klass.name },
|
66
|
+
:readonly => true)
|
67
|
+
|
68
|
+
klass.send(:has_many,
|
69
|
+
:links_as_descendant,
|
70
|
+
:class_name => link_class.transitive_closure_class.name,
|
71
|
+
:foreign_key => link_class.descendant_id_column,
|
72
|
+
:conditions => { link_class.descendant_type_column => klass.name },
|
73
|
+
:readonly => true)
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def inherited(subclass)
|
79
|
+
super(subclass)
|
80
|
+
subclass.install_associations
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
module Dagnabit
|
4
|
+
module Node
|
5
|
+
module ClassMethods
|
6
|
+
#
|
7
|
+
# Returns a subgraph rooted at a given set of nodes.
|
8
|
+
#
|
9
|
+
# While you can retrieve all descendants of a node pretty easily (i.e.
|
10
|
+
# +node.descendants+), and all links from a node really easily (i.e.
|
11
|
+
# +node.links_as_ancestor), it's not quite as straightforward to get the
|
12
|
+
# nodes and edges (direct edges, that is) out of a graph.
|
13
|
+
#
|
14
|
+
# The subgraph is returned as an object with two properties: +nodes+ and
|
15
|
+
# +edges+. Both properties are instances of the Set class.
|
16
|
+
#
|
17
|
+
# == Examples
|
18
|
+
#
|
19
|
+
# In the following examples, the node class is called +Node+. Variables
|
20
|
+
# of the form +nM+, where M is an integer, denote Node instances.
|
21
|
+
#
|
22
|
+
# Retrieve all descendants of n1 and all direct edges "underneath" n1
|
23
|
+
# (i.e. n1 to its children and direct edges for each of n1's
|
24
|
+
# descendants):
|
25
|
+
#
|
26
|
+
# <pre>
|
27
|
+
# Node.subgraph_from(n1)
|
28
|
+
#
|
29
|
+
# => #<Graph nodes=... edges=...>
|
30
|
+
# </pre>
|
31
|
+
#
|
32
|
+
# Same as above, but builds a graph using n1 and n2 as roots.
|
33
|
+
#
|
34
|
+
# <pre>
|
35
|
+
# Node.subgraph_from(n1, n2)
|
36
|
+
# </pre>
|
37
|
+
#
|
38
|
+
# == Usage tip
|
39
|
+
#
|
40
|
+
# +subgraph_from+ forces loading of the +descendants+ and
|
41
|
+
# +links_as_parent+ +links_as_parent+ associations on Node. In some
|
42
|
+
# situations, you may experience better performance if you use
|
43
|
+
# ActiveRecord's eager-loading capabilities when using subgraph_from:
|
44
|
+
#
|
45
|
+
# <pre>
|
46
|
+
# roots = Node.find(..., :include => [:descendants, :links_as_parent])
|
47
|
+
# Node.subgraph_from(roots)
|
48
|
+
# </pre>
|
49
|
+
#
|
50
|
+
def subgraph_from(*roots)
|
51
|
+
returning(OpenStruct.new) do |g|
|
52
|
+
g.nodes = all_nodes_of(roots)
|
53
|
+
g.edges = direct_edges_of(roots)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def all_nodes_of(roots)
|
60
|
+
roots.inject(Set.new) do |r, root|
|
61
|
+
r += root.descendants
|
62
|
+
r << root
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def direct_edges_of(roots)
|
67
|
+
roots.inject(Set.new) do |r, root|
|
68
|
+
r += root.links_as_ancestor.find(:all, :include => { :descendant => :links_as_parent }).map { |l| l.descendant.links_as_parent }.flatten
|
69
|
+
r += root.links_as_parent
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Dagnabit
|
2
|
+
module Node
|
3
|
+
#
|
4
|
+
# Configuration mechanism for nodes in a dagnabit-managed dag.
|
5
|
+
#
|
6
|
+
module Configuration
|
7
|
+
#
|
8
|
+
# Writes accessors for configuration data into a node.
|
9
|
+
#
|
10
|
+
# The following accessors are available:
|
11
|
+
# [link_class_name]
|
12
|
+
# The name of the model used to link nodes of this class.
|
13
|
+
#
|
14
|
+
def self.extended(base)
|
15
|
+
base.class_inheritable_accessor :link_class_name
|
16
|
+
end
|
17
|
+
|
18
|
+
#
|
19
|
+
# Configure node behavior.
|
20
|
+
#
|
21
|
+
def configure_acts_as_dag_node(link_class_name)
|
22
|
+
self.link_class_name = link_class_name
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Dagnabit
|
2
|
+
module Node
|
3
|
+
#
|
4
|
+
# Instance methods for finding out the neighbors of a node.
|
5
|
+
#
|
6
|
+
# These methods do _not_ behave like association proxies: they're just
|
7
|
+
# wrappers around <tt>ActiveRecord::Base#find</tt>. Therefore, they do not
|
8
|
+
# cache, do not support calculations, do not support extension modules,
|
9
|
+
# named scopes, etc.
|
10
|
+
#
|
11
|
+
# These methods aren't association proxies because a link's ancestor and
|
12
|
+
# descendant are polymorphic associations, and ActiveRecord does not
|
13
|
+
# support polymorphic has_many :through associations.
|
14
|
+
#
|
15
|
+
module Neighbors
|
16
|
+
#
|
17
|
+
# Finds the parents (immediate predecessors) of this node.
|
18
|
+
#
|
19
|
+
def parents
|
20
|
+
links_as_child.find(:all, :include => :ancestor).map { |l| l.ancestor }
|
21
|
+
end
|
22
|
+
|
23
|
+
#
|
24
|
+
# Finds the parents (immediate predecessors) of this node satisfying a given type.
|
25
|
+
#
|
26
|
+
def parents_of_type(type)
|
27
|
+
links_as_child.ancestor_type(type).find(:all, :include => :ancestor).map { |l| l.ancestor }
|
28
|
+
end
|
29
|
+
|
30
|
+
#
|
31
|
+
# Finds the children (immediate successors) of this node.
|
32
|
+
#
|
33
|
+
def children
|
34
|
+
links_as_parent.find(:all, :include => :descendant).map { |l| l.descendant }
|
35
|
+
end
|
36
|
+
|
37
|
+
#
|
38
|
+
# Finds the children (immediate successors) of this node satisfying a given type.
|
39
|
+
#
|
40
|
+
def children_of_type(type)
|
41
|
+
links_as_parent.descendant_type(type).find(:all, :include => :descendant).map { |l| l.descendant }
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
# Find the ancestors (predecessors) of this node.
|
46
|
+
#
|
47
|
+
def ancestors
|
48
|
+
links_as_descendant.find(:all, :include => :ancestor).map { |l| l.ancestor }
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# Find the ancestors (predecessors) of this node satisfying a given type.
|
53
|
+
#
|
54
|
+
def ancestors_of_type(type)
|
55
|
+
links_as_descendant.ancestor_type(type).find(:all, :include => :ancestor).map { |l| l.ancestor }
|
56
|
+
end
|
57
|
+
|
58
|
+
#
|
59
|
+
# Finds the descendants (successors) of this node.
|
60
|
+
#
|
61
|
+
def descendants
|
62
|
+
links_as_ancestor.find(:all, :include => :descendant).map { |l| l.descendant }
|
63
|
+
end
|
64
|
+
|
65
|
+
#
|
66
|
+
# Finds the descendants (successors) of this node satisfying a given type.
|
67
|
+
#
|
68
|
+
def descendants_of_type(type)
|
69
|
+
links_as_ancestor.descendant_type(type).find(:all, :include => :descendant).map { |l| l.descendant }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|