dagnabit 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|