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 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
- gem install acts-as-dag
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
- Robert Schmitt: I have modified the Rake system to support Jeweler to build this as a Ruby gem that can be installed and managed through the gem system. Updated to work without deprecation warnings from Rails 3.0.0.
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
@@ -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