acts-as-dag 1.1.0 → 1.1.2
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/MIT-LICENSE +20 -0
- data/README.rdoc +1 -1
- data/Rakefile +39 -0
- data/VERSION +1 -0
- data/lib/acts-as-dag.rb +11 -0
- data/lib/dag/dag.rb +791 -0
- data/test/dag_test.rb +0 -2
- data/test/database.test +0 -0
- metadata +15 -7
- data/lib/active_record/acts/dag.rb +0 -800
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) 2008 Matthew Leventi
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
CHANGED
|
@@ -28,7 +28,7 @@ Yes, but I think they aren't as fast or feature filled. Flogic has a good simple
|
|
|
28
28
|
|
|
29
29
|
This uses named_scope so your going to need Rails 2.1 or above.
|
|
30
30
|
|
|
31
|
-
Currently tested using Rails 2.3.8. Working on a separate version of acts-as-dag that is based on Rails 3.x and use all of the new ActiveRecord goodness found there.
|
|
31
|
+
Currently tested using Rails 2.3.8 and Ruby 1.9.2-p0. Working on a separate version of acts-as-dag that is based on Rails 3.x and use all of the new ActiveRecord goodness found there.
|
|
32
32
|
|
|
33
33
|
== Installation
|
|
34
34
|
|
data/Rakefile
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require 'rake'
|
|
2
|
+
require 'rake/testtask'
|
|
3
|
+
require 'rake/rdoctask'
|
|
4
|
+
|
|
5
|
+
desc 'Default: run unit tests.'
|
|
6
|
+
task :default => :test
|
|
7
|
+
|
|
8
|
+
desc 'Test the acts_as_dag plugin.'
|
|
9
|
+
Rake::TestTask.new(:test) do |t|
|
|
10
|
+
t.libs << 'lib'
|
|
11
|
+
t.pattern = 'test/**/*_test.rb'
|
|
12
|
+
t.verbose = true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
desc 'Generate documentation for the acts_as_dag plugin.'
|
|
16
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
|
17
|
+
rdoc.rdoc_dir = 'rdoc'
|
|
18
|
+
rdoc.title = 'ActsAsDag'
|
|
19
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
|
20
|
+
rdoc.rdoc_files.include('README.rdoc')
|
|
21
|
+
rdoc.rdoc_files.include('lib/active_record/acts/dag.rb')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# setup to build plugin as a gem
|
|
25
|
+
begin
|
|
26
|
+
require 'jeweler'
|
|
27
|
+
Jeweler::Tasks.new do |gemspec|
|
|
28
|
+
gemspec.name = "acts-as-dag"
|
|
29
|
+
gemspec.summary = "Acts As DAG Gem"
|
|
30
|
+
gemspec.description = "Acts As Dag, short for Acts As Directed Acyclic Graph, is a gem which allows you to represent DAG hierarchy using your ActiveRecord models."
|
|
31
|
+
gemspec.authors = ["Matthew Leventi", "Robert Schmitt"]
|
|
32
|
+
gemspec.email = "resgraph@cox.net"
|
|
33
|
+
gemspec.rubyforge_project = "acts-as-dag"
|
|
34
|
+
gemspec.homepage = "http://github.com/resgraph/acts-as-dag"
|
|
35
|
+
gemspec.files = FileList["[A-Z]*", "{lib,test}/**/*"]
|
|
36
|
+
end
|
|
37
|
+
rescue
|
|
38
|
+
puts "Jeweler or one of its dependencies is not installed."
|
|
39
|
+
end
|
data/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.1.2
|
data/lib/acts-as-dag.rb
ADDED
data/lib/dag/dag.rb
ADDED
|
@@ -0,0 +1,791 @@
|
|
|
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, '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_presence_of ancestor_type_column_name, descendant_type_column_name
|
|
69
|
+
validates_uniqueness_of ancestor_id_column_name, :scope => [ancestor_type_column_name, descendant_type_column_name, descendant_id_column_name]
|
|
70
|
+
|
|
71
|
+
named_scope :with_ancestor, lambda { |ancestor| {:conditions => {ancestor_id_column_name => ancestor.id, ancestor_type_column_name => ancestor.class.to_s}} }
|
|
72
|
+
named_scope :with_descendant, lambda { |descendant| {:conditions => {descendant_id_column_name => descendant.id, descendant_type_column_name => descendant.class.to_s}} }
|
|
73
|
+
|
|
74
|
+
named_scope :with_ancestor_point, lambda { |point| {:conditions => {ancestor_id_column_name => point.id, ancestor_type_column_name => point.type}} }
|
|
75
|
+
named_scope :with_descendant_point, lambda { |point| {:conditions => {descendant_id_column_name => point.id, descendant_type_column_name => point.type}} }
|
|
76
|
+
|
|
77
|
+
extend PolyEdgeClassMethods
|
|
78
|
+
include PolyEdgeClasses
|
|
79
|
+
include PolyEdgeInstanceMethods
|
|
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_uniqueness_of ancestor_id_column_name, :scope => [descendant_id_column_name]
|
|
85
|
+
|
|
86
|
+
named_scope :with_ancestor, lambda { |ancestor| {:conditions => {ancestor_id_column_name => ancestor.id}} }
|
|
87
|
+
named_scope :with_descendant, lambda { |descendant| {:conditions => {descendant_id_column_name => descendant.id}} }
|
|
88
|
+
|
|
89
|
+
named_scope :with_ancestor_point, lambda { |point| {:conditions => {ancestor_id_column_name => point.id}} }
|
|
90
|
+
named_scope :with_descendant_point, lambda { |point| {:conditions => {descendant_id_column_name => point.id}} }
|
|
91
|
+
|
|
92
|
+
extend NonPolyEdgeClassMethods
|
|
93
|
+
include NonPolyEdgeClasses
|
|
94
|
+
include NonPolyEdgeInstanceMethods
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
named_scope :direct, :conditions => {:direct => true}
|
|
98
|
+
named_scope :indirect, :conditions => {:direct => false}
|
|
99
|
+
|
|
100
|
+
named_scope :ancestor_nodes, :joins => :ancestor
|
|
101
|
+
named_scope :descendant_nodes, :joins => :descendant
|
|
102
|
+
|
|
103
|
+
validates_presence_of ancestor_id_column_name, descendant_id_column_name
|
|
104
|
+
validates_numericality_of ancestor_id_column_name, descendant_id_column_name
|
|
105
|
+
|
|
106
|
+
extend EdgeClassMethods
|
|
107
|
+
include EdgeInstanceMethods
|
|
108
|
+
|
|
109
|
+
before_destroy :destroyable!, :perpetuate
|
|
110
|
+
before_save :perpetuate
|
|
111
|
+
before_validation_on_update :field_check, :fill_defaults
|
|
112
|
+
before_validation_on_create :fill_defaults
|
|
113
|
+
|
|
114
|
+
#internal fields
|
|
115
|
+
code = 'def field_check ' + "\n"
|
|
116
|
+
internal_columns.each do |column|
|
|
117
|
+
code += "if " + column + "_changed? \n" + ' raise ActiveRecord::ActiveRecordError, "Column: '+column+' cannot be changed for an existing record it is immutable"' + "\n end \n"
|
|
118
|
+
end
|
|
119
|
+
code += 'end'
|
|
120
|
+
module_eval code
|
|
121
|
+
|
|
122
|
+
[count_column_name].each do |column|
|
|
123
|
+
module_eval <<-"end_eval", __FILE__, __LINE__
|
|
124
|
+
def #{column}=(x)
|
|
125
|
+
raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_dag code."
|
|
126
|
+
end
|
|
127
|
+
end_eval
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def has_dag_links(options = {})
|
|
132
|
+
conf = {
|
|
133
|
+
:class_name => nil,
|
|
134
|
+
:prefix => '',
|
|
135
|
+
:ancestor_class_names => [],
|
|
136
|
+
:descendant_class_names => []
|
|
137
|
+
}
|
|
138
|
+
conf.update(options)
|
|
139
|
+
|
|
140
|
+
#check that class_name is filled
|
|
141
|
+
if conf[:link_class_name].nil?
|
|
142
|
+
raise ActiveRecord::ActiveRecordError, "has_dag must be provided with :link_class_name option"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
#add trailing '_' to prefix
|
|
146
|
+
unless conf[:prefix] == ''
|
|
147
|
+
conf[:prefix] += '_'
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
prefix = conf[:prefix]
|
|
151
|
+
dag_link_class_name = conf[:link_class_name]
|
|
152
|
+
dag_link_class = conf[:link_class_name].constantize
|
|
153
|
+
|
|
154
|
+
if dag_link_class.acts_as_dag_polymorphic?
|
|
155
|
+
self.class_eval <<-EOL
|
|
156
|
+
has_many :#{prefix}links_as_ancestor, :as => :ancestor, :class_name => '#{dag_link_class_name}'
|
|
157
|
+
has_many :#{prefix}links_as_descendant, :as => :descendant, :class_name => '#{dag_link_class_name}'
|
|
158
|
+
|
|
159
|
+
has_many :#{prefix}links_as_parent, :as => :ancestor, :class_name => '#{dag_link_class_name}', :conditions => {'#{dag_link_class.direct_column_name}' => true}
|
|
160
|
+
has_many :#{prefix}links_as_child, :as => :descendant, :class_name => '#{dag_link_class_name}', :conditions => {'#{dag_link_class.direct_column_name}' => true}
|
|
161
|
+
|
|
162
|
+
EOL
|
|
163
|
+
|
|
164
|
+
ancestor_table_names = []
|
|
165
|
+
parent_table_names = []
|
|
166
|
+
conf[:ancestor_class_names].each do |class_name|
|
|
167
|
+
table_name = class_name.tableize
|
|
168
|
+
self.class_eval <<-EOL2
|
|
169
|
+
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}'}
|
|
170
|
+
has_many :#{prefix}ancestor_#{table_name}, :through => :#{prefix}links_as_descendant_for_#{table_name}, :source => :ancestor, :source_type => '#{class_name}'
|
|
171
|
+
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}
|
|
172
|
+
has_many :#{prefix}parent_#{table_name}, :through => :#{prefix}links_as_child_for_#{table_name}, :source => :ancestor, :source_type => '#{class_name}'
|
|
173
|
+
|
|
174
|
+
def #{prefix}root_for_#{table_name}?
|
|
175
|
+
return self.links_as_descendant_for_#{table_name}.empty?
|
|
176
|
+
end
|
|
177
|
+
EOL2
|
|
178
|
+
ancestor_table_names << (prefix+'ancestor_'+table_name)
|
|
179
|
+
parent_table_names << (prefix+'parent_'+table_name)
|
|
180
|
+
unless conf[:descendant_class_names].include?(class_name)
|
|
181
|
+
#this apparently is only one way is we can create some aliases making things easier
|
|
182
|
+
self.class_eval "has_many :#{prefix}#{table_name}, :through => :#{prefix}links_as_descendant_for_#{table_name}, :source => :ancestor, :source_type => '#{class_name}'"
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
unless conf[:ancestor_class_names].empty?
|
|
187
|
+
self.class_eval <<-EOL25
|
|
188
|
+
def #{prefix}ancestors
|
|
189
|
+
return #{ancestor_table_names.join(' + ')}
|
|
190
|
+
end
|
|
191
|
+
def #{prefix}parents
|
|
192
|
+
return #{parent_table_names.join(' + ')}
|
|
193
|
+
end
|
|
194
|
+
EOL25
|
|
195
|
+
else
|
|
196
|
+
self.class_eval <<-EOL26
|
|
197
|
+
def #{prefix}ancestors
|
|
198
|
+
a = []
|
|
199
|
+
#{prefix}links_as_descendant.each do |link|
|
|
200
|
+
a << link.ancestor
|
|
201
|
+
end
|
|
202
|
+
return a
|
|
203
|
+
end
|
|
204
|
+
def #{prefix}parents
|
|
205
|
+
a = []
|
|
206
|
+
#{prefix}links_as_child.each do |link|
|
|
207
|
+
a << link.ancestor
|
|
208
|
+
end
|
|
209
|
+
return a
|
|
210
|
+
end
|
|
211
|
+
EOL26
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
descendant_table_names = []
|
|
215
|
+
child_table_names = []
|
|
216
|
+
conf[:descendant_class_names].each do |class_name|
|
|
217
|
+
table_name = class_name.tableize
|
|
218
|
+
self.class_eval <<-EOL3
|
|
219
|
+
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}'}
|
|
220
|
+
has_many :#{prefix}descendant_#{table_name}, :through => :#{prefix}links_as_ancestor_for_#{table_name}, :source => :descendant, :source_type => '#{class_name}'
|
|
221
|
+
|
|
222
|
+
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}
|
|
223
|
+
has_many :#{prefix}child_#{table_name}, :through => :#{prefix}links_as_parent_for_#{table_name}, :source => :descendant, :source_type => '#{class_name}'
|
|
224
|
+
|
|
225
|
+
def #{prefix}leaf_for_#{table_name}?
|
|
226
|
+
return self.links_as_ancestor_for_#{table_name}.empty?
|
|
227
|
+
end
|
|
228
|
+
EOL3
|
|
229
|
+
descendant_table_names << (prefix+'descendant_'+table_name)
|
|
230
|
+
child_table_names << (prefix+'child_'+table_name)
|
|
231
|
+
unless conf[:ancestor_class_names].include?(class_name)
|
|
232
|
+
self.class_eval "has_many :#{prefix}#{table_name}, :through => :#{prefix}links_as_ancestor_for_#{table_name}, :source => :descendant, :source_type => '#{class_name}'"
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
unless conf[:descendant_class_names].empty?
|
|
237
|
+
self.class_eval <<-EOL35
|
|
238
|
+
def #{prefix}descendants
|
|
239
|
+
return #{descendant_table_names.join(' + ')}
|
|
240
|
+
end
|
|
241
|
+
def #{prefix}children
|
|
242
|
+
return #{child_table_names.join(' + ')}
|
|
243
|
+
end
|
|
244
|
+
EOL35
|
|
245
|
+
else
|
|
246
|
+
self.class_eval <<-EOL36
|
|
247
|
+
def #{prefix}descendants
|
|
248
|
+
d = []
|
|
249
|
+
#{prefix}links_as_ancestor.each do |link|
|
|
250
|
+
d << link.descendant
|
|
251
|
+
end
|
|
252
|
+
return d
|
|
253
|
+
end
|
|
254
|
+
def #{prefix}children
|
|
255
|
+
d = []
|
|
256
|
+
#{prefix}links_as_parent.each do |link|
|
|
257
|
+
d << link.descendant
|
|
258
|
+
end
|
|
259
|
+
return d
|
|
260
|
+
end
|
|
261
|
+
EOL36
|
|
262
|
+
end
|
|
263
|
+
else
|
|
264
|
+
self.class_eval <<-EOL4
|
|
265
|
+
has_many :#{prefix}links_as_ancestor, :foreign_key => '#{dag_link_class.ancestor_id_column_name}', :class_name => '#{dag_link_class_name}'
|
|
266
|
+
has_many :#{prefix}links_as_descendant, :foreign_key => '#{dag_link_class.descendant_id_column_name}', :class_name => '#{dag_link_class_name}'
|
|
267
|
+
|
|
268
|
+
has_many :#{prefix}ancestors, :through => :#{prefix}links_as_descendant, :source => :ancestor
|
|
269
|
+
has_many :#{prefix}descendants, :through => :#{prefix}links_as_ancestor, :source => :descendant
|
|
270
|
+
|
|
271
|
+
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}
|
|
272
|
+
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}
|
|
273
|
+
|
|
274
|
+
has_many :#{prefix}parents, :through => :#{prefix}links_as_child, :source => :ancestor
|
|
275
|
+
has_many :#{prefix}children, :through => :#{prefix}links_as_parent, :source => :descendant
|
|
276
|
+
|
|
277
|
+
EOL4
|
|
278
|
+
end
|
|
279
|
+
self.class_eval <<-EOL5
|
|
280
|
+
def #{prefix}leaf?
|
|
281
|
+
return self.#{prefix}links_as_ancestor.empty?
|
|
282
|
+
end
|
|
283
|
+
def #{prefix}root?
|
|
284
|
+
return self.#{prefix}links_as_descendant.empty?
|
|
285
|
+
end
|
|
286
|
+
EOL5
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
#Methods that show the columns for polymorphic DAGs
|
|
291
|
+
module PolyColumns
|
|
292
|
+
def ancestor_type_column_name
|
|
293
|
+
acts_as_dag_options[:ancestor_type_column]
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def descendant_type_column_name
|
|
297
|
+
acts_as_dag_options[:descendant_type_column]
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
#Methods that show columns
|
|
302
|
+
module Columns
|
|
303
|
+
def ancestor_id_column_name
|
|
304
|
+
acts_as_dag_options[:ancestor_id_column]
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def descendant_id_column_name
|
|
308
|
+
acts_as_dag_options[:descendant_id_column]
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def direct_column_name
|
|
312
|
+
acts_as_dag_options[:direct_column]
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def count_column_name
|
|
316
|
+
acts_as_dag_options[:count_column]
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def acts_as_dag_polymorphic?
|
|
320
|
+
acts_as_dag_options[:polymorphic]
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
#Contains class methods that extend the link model for polymorphic DAGs
|
|
325
|
+
module PolyEdgeClassMethods
|
|
326
|
+
#Builds a hash that describes a link from a source and a sink
|
|
327
|
+
def conditions_for(source, sink)
|
|
328
|
+
{
|
|
329
|
+
ancestor_id_column_name => source.id,
|
|
330
|
+
ancestor_type_column_name => source.type,
|
|
331
|
+
descendant_id_column_name => sink.id,
|
|
332
|
+
descendant_type_column_name => sink.type
|
|
333
|
+
}
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
#Contains nested classes in the link model for polymorphic DAGs
|
|
337
|
+
module PolyEdgeClasses
|
|
338
|
+
#Encapsulates the necessary information about a graph node
|
|
339
|
+
class EndPoint
|
|
340
|
+
#Does the endpoint match a model or another endpoint
|
|
341
|
+
def matches?(other)
|
|
342
|
+
return (self.id == other.id) && (self.type == other.type) if other.is_a?(EndPoint)
|
|
343
|
+
return (self.id == other.id) && (self.type == other.class.to_s)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
#Factory Construction method that creates an EndPoint instance from a model
|
|
347
|
+
def self.from_resource(resource)
|
|
348
|
+
self.new(resource.id, resource.class.to_s)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
#Factory Construction method that creates an EndPoint instance from a model if necessary
|
|
352
|
+
def self.from(obj)
|
|
353
|
+
return obj if obj.kind_of?(EndPoint)
|
|
354
|
+
return self.from_resource(obj)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
#Initializes the EndPoint instance with an id and type
|
|
358
|
+
def initialize(id, type)
|
|
359
|
+
@id = id
|
|
360
|
+
@type = type
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
attr_reader :id, :type
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
#Encapsulates information about the source of a link
|
|
367
|
+
class Source < EndPoint
|
|
368
|
+
#Factory Construction method that generates a source from a link
|
|
369
|
+
def self.from_edge(edge)
|
|
370
|
+
self.new(edge.ancestor_id, edge.ancestor_type)
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
#Encapsulates information about the sink (destination) of a link
|
|
375
|
+
class Sink < EndPoint
|
|
376
|
+
#Factory Construction method that generates a sink from a link
|
|
377
|
+
def self.from_edge(edge)
|
|
378
|
+
self.new(edge.descendant_id, edge.descendant_type)
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
#Contains class methods that extend the link model for a nonpolymorphic DAG
|
|
384
|
+
module NonPolyEdgeClassMethods
|
|
385
|
+
#Builds a hash that describes a link from a source and a sink
|
|
386
|
+
def conditions_for(source, sink)
|
|
387
|
+
{
|
|
388
|
+
ancestor_id_column_name => source.id,
|
|
389
|
+
descendant_id_column_name => sink.id
|
|
390
|
+
}
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
#Contains nested classes in the link model for a nonpolymorphic DAG
|
|
394
|
+
module NonPolyEdgeClasses
|
|
395
|
+
#Encapsulates the necessary information about a graph node
|
|
396
|
+
class EndPoint
|
|
397
|
+
#Does an endpoint match another endpoint or model instance
|
|
398
|
+
def matches?(other)
|
|
399
|
+
return (self.id == other.id)
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
#Factory Construction method that creates an endpoint from a model
|
|
403
|
+
def self.from_resource(resource)
|
|
404
|
+
self.new(resource.id)
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
#Factory Construction method that creates an endpoint from a model if necessary
|
|
408
|
+
def self.from(obj)
|
|
409
|
+
return obj if obj.kind_of?(EndPoint)
|
|
410
|
+
return self.from_resource(obj)
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
#Initializes an endpoint based on an Id
|
|
414
|
+
def initialize(id)
|
|
415
|
+
@id = id
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
attr_reader :id
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
#Encapsulates information about the source of a link
|
|
422
|
+
class Source < EndPoint
|
|
423
|
+
#Factory Construction method creates a source instance from a link
|
|
424
|
+
def self.from_edge(edge)
|
|
425
|
+
return self.new(edge.ancestor_id)
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
#Encapsulates information about the sink of a link
|
|
429
|
+
class Sink < EndPoint
|
|
430
|
+
#Factory Construction method creates a sink instance from a link
|
|
431
|
+
def self.from_edge(edge)
|
|
432
|
+
return self.new(edge.descendant_id)
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
#Class methods that extend the link model for both polymorphic and nonpolymorphic graphs
|
|
438
|
+
module EdgeClassMethods
|
|
439
|
+
|
|
440
|
+
#Returns a new edge between two points
|
|
441
|
+
def build_edge(ancestor, descendant)
|
|
442
|
+
source = self::EndPoint.from(ancestor)
|
|
443
|
+
sink = self::EndPoint.from(descendant)
|
|
444
|
+
conditions = self.conditions_for(source, sink)
|
|
445
|
+
path = self.new(conditions)
|
|
446
|
+
path.make_direct
|
|
447
|
+
return path
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
#Finds an edge between two points, Must be direct
|
|
451
|
+
def find_edge(ancestor, descendant)
|
|
452
|
+
source = self::EndPoint.from(ancestor)
|
|
453
|
+
sink = self::EndPoint.from(descendant)
|
|
454
|
+
edge = self.find(:first, :conditions => self.conditions_for(source, sink).merge!({direct_column_name => true}))
|
|
455
|
+
return edge
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
#Finds a link between two points
|
|
459
|
+
def find_link(ancestor, descendant)
|
|
460
|
+
source = self::EndPoint.from(ancestor)
|
|
461
|
+
sink = self::EndPoint.from(descendant)
|
|
462
|
+
link = self.find(:first, :conditions => self.conditions_for(source, sink))
|
|
463
|
+
return link
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
#Finds or builds an edge between two points
|
|
467
|
+
def find_or_build_edge(ancestor, descendant)
|
|
468
|
+
edge = self.find_edge(ancestor, descendant)
|
|
469
|
+
return edge unless edge.nil?
|
|
470
|
+
return build_edge(ancestor, descendant)
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
#Creates an edge between two points using save
|
|
474
|
+
def create_edge(ancestor, descendant)
|
|
475
|
+
link = self.find_link(ancestor, descendant)
|
|
476
|
+
if link.nil?
|
|
477
|
+
edge = self.build_edge(ancestor, descendant)
|
|
478
|
+
return edge.save
|
|
479
|
+
else
|
|
480
|
+
link.make_direct
|
|
481
|
+
return link.save
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
#Creates an edge between two points using save! Returns created edge
|
|
486
|
+
def create_edge!(ancestor, descendant)
|
|
487
|
+
link = self.find_link(ancestor, descendant)
|
|
488
|
+
if link.nil?
|
|
489
|
+
edge = self.build_edge(ancestor, descendant)
|
|
490
|
+
edge.save!
|
|
491
|
+
return edge
|
|
492
|
+
else
|
|
493
|
+
link.make_direct
|
|
494
|
+
link.save!
|
|
495
|
+
return link
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
#Alias for create_edge
|
|
500
|
+
def connect(ancestor, descendant)
|
|
501
|
+
return self.create_edge(ancestor, descendant)
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
#Alias for create_edge!
|
|
505
|
+
def connect!(ancestor, descendant)
|
|
506
|
+
return self.create_edge!(ancestor, descendant)
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
#Determines if a link exists between two points
|
|
510
|
+
def connected?(ancestor, descendant)
|
|
511
|
+
return !self.find_link(ancestor, descendant).nil?
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
#Finds the longest path between ancestor and descendant returning as an array
|
|
515
|
+
def longest_path_between(ancestor, descendant, path=[])
|
|
516
|
+
longest = []
|
|
517
|
+
ancestor.children.each do |child|
|
|
518
|
+
if child == descendent
|
|
519
|
+
temp = path.clone
|
|
520
|
+
temp << child
|
|
521
|
+
if temp.length > longest.length
|
|
522
|
+
longest = temp
|
|
523
|
+
end
|
|
524
|
+
elsif self.connected?(child, descendant)
|
|
525
|
+
temp = path.clone
|
|
526
|
+
temp << child
|
|
527
|
+
temp = self.longest_path_between(child, descendant, temp)
|
|
528
|
+
if temp.length > longest.length
|
|
529
|
+
longest = temp
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
return longest
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
#Determines if an edge exists between two points
|
|
537
|
+
def edge?(ancestor, descendant)
|
|
538
|
+
return !self.find_edge(ancestor, descendant).nil?
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
#Alias for edge
|
|
542
|
+
def direct?(ancestor, descendant)
|
|
543
|
+
return self.edge?(ancestor, descendant)
|
|
544
|
+
end
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
#Instance methods included into link model for a polymorphic DAG
|
|
548
|
+
module PolyEdgeInstanceMethods
|
|
549
|
+
def ancestor_type
|
|
550
|
+
return self[ancestor_type_column_name]
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def descendant_type
|
|
554
|
+
return self[descendant_type_column_name]
|
|
555
|
+
end
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
#Instance methods included into the link model for a nonpolymorphic DAG
|
|
559
|
+
module NonPolyEdgeInstanceMethods
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
#Instance methods included into the link model for polymorphic and nonpolymorphic DAGs
|
|
563
|
+
module EdgeInstanceMethods
|
|
564
|
+
|
|
565
|
+
attr_accessor :do_not_perpetuate
|
|
566
|
+
|
|
567
|
+
#Validations on model instance creation. Ensures no duplicate links, no cycles, and correct count and direct attributes
|
|
568
|
+
def validate_on_create
|
|
569
|
+
#make sure no duplicates
|
|
570
|
+
if self.class.find_link(self.source, self.sink)
|
|
571
|
+
self.errors.add_to_base('Link already exists between these points')
|
|
572
|
+
end
|
|
573
|
+
#make sure no long cycles
|
|
574
|
+
if self.class.find_link(self.sink, self.source)
|
|
575
|
+
self.errors.add_to_base('Link already exists in the opposite direction')
|
|
576
|
+
end
|
|
577
|
+
#make sure no short cycles
|
|
578
|
+
if self.sink.matches?(self.source)
|
|
579
|
+
self.errors.add_to_base('Link must start and end in different places')
|
|
580
|
+
end
|
|
581
|
+
#make sure not impossible
|
|
582
|
+
if self.direct?
|
|
583
|
+
if self.count != 0
|
|
584
|
+
self.errors.add_to_base('Cannot create a direct link with a count other than 0')
|
|
585
|
+
end
|
|
586
|
+
else
|
|
587
|
+
if self.count < 1
|
|
588
|
+
self.errors.add_to_base('Cannot create an indirect link with a count less than 1')
|
|
589
|
+
end
|
|
590
|
+
end
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
#Validations on update. Makes sure that something changed, that not making a lonely link indirect, and count is correct.
|
|
594
|
+
def validate_on_update
|
|
595
|
+
unless self.changed?
|
|
596
|
+
self.errors.add_to_base('No changes')
|
|
597
|
+
end
|
|
598
|
+
if direct_changed?
|
|
599
|
+
if count_changed?
|
|
600
|
+
self.errors.add_to_base('Do not manually change the count value')
|
|
601
|
+
end
|
|
602
|
+
if !self.direct?
|
|
603
|
+
if self.count == 1
|
|
604
|
+
self.errors.add_to_base('Cannot make a direct link with count 1 indirect')
|
|
605
|
+
end
|
|
606
|
+
end
|
|
607
|
+
end
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
#Fill default direct and count values if necessary. In place of after_initialize method
|
|
611
|
+
def fill_defaults
|
|
612
|
+
self[direct_column_name] = true if self[direct_column_name].nil?
|
|
613
|
+
self[count_column_name] = 0 if self[count_column_name].nil?
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
#Whether the edge can be destroyed
|
|
617
|
+
def destroyable?
|
|
618
|
+
(self.count == 0) || (self.direct? && self.count == 1)
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
#Raises an exception if the edge is not destroyable. Otherwise makes the edge indirect before destruction to cleanup graph.
|
|
622
|
+
def destroyable!
|
|
623
|
+
raise ActiveRecord::ActiveRecordError, 'Cannot destroy this edge' unless destroyable?
|
|
624
|
+
#this triggers rewiring on destruction via perpetuate
|
|
625
|
+
if self.direct?
|
|
626
|
+
self[direct_column_name] = false
|
|
627
|
+
end
|
|
628
|
+
return true
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
#Analyzes the changes in a model instance and rewires as necessary.
|
|
632
|
+
def perpetuate
|
|
633
|
+
#flag set by links that were modified in association
|
|
634
|
+
return true if self.do_not_perpetuate
|
|
635
|
+
|
|
636
|
+
#if edge changed this was manually altered
|
|
637
|
+
if direct_changed?
|
|
638
|
+
if self.direct?
|
|
639
|
+
self[count_column_name] += 1
|
|
640
|
+
else
|
|
641
|
+
self[count_column_name] -= 1
|
|
642
|
+
end
|
|
643
|
+
self.wiring
|
|
644
|
+
end
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
#Id of the ancestor
|
|
648
|
+
def ancestor_id
|
|
649
|
+
return self[ancestor_id_column_name]
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
#Id of the descendant
|
|
653
|
+
def descendant_id
|
|
654
|
+
return self[descendant_id_column_name]
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
#Count of the edge, ie the edge exists in X ways
|
|
658
|
+
def count
|
|
659
|
+
return self[count_column_name]
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
#Changes the count of the edge. DO NOT CALL THIS OUTSIDE THE PLUGIN
|
|
663
|
+
def internal_count=(val)
|
|
664
|
+
self[count_column_name] = val
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
#Whether the link is direct, ie manually created
|
|
668
|
+
def direct?
|
|
669
|
+
return self[direct_column_name]
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
#Whether the link is an edge?
|
|
673
|
+
def edge?
|
|
674
|
+
return self[direct_column_name]
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
#Makes the link direct, ie an edge
|
|
678
|
+
def make_direct
|
|
679
|
+
self[direct_column_name] = true
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
#Makes an edge indirect, ie a link.
|
|
683
|
+
def make_indirect
|
|
684
|
+
self[direct_column_name] = false
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
#Source of the edge, creates if necessary
|
|
688
|
+
def source
|
|
689
|
+
@source = self.class::Source.from_edge(self) if @source.nil?
|
|
690
|
+
return @source
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
#Sink (destination) of the edge, creates if necessary
|
|
694
|
+
def sink
|
|
695
|
+
@sink = self.class::Sink.from_edge(self) if @sink.nil?
|
|
696
|
+
return @sink
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
#All links that end at the source
|
|
700
|
+
def links_to_source
|
|
701
|
+
self.class.with_descendant_point(self.source)
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
#all links that start from the sink
|
|
705
|
+
def links_from_sink
|
|
706
|
+
self.class.with_ancestor_point(self.sink)
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
protected
|
|
710
|
+
|
|
711
|
+
#Changes on a wire based on the count (destroy! or save!) (should not be called outside this plugin)
|
|
712
|
+
def push_associated_modification!(edge)
|
|
713
|
+
raise ActiveRecord::ActiveRecordError, 'Cannot modify ourself in this way' if edge == self
|
|
714
|
+
edge.do_not_perpetuate = true
|
|
715
|
+
if edge.count == 0
|
|
716
|
+
edge.destroy!
|
|
717
|
+
else
|
|
718
|
+
edge.save!
|
|
719
|
+
end
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
#Updates the wiring of edges that dependent on the current one
|
|
723
|
+
def rewire_crossing(above_leg, below_leg)
|
|
724
|
+
if above_leg.count_changed?
|
|
725
|
+
was = above_leg.count_was
|
|
726
|
+
was = 0 if was.nil?
|
|
727
|
+
above_leg_count = above_leg.count - was
|
|
728
|
+
if below_leg.count_changed?
|
|
729
|
+
raise ActiveRecord::ActiveRecordError, 'ERROR: both legs cannot 0 normal count change'
|
|
730
|
+
else
|
|
731
|
+
below_leg_count = below_leg.count
|
|
732
|
+
end
|
|
733
|
+
else
|
|
734
|
+
above_leg_count = above_leg.count
|
|
735
|
+
if below_leg.count_changed?
|
|
736
|
+
was = below_leg.count_was
|
|
737
|
+
was = 0 if was.nil?
|
|
738
|
+
below_leg_count = below_leg.count - was
|
|
739
|
+
else
|
|
740
|
+
raise ActiveRecord::ActiveRecordError, 'ERROR: both legs cannot have count changes'
|
|
741
|
+
end
|
|
742
|
+
end
|
|
743
|
+
count = above_leg_count * below_leg_count
|
|
744
|
+
source = above_leg.source
|
|
745
|
+
sink = below_leg.sink
|
|
746
|
+
bridging_leg = self.class.find_link(source, sink)
|
|
747
|
+
if bridging_leg.nil?
|
|
748
|
+
bridging_leg = self.class.new(self.class.conditions_for(source, sink))
|
|
749
|
+
bridging_leg.make_indirect
|
|
750
|
+
bridging_leg.internal_count = 0
|
|
751
|
+
end
|
|
752
|
+
bridging_leg.internal_count = bridging_leg.count + count
|
|
753
|
+
return bridging_leg
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
#Find the edges that need to be updated
|
|
757
|
+
def wiring
|
|
758
|
+
source = self.source
|
|
759
|
+
sink = self.sink
|
|
760
|
+
above_sources = []
|
|
761
|
+
self.links_to_source.each do |edge|
|
|
762
|
+
above_sources << edge.source
|
|
763
|
+
end
|
|
764
|
+
below_sinks = []
|
|
765
|
+
self.links_from_sink.each do |edge|
|
|
766
|
+
below_sinks << edge.sink
|
|
767
|
+
end
|
|
768
|
+
above_bridging_legs = []
|
|
769
|
+
#everything above me tied to my sink
|
|
770
|
+
above_sources.each do |above_source|
|
|
771
|
+
above_leg = self.class.find_link(above_source, source)
|
|
772
|
+
above_bridging_leg = self.rewire_crossing(above_leg, self)
|
|
773
|
+
above_bridging_legs << above_bridging_leg unless above_bridging_leg.nil?
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
#everything beneath me tied to my source
|
|
777
|
+
below_sinks.each do |below_sink|
|
|
778
|
+
below_leg = self.class.find_link(sink, below_sink)
|
|
779
|
+
below_bridging_leg = self.rewire_crossing(self, below_leg)
|
|
780
|
+
self.push_associated_modification!(below_bridging_leg)
|
|
781
|
+
above_bridging_legs.each do |above_bridging_leg|
|
|
782
|
+
long_leg = self.rewire_crossing(above_bridging_leg, below_leg)
|
|
783
|
+
self.push_associated_modification!(long_leg)
|
|
784
|
+
end
|
|
785
|
+
end
|
|
786
|
+
above_bridging_legs.each do |above_bridging_leg|
|
|
787
|
+
self.push_associated_modification!(above_bridging_leg)
|
|
788
|
+
end
|
|
789
|
+
end
|
|
790
|
+
end
|
|
791
|
+
end
|