acts-as-dag 2.0.1 → 2.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +9 -10
- data/lib/dag.rb +18 -0
- data/lib/dag/columns.rb +26 -0
- data/lib/dag/dag.rb +296 -0
- data/lib/dag/edges.rb +301 -0
- data/lib/dag/poly_columns.rb +12 -0
- data/lib/dag/polymorphic.rb +76 -0
- data/lib/dag/standard.rb +62 -0
- data/lib/dag/validators.rb +58 -0
- data/test/dag_test.rb +0 -1
- data/test/database.test +0 -0
- metadata +12 -4
- data/lib/active_record/acts/dag.rb +0 -803
data/README.rdoc
CHANGED
@@ -6,6 +6,12 @@ Acts As Dag, short for Acts As Directed Acyclic Graph, is a plugin which allows
|
|
6
6
|
|
7
7
|
Say you have been using one of the many great plugins that allow for Tree hierarchy like: acts_as_tree or acts_as_nested_set, acts_as_better_nested_set, etc. Yet, you feel something is missing. Tree's are just not good enough. You want to allow each record to have multiple parent objects within a hierarchy as well as multiple children. Then you are going to need a DAG instead of a tree, and thats were this plugin becomes useful.
|
8
8
|
|
9
|
+
Version 1.1.1 tested using Rails 2.3.8 and Ruby 1.9.2-p0.
|
10
|
+
|
11
|
+
Version 2.0.x tested using Rails 3.0.0 and Ruby 1.9.2-p0, without major structural changes to code from the Rails 3 framework. Removes all deprecation warnings I have received while using AAD and Rails 3.0.0.
|
12
|
+
|
13
|
+
Version 2.5.0 is based on Rails 3.0.0 and uses all of the new ActiveModel/ActiveRecord goodness found there.
|
14
|
+
|
9
15
|
=== What's a DAG?
|
10
16
|
|
11
17
|
http://en.wikipedia.org/wiki/Directed_acyclic_graph
|
@@ -28,17 +34,11 @@ Yes, but I think they aren't as fast or feature filled. Flogic has a good simple
|
|
28
34
|
|
29
35
|
Version 1.x of Acts As Dag uses named_scope so your going to need Rails 2.1 or above.
|
30
36
|
|
31
|
-
Version 2.x of Acts As Dag uses validate and scope, so you are going to need Rails 3.0 or higher.
|
32
|
-
|
33
|
-
Version 1.1.1 tested using Rails 2.3.8 and Ruby 1.9.2-p0.
|
34
|
-
|
35
|
-
Version 2.0.0 tested using Rails 3.0.0 and Ruby 1.9.2-p0, without major structural changes to code from the Rails 3 framework. Removes all deprecation warnings I have received while using AAD and Rails 3.0.0.
|
36
|
-
|
37
|
-
Version 2.1.x (in development) will be based on Rails 3.0.0 and attempt to use all of the new ActiveRecord goodness found there.
|
37
|
+
Version 2.x of Acts As Dag uses validate and scope/where and ActiveModel::Validator, so you are going to need Rails 3.0 or higher.
|
38
38
|
|
39
39
|
== Installation
|
40
40
|
|
41
|
-
|
41
|
+
gem install acts-as-dag
|
42
42
|
|
43
43
|
== Terminology
|
44
44
|
|
@@ -284,5 +284,4 @@ Authors:: Matthew Leventi, Robert Schmitt
|
|
284
284
|
|
285
285
|
Algorithm and plugin designed by Matthew Leventi Email:(first letter of my first name followed by my last name @gmail.com). Im open to questions, very open to bugs, and even more receptive of bug fixes. I am also currently (August 2008) looking for a job just having graduated University of Rochester.
|
286
286
|
|
287
|
-
|
288
|
-
|
287
|
+
Gemification and upgrade to Rails 3.0.0 and Ruby 1.9.2 performed by Robert Schmitt. Please feel free to contact me with bug reports and suggestions, and any patches you may have developed/applied.
|
data/lib/dag.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require "active_model"
|
2
|
+
require "active_record"
|
3
|
+
|
4
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
5
|
+
|
6
|
+
require "dag/dag"
|
7
|
+
require "dag/columns"
|
8
|
+
require "dag/poly_columns"
|
9
|
+
require "dag/polymorphic"
|
10
|
+
require "dag/standard"
|
11
|
+
require "dag/edges"
|
12
|
+
require "dag/validators"
|
13
|
+
|
14
|
+
$LOAD_PATH.shift
|
15
|
+
|
16
|
+
if defined?(ActiveRecord::Base)
|
17
|
+
ActiveRecord::Base.extend Dag
|
18
|
+
end
|
data/lib/dag/columns.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module Dag
|
2
|
+
|
3
|
+
#Methods that show columns
|
4
|
+
module Columns
|
5
|
+
def ancestor_id_column_name
|
6
|
+
acts_as_dag_options[:ancestor_id_column]
|
7
|
+
end
|
8
|
+
|
9
|
+
def descendant_id_column_name
|
10
|
+
acts_as_dag_options[:descendant_id_column]
|
11
|
+
end
|
12
|
+
|
13
|
+
def direct_column_name
|
14
|
+
acts_as_dag_options[:direct_column]
|
15
|
+
end
|
16
|
+
|
17
|
+
def count_column_name
|
18
|
+
acts_as_dag_options[:count_column]
|
19
|
+
end
|
20
|
+
|
21
|
+
def acts_as_dag_polymorphic?
|
22
|
+
acts_as_dag_options[:polymorphic]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
data/lib/dag/dag.rb
ADDED
@@ -0,0 +1,296 @@
|
|
1
|
+
module Dag
|
2
|
+
|
3
|
+
#Sets up a model to act as dag links for models specified under the :for option
|
4
|
+
def acts_as_dag_links(options = {})
|
5
|
+
conf = {
|
6
|
+
:ancestor_id_column => 'ancestor_id',
|
7
|
+
:ancestor_type_column => 'ancestor_type',
|
8
|
+
:descendant_id_column => 'descendant_id',
|
9
|
+
:descendant_type_column => 'descendant_type',
|
10
|
+
:direct_column => 'direct',
|
11
|
+
:count_column => 'count',
|
12
|
+
:polymorphic => false,
|
13
|
+
:node_class_name => nil}
|
14
|
+
conf.update(options)
|
15
|
+
|
16
|
+
unless conf[:polymorphic]
|
17
|
+
if conf[:node_class_name].nil?
|
18
|
+
raise ActiveRecord::ActiveRecordError, 'ERROR: Non-polymorphic graphs need to specify :node_class_name with the receiving class like belong_to'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
write_inheritable_attribute :acts_as_dag_options, conf
|
23
|
+
class_inheritable_reader :acts_as_dag_options
|
24
|
+
|
25
|
+
extend Columns
|
26
|
+
include Columns
|
27
|
+
|
28
|
+
#access to _changed? and _was for (edge,count) if not default
|
29
|
+
unless direct_column_name == 'direct'
|
30
|
+
module_eval <<-"end_eval", __FILE__, __LINE__
|
31
|
+
def direct_changed?
|
32
|
+
self.#{direct_column_name}_changed?
|
33
|
+
end
|
34
|
+
def direct_was
|
35
|
+
self.#{direct_column_name}_was
|
36
|
+
end
|
37
|
+
end_eval
|
38
|
+
end
|
39
|
+
|
40
|
+
unless count_column_name == 'count'
|
41
|
+
module_eval <<-"end_eval", __FILE__, __LINE__
|
42
|
+
def count_changed?
|
43
|
+
self.#{count_column_name}_changed?
|
44
|
+
end
|
45
|
+
def count_was
|
46
|
+
self.#{count_column_name}_was
|
47
|
+
end
|
48
|
+
end_eval
|
49
|
+
end
|
50
|
+
|
51
|
+
internal_columns = [ancestor_id_column_name, descendant_id_column_name]
|
52
|
+
edge_class_name = self.to_s
|
53
|
+
|
54
|
+
direct_column_name.intern
|
55
|
+
count_column_name.intern
|
56
|
+
|
57
|
+
#links to ancestor and descendant
|
58
|
+
if acts_as_dag_polymorphic?
|
59
|
+
extend PolyColumns
|
60
|
+
include PolyColumns
|
61
|
+
|
62
|
+
internal_columns << ancestor_type_column_name
|
63
|
+
internal_columns << descendant_type_column_name
|
64
|
+
|
65
|
+
belongs_to :ancestor, :polymorphic => true
|
66
|
+
belongs_to :descendant, :polymorphic => true
|
67
|
+
|
68
|
+
validates ancestor_type_column_name.to_sym, :presence => true
|
69
|
+
validates descendant_type_column_name.to_sym, :presence => true
|
70
|
+
validates ancestor_id_column_name.to_sym, :uniqueness => {:scope => [ancestor_type_column_name, descendant_type_column_name, descendant_id_column_name]}
|
71
|
+
|
72
|
+
scope :with_ancestor, lambda { |ancestor| where(ancestor_id_column_name => ancestor.id, ancestor_type_column_name => ancestor.class.to_s) }
|
73
|
+
scope :with_descendant, lambda { |descendant| where(descendant_id_column_name => descendant.id, descendant_type_column_name => descendant.class.to_s) }
|
74
|
+
|
75
|
+
scope :with_ancestor_point, lambda { |point| where(ancestor_id_column_name => point.id, ancestor_type_column_name => point.type) }
|
76
|
+
scope :with_descendant_point, lambda { |point| where(descendant_id_column_name => point.id, descendant_type_column_name => point.type) }
|
77
|
+
|
78
|
+
extend Polymorphic
|
79
|
+
include Polymorphic
|
80
|
+
else
|
81
|
+
belongs_to :ancestor, :foreign_key => ancestor_id_column_name, :class_name => acts_as_dag_options[:node_class_name]
|
82
|
+
belongs_to :descendant, :foreign_key => descendant_id_column_name, :class_name => acts_as_dag_options[:node_class_name]
|
83
|
+
|
84
|
+
validates ancestor_id_column_name.to_sym, :uniqueness => {:scope => [descendant_id_column_name]}
|
85
|
+
|
86
|
+
scope :with_ancestor, lambda { |ancestor| where(ancestor_id_column_name => ancestor.id) }
|
87
|
+
scope :with_descendant, lambda { |descendant| where(descendant_id_column_name => descendant.id) }
|
88
|
+
|
89
|
+
scope :with_ancestor_point, lambda { |point| where(ancestor_id_column_name => point.id) }
|
90
|
+
scope :with_descendant_point, lambda { |point| where(descendant_id_column_name => point.id) }
|
91
|
+
|
92
|
+
extend Standard
|
93
|
+
include Standard
|
94
|
+
end
|
95
|
+
|
96
|
+
# TODO: rename? breaks when using 'where' query because :direct scope name and :direct => true parameter conflict?
|
97
|
+
scope :direct, :conditions => {:direct => true}
|
98
|
+
scope :indirect, :conditions => {:direct => false}
|
99
|
+
|
100
|
+
scope :ancestor_nodes, :joins => :ancestor
|
101
|
+
scope :descendant_nodes, :joins => :descendant
|
102
|
+
|
103
|
+
validates ancestor_id_column_name.to_sym, :presence => true,
|
104
|
+
:numericality => true
|
105
|
+
validates descendant_id_column_name.to_sym, :presence => true,
|
106
|
+
:numericality => true
|
107
|
+
|
108
|
+
extend Edges
|
109
|
+
include Edges
|
110
|
+
|
111
|
+
before_destroy :destroyable!, :perpetuate
|
112
|
+
before_save :perpetuate
|
113
|
+
before_validation :field_check, :fill_defaults, :on => :update
|
114
|
+
before_validation :fill_defaults, :on => :create
|
115
|
+
|
116
|
+
include ActiveModel::Validations
|
117
|
+
validates_with CreateCorrectnessValidator, :on => :create
|
118
|
+
validates_with UpdateCorrectnessValidator, :on => :update
|
119
|
+
|
120
|
+
|
121
|
+
#internal fields
|
122
|
+
code = 'def field_check ' + "\n"
|
123
|
+
internal_columns.each do |column|
|
124
|
+
code += "if " + column + "_changed? \n" + ' raise ActiveRecord::ActiveRecordError, "Column: '+column+' cannot be changed for an existing record it is immutable"' + "\n end \n"
|
125
|
+
end
|
126
|
+
code += 'end'
|
127
|
+
module_eval code
|
128
|
+
|
129
|
+
[count_column_name].each do |column|
|
130
|
+
module_eval <<-"end_eval", __FILE__, __LINE__
|
131
|
+
def #{column}=(x)
|
132
|
+
raise ActiveRecord::ActiveRecordError, "ERROR: Unauthorized assignment to #{column}: it's an internal field handled by acts_as_dag code."
|
133
|
+
end
|
134
|
+
end_eval
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def has_dag_links(options = {})
|
139
|
+
conf = {
|
140
|
+
:class_name => nil,
|
141
|
+
:prefix => '',
|
142
|
+
:ancestor_class_names => [],
|
143
|
+
:descendant_class_names => []
|
144
|
+
}
|
145
|
+
conf.update(options)
|
146
|
+
|
147
|
+
#check that class_name is filled
|
148
|
+
if conf[:link_class_name].nil?
|
149
|
+
raise ActiveRecord::ActiveRecordError, "has_dag_links must be provided with :link_class_name option"
|
150
|
+
end
|
151
|
+
|
152
|
+
#add trailing '_' to prefix
|
153
|
+
unless conf[:prefix] == ''
|
154
|
+
conf[:prefix] += '_'
|
155
|
+
end
|
156
|
+
|
157
|
+
prefix = conf[:prefix]
|
158
|
+
dag_link_class_name = conf[:link_class_name]
|
159
|
+
dag_link_class = conf[:link_class_name].constantize
|
160
|
+
|
161
|
+
if dag_link_class.acts_as_dag_polymorphic?
|
162
|
+
self.class_eval <<-EOL
|
163
|
+
has_many :#{prefix}links_as_ancestor, :as => :ancestor, :class_name => '#{dag_link_class_name}'
|
164
|
+
has_many :#{prefix}links_as_descendant, :as => :descendant, :class_name => '#{dag_link_class_name}'
|
165
|
+
|
166
|
+
has_many :#{prefix}links_as_parent, :as => :ancestor, :class_name => '#{dag_link_class_name}', :conditions => {'#{dag_link_class.direct_column_name}' => true}
|
167
|
+
has_many :#{prefix}links_as_child, :as => :descendant, :class_name => '#{dag_link_class_name}', :conditions => {'#{dag_link_class.direct_column_name}' => true}
|
168
|
+
|
169
|
+
EOL
|
170
|
+
|
171
|
+
ancestor_table_names = []
|
172
|
+
parent_table_names = []
|
173
|
+
conf[:ancestor_class_names].each do |class_name|
|
174
|
+
table_name = class_name.tableize
|
175
|
+
self.class_eval <<-EOL2
|
176
|
+
has_many :#{prefix}links_as_descendant_for_#{table_name}, :as => :descendant, :class_name => '#{dag_link_class_name}', :conditions => {'#{dag_link_class.ancestor_type_column_name}' => '#{class_name}'}
|
177
|
+
has_many :#{prefix}ancestor_#{table_name}, :through => :#{prefix}links_as_descendant_for_#{table_name}, :source => :ancestor, :source_type => '#{class_name}'
|
178
|
+
has_many :#{prefix}links_as_child_for_#{table_name}, :as => :descendant, :class_name => '#{dag_link_class_name}', :conditions => {'#{dag_link_class.ancestor_type_column_name}' => '#{class_name}','#{dag_link_class.direct_column_name}' => true}
|
179
|
+
has_many :#{prefix}parent_#{table_name}, :through => :#{prefix}links_as_child_for_#{table_name}, :source => :ancestor, :source_type => '#{class_name}'
|
180
|
+
|
181
|
+
def #{prefix}root_for_#{table_name}?
|
182
|
+
self.links_as_descendant_for_#{table_name}.empty?
|
183
|
+
end
|
184
|
+
EOL2
|
185
|
+
ancestor_table_names << (prefix+'ancestor_'+table_name)
|
186
|
+
parent_table_names << (prefix+'parent_'+table_name)
|
187
|
+
unless conf[:descendant_class_names].include?(class_name)
|
188
|
+
#this apparently is only one way is we can create some aliases making things easier
|
189
|
+
self.class_eval "has_many :#{prefix}#{table_name}, :through => :#{prefix}links_as_descendant_for_#{table_name}, :source => :ancestor, :source_type => '#{class_name}'"
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
unless conf[:ancestor_class_names].empty?
|
194
|
+
self.class_eval <<-EOL25
|
195
|
+
def #{prefix}ancestors
|
196
|
+
#{ancestor_table_names.join(' + ')}
|
197
|
+
end
|
198
|
+
def #{prefix}parents
|
199
|
+
#{parent_table_names.join(' + ')}
|
200
|
+
end
|
201
|
+
EOL25
|
202
|
+
else
|
203
|
+
self.class_eval <<-EOL26
|
204
|
+
def #{prefix}ancestors
|
205
|
+
a = []
|
206
|
+
#{prefix}links_as_descendant.each do |link|
|
207
|
+
a << link.ancestor
|
208
|
+
end
|
209
|
+
a
|
210
|
+
end
|
211
|
+
def #{prefix}parents
|
212
|
+
a = []
|
213
|
+
#{prefix}links_as_child.each do |link|
|
214
|
+
a << link.ancestor
|
215
|
+
end
|
216
|
+
a
|
217
|
+
end
|
218
|
+
EOL26
|
219
|
+
end
|
220
|
+
|
221
|
+
descendant_table_names = []
|
222
|
+
child_table_names = []
|
223
|
+
conf[:descendant_class_names].each do |class_name|
|
224
|
+
table_name = class_name.tableize
|
225
|
+
self.class_eval <<-EOL3
|
226
|
+
has_many :#{prefix}links_as_ancestor_for_#{table_name}, :as => :ancestor, :class_name => '#{dag_link_class_name}', :conditions => {'#{dag_link_class.descendant_type_column_name}' => '#{class_name}'}
|
227
|
+
has_many :#{prefix}descendant_#{table_name}, :through => :#{prefix}links_as_ancestor_for_#{table_name}, :source => :descendant, :source_type => '#{class_name}'
|
228
|
+
|
229
|
+
has_many :#{prefix}links_as_parent_for_#{table_name}, :as => :ancestor, :class_name => '#{dag_link_class_name}', :conditions => {'#{dag_link_class.descendant_type_column_name}' => '#{class_name}','#{dag_link_class.direct_column_name}' => true}
|
230
|
+
has_many :#{prefix}child_#{table_name}, :through => :#{prefix}links_as_parent_for_#{table_name}, :source => :descendant, :source_type => '#{class_name}'
|
231
|
+
|
232
|
+
def #{prefix}leaf_for_#{table_name}?
|
233
|
+
self.links_as_ancestor_for_#{table_name}.empty?
|
234
|
+
end
|
235
|
+
EOL3
|
236
|
+
descendant_table_names << (prefix+'descendant_'+table_name)
|
237
|
+
child_table_names << (prefix+'child_'+table_name)
|
238
|
+
unless conf[:ancestor_class_names].include?(class_name)
|
239
|
+
self.class_eval "has_many :#{prefix}#{table_name}, :through => :#{prefix}links_as_ancestor_for_#{table_name}, :source => :descendant, :source_type => '#{class_name}'"
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
unless conf[:descendant_class_names].empty?
|
244
|
+
self.class_eval <<-EOL35
|
245
|
+
def #{prefix}descendants
|
246
|
+
#{descendant_table_names.join(' + ')}
|
247
|
+
end
|
248
|
+
def #{prefix}children
|
249
|
+
#{child_table_names.join(' + ')}
|
250
|
+
end
|
251
|
+
EOL35
|
252
|
+
else
|
253
|
+
self.class_eval <<-EOL36
|
254
|
+
def #{prefix}descendants
|
255
|
+
d = []
|
256
|
+
#{prefix}links_as_ancestor.each do |link|
|
257
|
+
d << link.descendant
|
258
|
+
end
|
259
|
+
d
|
260
|
+
end
|
261
|
+
def #{prefix}children
|
262
|
+
d = []
|
263
|
+
#{prefix}links_as_parent.each do |link|
|
264
|
+
d << link.descendant
|
265
|
+
end
|
266
|
+
d
|
267
|
+
end
|
268
|
+
EOL36
|
269
|
+
end
|
270
|
+
else
|
271
|
+
self.class_eval <<-EOL4
|
272
|
+
has_many :#{prefix}links_as_ancestor, :foreign_key => '#{dag_link_class.ancestor_id_column_name}', :class_name => '#{dag_link_class_name}'
|
273
|
+
has_many :#{prefix}links_as_descendant, :foreign_key => '#{dag_link_class.descendant_id_column_name}', :class_name => '#{dag_link_class_name}'
|
274
|
+
|
275
|
+
has_many :#{prefix}ancestors, :through => :#{prefix}links_as_descendant, :source => :ancestor
|
276
|
+
has_many :#{prefix}descendants, :through => :#{prefix}links_as_ancestor, :source => :descendant
|
277
|
+
|
278
|
+
has_many :#{prefix}links_as_parent, :foreign_key => '#{dag_link_class.ancestor_id_column_name}', :class_name => '#{dag_link_class_name}', :conditions => {'#{dag_link_class.direct_column_name}' => true}
|
279
|
+
has_many :#{prefix}links_as_child, :foreign_key => '#{dag_link_class.descendant_id_column_name}', :class_name => '#{dag_link_class_name}', :conditions => {'#{dag_link_class.direct_column_name}' => true}
|
280
|
+
|
281
|
+
has_many :#{prefix}parents, :through => :#{prefix}links_as_child, :source => :ancestor
|
282
|
+
has_many :#{prefix}children, :through => :#{prefix}links_as_parent, :source => :descendant
|
283
|
+
|
284
|
+
EOL4
|
285
|
+
end
|
286
|
+
self.class_eval <<-EOL5
|
287
|
+
def #{prefix}leaf?
|
288
|
+
self.#{prefix}links_as_ancestor.empty?
|
289
|
+
end
|
290
|
+
def #{prefix}root?
|
291
|
+
self.#{prefix}links_as_descendant.empty?
|
292
|
+
end
|
293
|
+
EOL5
|
294
|
+
end
|
295
|
+
|
296
|
+
end
|
data/lib/dag/edges.rb
ADDED
@@ -0,0 +1,301 @@
|
|
1
|
+
module Dag
|
2
|
+
module Edges
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.send :include, EdgeInstanceMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
#Class methods that extend the link model for both polymorphic and non-polymorphic graphs
|
9
|
+
#Returns a new edge between two points
|
10
|
+
def build_edge(ancestor, descendant)
|
11
|
+
source = self::EndPoint.from(ancestor)
|
12
|
+
sink = self::EndPoint.from(descendant)
|
13
|
+
conditions = self.conditions_for(source, sink)
|
14
|
+
path = self.new(conditions)
|
15
|
+
path.make_direct
|
16
|
+
path
|
17
|
+
end
|
18
|
+
|
19
|
+
#Finds an edge between two points, Must be direct
|
20
|
+
def find_edge(ancestor, descendant)
|
21
|
+
source = self::EndPoint.from(ancestor)
|
22
|
+
sink = self::EndPoint.from(descendant)
|
23
|
+
self.find(:first, :conditions => self.conditions_for(source, sink).merge!({direct_column_name => true}))
|
24
|
+
end
|
25
|
+
|
26
|
+
#Finds a link between two points
|
27
|
+
def find_link(ancestor, descendant)
|
28
|
+
source = self::EndPoint.from(ancestor)
|
29
|
+
sink = self::EndPoint.from(descendant)
|
30
|
+
self.find(:first, :conditions => self.conditions_for(source, sink))
|
31
|
+
end
|
32
|
+
|
33
|
+
#Finds or builds an edge between two points
|
34
|
+
def find_or_build_edge(ancestor, descendant)
|
35
|
+
edge = self.find_edge(ancestor, descendant)
|
36
|
+
return edge unless edge.nil?
|
37
|
+
return build_edge(ancestor, descendant)
|
38
|
+
end
|
39
|
+
|
40
|
+
#Creates an edge between two points using save
|
41
|
+
def create_edge(ancestor, descendant)
|
42
|
+
link = self.find_link(ancestor, descendant)
|
43
|
+
if link.nil?
|
44
|
+
edge = self.build_edge(ancestor, descendant)
|
45
|
+
return edge.save
|
46
|
+
else
|
47
|
+
link.make_direct
|
48
|
+
return link.save
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
#Creates an edge between two points using save! Returns created edge
|
53
|
+
def create_edge!(ancestor, descendant)
|
54
|
+
link = self.find_link(ancestor, descendant)
|
55
|
+
if link.nil?
|
56
|
+
edge = self.build_edge(ancestor, descendant)
|
57
|
+
edge.save!
|
58
|
+
edge
|
59
|
+
else
|
60
|
+
link.make_direct
|
61
|
+
link.save!
|
62
|
+
link
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
#Alias for create_edge
|
67
|
+
def connect(ancestor, descendant)
|
68
|
+
self.create_edge(ancestor, descendant)
|
69
|
+
end
|
70
|
+
|
71
|
+
#Alias for create_edge!
|
72
|
+
def connect!(ancestor, descendant)
|
73
|
+
self.create_edge!(ancestor, descendant)
|
74
|
+
end
|
75
|
+
|
76
|
+
#Determines if a link exists between two points
|
77
|
+
def connected?(ancestor, descendant)
|
78
|
+
!self.find_link(ancestor, descendant).nil?
|
79
|
+
end
|
80
|
+
|
81
|
+
#Finds the longest path between ancestor and descendant returning as an array
|
82
|
+
def longest_path_between(ancestor, descendant, path=[])
|
83
|
+
longest = []
|
84
|
+
ancestor.children.each do |child|
|
85
|
+
if child == descendent
|
86
|
+
temp = path.clone
|
87
|
+
temp << child
|
88
|
+
if temp.length > longest.length
|
89
|
+
longest = temp
|
90
|
+
end
|
91
|
+
elsif self.connected?(child, descendant)
|
92
|
+
temp = path.clone
|
93
|
+
temp << child
|
94
|
+
temp = self.longest_path_between(child, descendant, temp)
|
95
|
+
if temp.length > longest.length
|
96
|
+
longest = temp
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
longest
|
101
|
+
end
|
102
|
+
|
103
|
+
#Determines if an edge exists between two points
|
104
|
+
def edge?(ancestor, descendant)
|
105
|
+
!self.find_edge(ancestor, descendant).nil?
|
106
|
+
end
|
107
|
+
|
108
|
+
#Alias for edge
|
109
|
+
def direct?(ancestor, descendant)
|
110
|
+
self.edge?(ancestor, descendant)
|
111
|
+
end
|
112
|
+
|
113
|
+
#Instance methods included into the link model for polymorphic and non-polymorphic DAGs
|
114
|
+
module EdgeInstanceMethods
|
115
|
+
|
116
|
+
attr_accessor :do_not_perpetuate
|
117
|
+
|
118
|
+
#Fill default direct and count values if necessary. In place of after_initialize method
|
119
|
+
def fill_defaults
|
120
|
+
self[direct_column_name] = true if self[direct_column_name].nil?
|
121
|
+
self[count_column_name] = 0 if self[count_column_name].nil?
|
122
|
+
end
|
123
|
+
|
124
|
+
#Whether the edge can be destroyed
|
125
|
+
def destroyable?
|
126
|
+
(self.count == 0) || (self.direct? && self.count == 1)
|
127
|
+
end
|
128
|
+
|
129
|
+
#Raises an exception if the edge is not destroyable. Otherwise makes the edge indirect before destruction to cleanup graph.
|
130
|
+
def destroyable!
|
131
|
+
raise ActiveRecord::ActiveRecordError, 'ERROR: cannot destroy this edge' unless destroyable?
|
132
|
+
#this triggers rewiring on destruction via perpetuate
|
133
|
+
if self.direct?
|
134
|
+
self[direct_column_name] = false
|
135
|
+
end
|
136
|
+
true
|
137
|
+
end
|
138
|
+
|
139
|
+
#Analyzes the changes in a model instance and rewires as necessary.
|
140
|
+
def perpetuate
|
141
|
+
#flag set by links that were modified in association
|
142
|
+
return true if self.do_not_perpetuate
|
143
|
+
|
144
|
+
#if edge changed this was manually altered
|
145
|
+
if direct_changed?
|
146
|
+
if self.direct?
|
147
|
+
self[count_column_name] += 1
|
148
|
+
else
|
149
|
+
self[count_column_name] -= 1
|
150
|
+
end
|
151
|
+
self.wiring
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
#Id of the ancestor
|
156
|
+
def ancestor_id
|
157
|
+
self[ancestor_id_column_name]
|
158
|
+
end
|
159
|
+
|
160
|
+
#Id of the descendant
|
161
|
+
def descendant_id
|
162
|
+
self[descendant_id_column_name]
|
163
|
+
end
|
164
|
+
|
165
|
+
#Count of the edge, ie the edge exists in X ways
|
166
|
+
def count
|
167
|
+
self[count_column_name]
|
168
|
+
end
|
169
|
+
|
170
|
+
#Changes the count of the edge. DO NOT CALL THIS OUTSIDE THE PLUGIN
|
171
|
+
def internal_count=(val)
|
172
|
+
self[count_column_name] = val
|
173
|
+
end
|
174
|
+
|
175
|
+
#Whether the link is direct, ie manually created
|
176
|
+
def direct?
|
177
|
+
self[direct_column_name]
|
178
|
+
end
|
179
|
+
|
180
|
+
#Whether the link is an edge?
|
181
|
+
def edge?
|
182
|
+
self[direct_column_name]
|
183
|
+
end
|
184
|
+
|
185
|
+
#Makes the link direct, ie an edge
|
186
|
+
def make_direct
|
187
|
+
self[direct_column_name] = true
|
188
|
+
end
|
189
|
+
|
190
|
+
#Makes an edge indirect, ie a link.
|
191
|
+
def make_indirect
|
192
|
+
self[direct_column_name] = false
|
193
|
+
end
|
194
|
+
|
195
|
+
#Source of the edge, creates if necessary
|
196
|
+
def source
|
197
|
+
@source = self.class::Source.from_edge(self) if @source.nil?
|
198
|
+
@source
|
199
|
+
end
|
200
|
+
|
201
|
+
#Sink (destination) of the edge, creates if necessary
|
202
|
+
def sink
|
203
|
+
@sink = self.class::Sink.from_edge(self) if @sink.nil?
|
204
|
+
@sink
|
205
|
+
end
|
206
|
+
|
207
|
+
#All links that end at the source
|
208
|
+
def links_to_source
|
209
|
+
self.class.with_descendant_point(self.source)
|
210
|
+
end
|
211
|
+
|
212
|
+
#all links that start from the sink
|
213
|
+
def links_from_sink
|
214
|
+
self.class.with_ancestor_point(self.sink)
|
215
|
+
end
|
216
|
+
|
217
|
+
protected
|
218
|
+
|
219
|
+
#Changes on a wire based on the count (destroy! or save!) (should not be called outside this plugin)
|
220
|
+
def push_associated_modification!(edge)
|
221
|
+
raise ActiveRecord::ActiveRecordError, 'ERROR: cannot modify our self in this way' if edge == self
|
222
|
+
edge.do_not_perpetuate = true
|
223
|
+
if edge.count == 0
|
224
|
+
edge.destroy!
|
225
|
+
else
|
226
|
+
edge.save!
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
#Updates the wiring of edges that dependent on the current one
|
231
|
+
def rewire_crossing(above_leg, below_leg)
|
232
|
+
if above_leg.count_changed?
|
233
|
+
was = above_leg.count_was
|
234
|
+
was = 0 if was.nil?
|
235
|
+
above_leg_count = above_leg.count - was
|
236
|
+
if below_leg.count_changed?
|
237
|
+
raise ActiveRecord::ActiveRecordError, 'ERROR: both legs cannot 0 normal count change'
|
238
|
+
else
|
239
|
+
below_leg_count = below_leg.count
|
240
|
+
end
|
241
|
+
else
|
242
|
+
above_leg_count = above_leg.count
|
243
|
+
if below_leg.count_changed?
|
244
|
+
was = below_leg.count_was
|
245
|
+
was = 0 if was.nil?
|
246
|
+
below_leg_count = below_leg.count - was
|
247
|
+
else
|
248
|
+
raise ActiveRecord::ActiveRecordError, 'ERROR: both legs cannot have count changes'
|
249
|
+
end
|
250
|
+
end
|
251
|
+
count = above_leg_count * below_leg_count
|
252
|
+
source = above_leg.source
|
253
|
+
sink = below_leg.sink
|
254
|
+
bridging_leg = self.class.find_link(source, sink)
|
255
|
+
if bridging_leg.nil?
|
256
|
+
bridging_leg = self.class.new(self.class.conditions_for(source, sink))
|
257
|
+
bridging_leg.make_indirect
|
258
|
+
bridging_leg.internal_count = 0
|
259
|
+
end
|
260
|
+
bridging_leg.internal_count = bridging_leg.count + count
|
261
|
+
bridging_leg
|
262
|
+
end
|
263
|
+
|
264
|
+
#Find the edges that need to be updated
|
265
|
+
def wiring
|
266
|
+
source = self.source
|
267
|
+
sink = self.sink
|
268
|
+
above_sources = []
|
269
|
+
self.links_to_source.each do |edge|
|
270
|
+
above_sources << edge.source
|
271
|
+
end
|
272
|
+
below_sinks = []
|
273
|
+
self.links_from_sink.each do |edge|
|
274
|
+
below_sinks << edge.sink
|
275
|
+
end
|
276
|
+
above_bridging_legs = []
|
277
|
+
#everything above me tied to my sink
|
278
|
+
above_sources.each do |above_source|
|
279
|
+
above_leg = self.class.find_link(above_source, source)
|
280
|
+
above_bridging_leg = self.rewire_crossing(above_leg, self)
|
281
|
+
above_bridging_legs << above_bridging_leg unless above_bridging_leg.nil?
|
282
|
+
end
|
283
|
+
|
284
|
+
#everything beneath me tied to my source
|
285
|
+
below_sinks.each do |below_sink|
|
286
|
+
below_leg = self.class.find_link(sink, below_sink)
|
287
|
+
below_bridging_leg = self.rewire_crossing(self, below_leg)
|
288
|
+
self.push_associated_modification!(below_bridging_leg)
|
289
|
+
above_bridging_legs.each do |above_bridging_leg|
|
290
|
+
long_leg = self.rewire_crossing(above_bridging_leg, below_leg)
|
291
|
+
self.push_associated_modification!(long_leg)
|
292
|
+
end
|
293
|
+
end
|
294
|
+
above_bridging_legs.each do |above_bridging_leg|
|
295
|
+
self.push_associated_modification!(above_bridging_leg)
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
end
|
301
|
+
end
|