Total Complexity | 110 |
Total Lines | 758 |
Duplicated Lines | 7.12 % |
Changes | 0 |
Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like ore.models.node often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
1 | from . import xml_fuzztree |
||
2 | from . import xml_faulttree |
||
3 | from .graph import Graph |
||
4 | |||
5 | import json |
||
6 | import notations |
||
7 | import sys |
||
8 | import datetime |
||
9 | import time |
||
10 | import logging |
||
11 | |||
12 | from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned |
||
13 | from django.db import models |
||
14 | from django.db.models.signals import post_save, pre_delete |
||
15 | from django.dispatch import receiver |
||
16 | |||
17 | logger = logging.getLogger('ore') |
||
18 | |||
19 | |||
20 | def new_client_id(): |
||
21 | return str(int(time.mktime(time.gmtime()))) |
||
22 | |||
23 | |||
24 | fuzztree_classes = { |
||
25 | 'topEvent': xml_fuzztree.TopEvent, |
||
26 | 'basicEvent': xml_fuzztree.BasicEvent, |
||
27 | 'basicEventSet': xml_fuzztree.BasicEventSet, |
||
28 | 'intermediateEvent': xml_fuzztree.IntermediateEvent, |
||
29 | 'intermediateEventSet': xml_fuzztree.IntermediateEventSet, |
||
30 | 'houseEvent': xml_fuzztree.HouseEvent, |
||
31 | 'undevelopedEvent': xml_fuzztree.UndevelopedEvent, |
||
32 | 'andGate': xml_fuzztree.And, |
||
33 | 'orGate': xml_fuzztree.Or, |
||
34 | 'xorGate': xml_fuzztree.Xor, |
||
35 | 'votingOrGate': xml_fuzztree.VotingOr, |
||
36 | 'featureVariation': xml_fuzztree.FeatureVariationPoint, |
||
37 | 'redundancyVariation': xml_fuzztree.RedundancyVariationPoint, |
||
38 | 'transferIn': xml_fuzztree.TransferIn, |
||
39 | xml_fuzztree.TopEvent_: 'topEvent', |
||
40 | xml_fuzztree.BasicEvent_: 'basicEvent', |
||
41 | xml_fuzztree.BasicEventSet_: 'basicEventSet', |
||
42 | xml_fuzztree.IntermediateEvent_: 'intermediateEvent', |
||
43 | xml_fuzztree.IntermediateEventSet_: 'intermediateEventSet', |
||
44 | xml_fuzztree.HouseEvent_: 'houseEvent', |
||
45 | xml_fuzztree.UndevelopedEvent_: 'undevelopedEvent', |
||
46 | xml_fuzztree.And_: 'andGate', |
||
47 | xml_fuzztree.Or_: 'orGate', |
||
48 | xml_fuzztree.Xor_: 'xorGate', |
||
49 | xml_fuzztree.VotingOr_: 'votingOrGate', |
||
50 | xml_fuzztree.FeatureVariationPoint_: 'featureVariation', |
||
51 | xml_fuzztree.RedundancyVariationPoint_: 'redundancyVariation', |
||
52 | xml_fuzztree.TransferIn_: 'transferIn' |
||
53 | } |
||
54 | |||
55 | faulttree_classes = { |
||
56 | 'topEvent': xml_faulttree.TopEvent, |
||
57 | 'basicEvent': xml_faulttree.BasicEvent, |
||
58 | 'basicEventSet': xml_faulttree.BasicEventSet, |
||
59 | 'intermediateEvent': xml_faulttree.IntermediateEvent, |
||
60 | 'intermediateEventSet': xml_faulttree.IntermediateEventSet, |
||
61 | 'houseEvent': xml_faulttree.HouseEvent, |
||
62 | 'undevelopedEvent': xml_faulttree.UndevelopedEvent, |
||
63 | 'andGate': xml_faulttree.And, |
||
64 | 'orGate': xml_faulttree.Or, |
||
65 | 'xorGate': xml_faulttree.Xor, |
||
66 | 'votingOrGate': xml_faulttree.VotingOr, |
||
67 | 'transferIn': xml_faulttree.TransferIn, |
||
68 | 'fdepGate': xml_faulttree.FDEP, |
||
69 | 'priorityAndGate': xml_faulttree.PriorityAnd, |
||
70 | 'seqGate': xml_faulttree.Sequence, |
||
71 | 'spareGate': xml_faulttree.Spare, |
||
72 | xml_faulttree.TopEvent_: 'topEvent', |
||
73 | xml_faulttree.BasicEvent_: 'basicEvent', |
||
74 | xml_faulttree.BasicEventSet_: 'basicEventSet', |
||
75 | xml_faulttree.IntermediateEvent_: 'intermediateEvent', |
||
76 | xml_faulttree.IntermediateEventSet_: 'intermediateEventSet', |
||
77 | xml_faulttree.HouseEvent_: 'houseEvent', |
||
78 | xml_faulttree.UndevelopedEvent_: 'undevelopedEvent', |
||
79 | xml_faulttree.And_: 'andGate', |
||
80 | xml_faulttree.Or_: 'orGate', |
||
81 | xml_faulttree.Xor_: 'xorGate', |
||
82 | xml_faulttree.VotingOr_: 'votingOrGate', |
||
83 | xml_faulttree.TransferIn_: 'transferIn', |
||
84 | xml_faulttree.FDEP_: 'fdepGate', |
||
85 | xml_faulttree.PriorityAnd_: 'priorityAndGate', |
||
86 | xml_faulttree.Sequence_: 'seqGate', |
||
87 | xml_faulttree.Spare_: 'spareGate' |
||
88 | } |
||
89 | |||
90 | |||
91 | class Node(models.Model): |
||
92 | |||
93 | """ |
||
94 | Class: Node |
||
95 | |||
96 | This class models a generic node for any diagram notation. |
||
97 | |||
98 | Fields: |
||
99 | {long} client_id - an id for this node that is generated by the client |
||
100 | {str} kind - a unique identifier for the kind of the node in its notation - e.g. "choice" for FuzzTrees. |
||
101 | Must be in the set of available node kinds of the owning graph's notation |
||
102 | {<Graph>} graph - the graph that owns the node |
||
103 | {int} x - the x coordinate of the node (default: 0) |
||
104 | {int} y - the y coordinate of the node (default: 0) |
||
105 | {bool} deleted - flag indicating whether this node is deleted. Simplifies restoration of nodes by toggling |
||
106 | the flag (default: False) |
||
107 | """ |
||
108 | class Meta: |
||
109 | app_label = 'ore' |
||
110 | |||
111 | # Nodes that are created by the server (e.g. default nodes in the notation) should receive ids starting at |
||
112 | # -sys.maxint and autoincrement from there on. The whole negative number range is reserved for the server. IDs from |
||
113 | # the client MUST be zero or greater |
||
114 | client_id = models.BigIntegerField(default=-sys.maxsize) |
||
115 | kind = models.CharField(max_length=127, choices=notations.node_choices) |
||
116 | graph = models.ForeignKey(Graph, null=False, related_name='nodes') |
||
117 | x = models.IntegerField(default=0) |
||
118 | y = models.IntegerField(default=0) |
||
119 | deleted = models.BooleanField(default=False) |
||
120 | |||
121 | def __unicode__(self): |
||
122 | prefix = '[DELETED] ' if self.deleted else '' |
||
123 | |||
124 | try: |
||
125 | name = unicode(self.properties.get(key='name').value) |
||
|
|||
126 | return unicode('%s%s' % (prefix, name)) |
||
127 | |||
128 | except ObjectDoesNotExist: |
||
129 | try: |
||
130 | name = notations.by_kind[ |
||
131 | self.graph.kind]['nodes'][ |
||
132 | self.kind]['properties']['name']['default'] |
||
133 | return unicode('%s%s_%s' % (prefix, self.pk, name)) |
||
134 | except KeyError: |
||
135 | return self.kind |
||
136 | |||
137 | def to_dict(self, use_value_dict=False): |
||
138 | """ |
||
139 | Method: to_dict |
||
140 | |||
141 | Serializes this node into a native dictionary |
||
142 | |||
143 | Returns: |
||
144 | {dict} the node as dictionary |
||
145 | """ |
||
146 | if use_value_dict: |
||
147 | prop_values = { |
||
148 | prop.key: { |
||
149 | 'value': prop.get_value()} for prop in self.properties.filter( |
||
150 | deleted=False)} |
||
151 | else: |
||
152 | prop_values = { |
||
153 | prop.key: prop.get_value() for prop in self.properties.filter( |
||
154 | deleted=False)} |
||
155 | return { |
||
156 | 'properties': prop_values, |
||
157 | 'id': self.client_id, |
||
158 | 'kind': self.kind, |
||
159 | 'x': self.x, |
||
160 | 'y': self.y, |
||
161 | 'outgoing': [edge.client_id for edge in self.outgoing.filter(deleted=False)], |
||
162 | 'incoming': [edge.client_id for edge in self.incoming.filter(deleted=False)] |
||
163 | } |
||
164 | |||
165 | def to_graphml(self): |
||
166 | """ |
||
167 | Method: to_graphml |
||
168 | |||
169 | Serializes this node instance into its graphml representation. Recursively serializes also its attributes. |
||
170 | |||
171 | Returns: |
||
172 | {str} this node instance as graphml |
||
173 | """ |
||
174 | |||
175 | return ''.join([ |
||
176 | ' <node id="%s">\n' |
||
177 | ' <data key="kind">%s</data>\n' |
||
178 | ' <data key="x">%d</data>\n' |
||
179 | ' <data key="y">%d</data>\n' % (self.client_id, self.kind, self.x, self.y,)] + |
||
180 | self.properties_to_graphml() + |
||
181 | [' </node>\n' |
||
182 | ]) |
||
183 | |||
184 | def properties_to_graphml(self): |
||
185 | # properties_notation = notations.by_kind[ |
||
186 | # self.graph.kind]['nodes'][ |
||
187 | # self.kind]['properties'] |
||
188 | graphml = [] |
||
189 | properties = self.properties.filter(deleted=False) |
||
190 | |||
191 | for prop in properties: |
||
192 | if prop.key == 'missionTime': |
||
193 | continue |
||
194 | # property_notation = properties_notation[prop.key] |
||
195 | # property_kind = property_notation['kind'] |
||
196 | # if property_kind == 'compound': |
||
197 | # part_kind = property_notation['parts'][value[0]]['partName'] |
||
198 | # graphml.append(self.graphml_data_key(key + 'Kind', part_kind)) |
||
199 | # graphml.append(self.graphml_data_key(key, value[1])) |
||
200 | # elif property_kind == 'epsilon': |
||
201 | # graphml.append(self.graphml_data_key(key, value[0])) |
||
202 | # graphml.append(self.graphml_data_key(key + 'Epsilon', value[1])) |
||
203 | # else: |
||
204 | # graphml.append(self.graphml_data_key(key, value)) |
||
205 | graphml.append(self.graphml_data_key(prop.key, prop.get_value())) |
||
206 | |||
207 | return graphml |
||
208 | |||
209 | def graphml_data_key(self, key, value): |
||
210 | return ' <data key="%s">%s</data>\n' % (key, str(value)) |
||
211 | |||
212 | def to_json(self, use_value_dict=False): |
||
213 | """ |
||
214 | Method: to_json |
||
215 | |||
216 | Serializes the values of this node into JSON notation. |
||
217 | |||
218 | Returns: |
||
219 | {str} the node in JSON representation |
||
220 | """ |
||
221 | return json.dumps(self.to_dict(use_value_dict)) |
||
222 | |||
223 | def to_bool_term(self): |
||
224 | edges = self.outgoing.filter(deleted=False).all() |
||
225 | children = [] |
||
226 | |||
227 | for edge in edges: |
||
228 | children.append(edge.target.to_bool_term()) |
||
229 | |||
230 | if self.kind == 'orGate': |
||
231 | return '(%s)' % (' or '.join(children)) |
||
232 | |||
233 | elif self.kind == 'andGate': |
||
234 | return '(%s)' % (' and '.join(children)) |
||
235 | |||
236 | elif self.kind in {'basicEvent'}: |
||
237 | return str(self.client_id) |
||
238 | |||
239 | elif self.kind == 'topEvent': |
||
240 | return str(children[0]) |
||
241 | |||
242 | raise ValueError('Node %s has unsupported kind' % self) |
||
243 | |||
244 | def children(self): |
||
245 | from .edge import Edge |
||
246 | return [edge.target for edge in Edge.objects.filter(source=self)] |
||
247 | |||
248 | def children_left2right(self): |
||
249 | return sorted(self.children(), key=lambda child: child.x) |
||
250 | |||
251 | def parents(self): |
||
252 | from .edge import Edge |
||
253 | return [edge.target for edge in Edge.objects.filter(target=self)] |
||
254 | |||
255 | def get_all_mirror_properties(self, hiddenProps=[]): |
||
256 | """ |
||
257 | Returns a sorted set of all node properties and their values, according to the notation rendering rules. |
||
258 | """ |
||
259 | result = [] |
||
260 | # Only consider properties that have to be displayed in the mirror |
||
261 | displayOrder = notations.by_kind[ |
||
262 | self.graph.kind]['propertiesDisplayOrder'] |
||
263 | propdetails = notations.by_kind[ |
||
264 | self.graph.kind]['nodes'][ |
||
265 | self.kind]['properties'] |
||
266 | for prop in displayOrder: |
||
267 | # the displayOrder list is static, the property does not have to be |
||
268 | # part of this node |
||
269 | if prop in propdetails and prop not in hiddenProps: |
||
270 | val = self.get_property(prop, None) |
||
271 | # Some properties do not have a config dict in notations, such |
||
272 | # as optional=None |
||
273 | if isinstance(propdetails[prop], dict): |
||
274 | kind = propdetails[prop]['kind'] |
||
275 | else: |
||
276 | logger.debug( |
||
277 | "Property '%s' in %s has no config dictionary" % |
||
278 | (prop, self.kind)) |
||
279 | kind = "text" |
||
280 | if val is not None: |
||
281 | if kind == "range": |
||
282 | format = propdetails[prop]['mirror']['format'] |
||
283 | format = format.replace( |
||
284 | u"\xb1", |
||
285 | "$\\pm$") # Special unicodes used in format strings, such as \xb1 |
||
286 | val = format.replace( |
||
287 | "{{$0}}", str( |
||
288 | val[0])).replace( |
||
289 | "{{$1}}", str( |
||
290 | val[1])) |
||
291 | elif kind == "compound": |
||
292 | # Compounds are unions, the first number tells us the |
||
293 | # active part defintion |
||
294 | active_part = val[0] |
||
295 | partkind = propdetails[prop][ |
||
296 | 'parts'][active_part]['kind'] |
||
297 | format = propdetails[prop]['parts'][ |
||
298 | active_part]['mirror']['format'] |
||
299 | logger.debug( |
||
300 | "Property '%s' with kind '%s' has part_kind '%s' with format '%s' for value '%s'" % |
||
301 | (prop, kind, partkind, format, str(val))) |
||
302 | # Special unicodes used in format strings must be |
||
303 | # replaced by their Latex counterpart |
||
304 | format = format.replace(u"\xb1", "$\\pm$") |
||
305 | format = format.replace(u"\u03bb", "$\\lambda$") |
||
306 | if partkind == 'epsilon': |
||
307 | val = format.replace( |
||
308 | "{{$0}}", str( |
||
309 | val[1][0])).replace( |
||
310 | "{{$1}}", str( |
||
311 | val[1][1])) |
||
312 | elif partkind == 'choice': |
||
313 | choices = propdetails[prop][ |
||
314 | 'parts'][active_part]['choices'] |
||
315 | choice_values = propdetails[prop][ |
||
316 | 'parts'][active_part]['values'] |
||
317 | for choice_name, choice_vals in zip( |
||
318 | choices, choice_values): |
||
319 | if val[1][0] == choice_vals[ |
||
320 | 0] and val[1][1] == choice_vals[1]: |
||
321 | val = format.replace("{{$0}}", choice_name) |
||
322 | break |
||
323 | elif partkind == 'numeric': |
||
324 | val = format.replace("{{$0}}", str(val[1])) |
||
325 | elif 'mirror' in propdetails[prop]: |
||
326 | if 'format' in propdetails[prop]['mirror']: |
||
327 | format = propdetails[prop]['mirror'][ |
||
328 | 'format'].encode('utf-8') |
||
329 | if isinstance(val, int): |
||
330 | val = str(val) |
||
331 | val = format.replace( |
||
332 | "{{$0}}".encode('utf-8'), |
||
333 | val.encode('utf-8')) |
||
334 | else: |
||
335 | logger.debug( |
||
336 | "Property '%s' has no specified mirror format" % |
||
337 | prop) |
||
338 | val = str(val) |
||
339 | else: |
||
340 | # Property has no special type and no mirror definition, so it shouldn't be shown |
||
341 | # One example is the name of the top event |
||
342 | continue |
||
343 | result.append(val) |
||
344 | return result |
||
345 | |||
346 | def to_tikz(self, x_offset=0, y_offset=0, parent_kind=None): |
||
347 | """ |
||
348 | Serializes this node and all its children into a TiKZ representation. |
||
349 | A positive x offset shifts the resulting tree accordingly to the right. |
||
350 | Negative offsets are allowed. |
||
351 | |||
352 | We are intentionally do not use the TiKZ tree rendering capabilities, since this |
||
353 | would ignore all user formatting of the tree from the editor. |
||
354 | |||
355 | TikZ starts the coordinate system in the upper left corner, while we start in the lower left corner. |
||
356 | This demands some coordinate mangling on the Y axis. |
||
357 | |||
358 | Returns: |
||
359 | {str} the node and its children in LaTex representation |
||
360 | """ |
||
361 | # Optional nodes are dashed |
||
362 | if self.get_property("optional", False): |
||
363 | nodeStyle = "shapeStyleDashed" |
||
364 | else: |
||
365 | nodeStyle = "shapeStyle" |
||
366 | # If this is a child node, we need to check if the parent wants to hide |
||
367 | # some child property |
||
368 | hiddenProps = [] |
||
369 | if parent_kind: |
||
370 | nodeConfig = notations.by_kind[ |
||
371 | self.graph.kind]['nodes'][parent_kind] |
||
372 | if 'childProperties' in nodeConfig: |
||
373 | for childPropName in nodeConfig['childProperties']: |
||
374 | for childPropSettingName, childPropSettingVal in nodeConfig[ |
||
375 | 'childProperties'][childPropName].iteritems(): |
||
376 | if childPropSettingName == "hidden" and childPropSettingVal is True: |
||
377 | hiddenProps.append(childPropName) |
||
378 | # Create Tikz snippet for tree node, we start with the TiKZ node for the graph icon |
||
379 | # Y coordinates are stretched a little bit, for optics |
||
380 | result = "\\node [shape=%s, %s] at (%u, -%f) (%u) {};\n" % ( |
||
381 | self.kind, nodeStyle, self.x + x_offset, (self.y + y_offset) * 1.2, self.pk) |
||
382 | # Determine the mirror text based on all properties |
||
383 | # Text width is exactly the double width of the icons |
||
384 | mirrorText = unicode() |
||
385 | for index, propvalue in enumerate( |
||
386 | self.get_all_mirror_properties(hiddenProps)): |
||
387 | propvalue = propvalue.replace( |
||
388 | "#", |
||
389 | "\\#") # consider special LaTex character in mirror text |
||
390 | if index == 0: |
||
391 | # Make the first property bigger, since it is supposed to be |
||
392 | # the name |
||
393 | propvalue = "\\baselineskip=0.8\\baselineskip\\textbf{{\\footnotesize %s}}" % propvalue |
||
394 | else: |
||
395 | propvalue = "{\\it\\scriptsize %s}" % propvalue |
||
396 | propvalue = propvalue.decode('utf-8') |
||
397 | mirrorText += propvalue + "\\\\" |
||
398 | # Create child nodes and their edges |
||
399 | for edge in self.outgoing.filter(deleted=False): |
||
400 | # Add child node rendering |
||
401 | result += edge.target.to_tikz(x_offset, y_offset, self.kind) |
||
402 | # Add connector from this node to the added child, consider if |
||
403 | # dashed line is needed |
||
404 | if 'dashstyle' in notations.by_kind[self.graph.kind][ |
||
405 | 'nodes'][self.kind]['connector']: |
||
406 | result += "\path[fork edge, dashed] (%s.south) edge (%u.north);\n" % ( |
||
407 | self.pk, edge.target.pk) |
||
408 | else: |
||
409 | result += "\path[fork edge] (%s.south) edge (%u.north);\n" % ( |
||
410 | self.pk, edge.target.pk) |
||
411 | # Add the mirror text as separate text node, which makes formatting |
||
412 | # more precise |
||
413 | if mirrorText != "": |
||
414 | result += "\\node [mirrorStyle] at (%u.south) (text%u) {%s};\n" % ( |
||
415 | self.pk, self.pk, mirrorText) |
||
416 | return result |
||
417 | |||
418 | def load_xml(self, xml_node, parent=None, xmltype=None): |
||
419 | """ |
||
420 | Method load_xml |
||
421 | |||
422 | Deserialize this node and it's children from the given PyXB XML tree, given by its root node. |
||
423 | The (self) Node object is expected to already have a valid graph attribute. |
||
424 | """ |
||
425 | from .edge import Edge |
||
426 | |||
427 | # If the target XML type is not given, we take the graph type |
||
428 | if not xmltype: |
||
429 | xmltype = self.graph.kind |
||
430 | |||
431 | assert (xmltype in ["faulttree", "fuzztree"]) |
||
432 | |||
433 | # All XML nodes got a client id |
||
434 | self.client_id = xml_node.id |
||
435 | self.x = xml_node.x |
||
436 | self.y = xml_node.y |
||
437 | |||
438 | # Finding the right kind string is not possible by lookup, since the type(xml_node) result |
||
439 | # is different from the used type on generation (TopEvent vs. |
||
440 | # TopEvent_) |
||
441 | if xmltype == "faulttree": |
||
442 | classes = faulttree_classes |
||
443 | elif xmltype == "fuzztree": |
||
444 | classes = fuzztree_classes |
||
445 | |||
446 | self.kind = classes[type(xml_node)] |
||
447 | logger.debug("Adding %s node with id %s" % (self.kind, self.client_id)) |
||
448 | self.save() |
||
449 | |||
450 | # Add properties |
||
451 | if hasattr(xml_node, 'name') and self.allows_property('name'): |
||
452 | logger.debug("Setting name to " + xml_node.name) |
||
453 | self.set_attr('name', xml_node.name) |
||
454 | if hasattr(xml_node, 'optional') and self.allows_property('optional'): |
||
455 | logger.debug( |
||
456 | "Setting optionality flag to " + str(xml_node.optional)) |
||
457 | self.set_attr('optional', xml_node.optional) |
||
458 | # if hasattr(xml_node,'probability') and self.allows_property('probability'): |
||
459 | # TODO: Consider also fuzzy probabilities and rates |
||
460 | # self.set_attr('probability', [0, xml_node.probability.value_]) |
||
461 | |||
462 | # Create edge to parent, if needed |
||
463 | if parent: |
||
464 | e = Edge( |
||
465 | source=parent, |
||
466 | target=self, |
||
467 | graph=self.graph, |
||
468 | client_id=new_client_id()) |
||
469 | e.save() |
||
470 | |||
471 | # Dive into the children |
||
472 | for child in xml_node.children: |
||
473 | n = Node(graph=self.graph) |
||
474 | n.load_xml(child, self) |
||
475 | |||
476 | def to_xml_probability(self, probability): |
||
477 | """ |
||
478 | Returns an XML wrapper object for the probability value given in frontend encoding. |
||
479 | """ |
||
480 | logger.debug( |
||
481 | "Determining XML representation for probability " + |
||
482 | str(probability)) |
||
483 | # Probability is a 2-tuple, were the first value is a type indicator |
||
484 | # and the second the value |
||
485 | if probability[0] == 0: |
||
486 | # Crisp probability |
||
487 | if self.graph.kind == "faulttree": |
||
488 | point = probability[1] |
||
489 | return xml_faulttree.CrispProbability(value_=point) |
||
490 | elif self.graph.kind == "fuzztree": |
||
491 | point = probability[1][0] |
||
492 | alpha = probability[1][1] |
||
493 | return xml_fuzztree.TriangularFuzzyInterval( |
||
494 | a=point - alpha, b1=point, b2=point, c=point + alpha) |
||
495 | else: |
||
496 | raise ValueError( |
||
497 | 'Cannot handle crisp probability value for this graph type') |
||
498 | elif probability[0] == 1: |
||
499 | # Failure rate |
||
500 | if self.graph.kind == "faulttree": |
||
501 | return xml_faulttree.FailureRate(value_=probability[1]) |
||
502 | elif self.graph.kind == "fuzztree": |
||
503 | return xml_fuzztree.FailureRate(value_=probability[1]) |
||
504 | else: |
||
505 | raise ValueError( |
||
506 | 'Cannot handle failure rate value for this graph type') |
||
507 | elif probability[0] == 2: |
||
508 | # Fuzzy probability |
||
509 | point = probability[1][0] |
||
510 | alpha = probability[1][1] |
||
511 | if self.graph.kind == "fuzztree": |
||
512 | return xml_fuzztree.TriangularFuzzyInterval( |
||
513 | a=point - alpha, b1=point, b2=point, c=point + alpha) |
||
514 | else: |
||
515 | raise ValueError( |
||
516 | 'Cannot handle fuzzy probability value for this graph type') |
||
517 | else: |
||
518 | raise ValueError( |
||
519 | 'Cannot handle probability value: "%s"' % |
||
520 | probability) |
||
521 | |||
522 | def to_xml(self, xmltype=None): |
||
523 | """ |
||
524 | Method: to_xml |
||
525 | |||
526 | Serializes this node into an PyXB XML tree. Please note |
||
527 | the backend node ID is used instead of client_id, since the latter one is not globally unique and may be too |
||
528 | long for some XML processors. |
||
529 | |||
530 | Returns: |
||
531 | The XML node instance for this graph node and its children |
||
532 | """ |
||
533 | |||
534 | # If the target XML type is not given, we take the graph type |
||
535 | if not xmltype: |
||
536 | xmltype = self.graph.kind |
||
537 | |||
538 | from .node_group import NodeGroup |
||
539 | group = NodeGroup.objects.filter( |
||
540 | deleted=False, |
||
541 | graph=self.graph, |
||
542 | nodes=self) |
||
543 | if len(group) == 0: |
||
544 | # This node is not in a node group |
||
545 | prop_src = self |
||
546 | else: |
||
547 | logger.debug( |
||
548 | "Considering node group properties instead of node properties") |
||
549 | # In theory, the node can be in multiple groups |
||
550 | # In (fault / fuzz) tree practice, it is only in one |
||
551 | prop_src = group[0] |
||
552 | |||
553 | properties = { |
||
554 | 'id': self.client_id, |
||
555 | 'name': prop_src.get_property('name', '-'), |
||
556 | 'x': self.x, |
||
557 | 'y': self.y |
||
558 | } |
||
559 | |||
560 | if self.kind == 'transferIn': |
||
561 | properties['fromModelId'] = prop_src.get_property('transfer') |
||
562 | |||
563 | # for any node that may have a quantity, set the according property |
||
564 | if self.kind in {'basicEventSet', 'intermediateEventSet'}: |
||
565 | properties['quantity'] = prop_src.get_property('cardinality') |
||
566 | |||
567 | if self.kind == 'topEvent': |
||
568 | properties['missionTime'] = prop_src.get_property('missionTime') |
||
569 | properties['decompositionNumber'] = prop_src.get_property( |
||
570 | 'decompositions') |
||
571 | |||
572 | # Special treatment for some of the FuzzTree node types |
||
573 | if xmltype == 'fuzztree': |
||
574 | # for any node that may be optional, set the according property |
||
575 | if self.kind in {'basicEvent', 'basicEventSet', |
||
576 | 'intermediateEvent', 'intermediateEventSet', 'houseEvent'}: |
||
577 | properties['optional'] = prop_src.get_property( |
||
578 | 'optional', |
||
579 | False) |
||
580 | |||
581 | # determine fuzzy or crisp probability, set it accordingly |
||
582 | if self.kind in {'basicEvent', 'basicEventSet', 'houseEvent'}: |
||
583 | properties['probability'] = self.to_xml_probability( |
||
584 | prop_src.get_property( |
||
585 | 'probability', |
||
586 | False)) |
||
587 | # nodes that have a probability also have costs in FuzzTrees |
||
588 | properties['costs'] = prop_src.get_property('cost', 0) |
||
589 | |||
590 | # Voting OR in FuzzTrees has different parameter name than in fault |
||
591 | # trees |
||
592 | elif self.kind == 'votingOrGate': |
||
593 | properties['k'] = prop_src.get_property('k') |
||
594 | |||
595 | # add range attribute for redundancy variation |
||
596 | elif self.kind == 'redundancyVariation': |
||
597 | nRange = prop_src.get_property('nRange') |
||
598 | properties['start'] = nRange[0] |
||
599 | properties['end'] = nRange[1] |
||
600 | properties['formula'] = prop_src.get_property('kFormula') |
||
601 | |||
602 | xml_node = fuzztree_classes[self.kind](**properties) |
||
603 | |||
604 | # Special treatment for some of the FaultTree node types |
||
605 | elif xmltype == 'faulttree': |
||
606 | if self.kind == 'votingOrGate': |
||
607 | properties['k'] = prop_src.get_property('k') |
||
608 | |||
609 | # determine fuzzy or crisp probability, set it accordingly |
||
610 | if self.kind in {'basicEvent', 'basicEventSet', 'houseEvent'}: |
||
611 | properties['probability'] = self.to_xml_probability( |
||
612 | prop_src.get_property('probability')) |
||
613 | |||
614 | if self.kind == 'fdepGate': |
||
615 | properties['triggeredEvents'] = [ |
||
616 | parent.client_id for parent in self.parents()] |
||
617 | children = self.children() |
||
618 | # Frontend restriction, comes from notations.json |
||
619 | assert(len(children) == 1) |
||
620 | properties['trigger'] = children[0].client_id |
||
621 | |||
622 | if self.kind == 'spareGate': |
||
623 | children_sorted = self.children_left2right() |
||
624 | # TODO: This will kill the XML generation if the graph is |
||
625 | # incompletly drawn. Do we want that? |
||
626 | assert(len(children_sorted) > 0) |
||
627 | properties['primaryID'] = children_sorted[0].client_id |
||
628 | properties['dormancyFactor'] = prop_src.get_property( |
||
629 | 'dormancyFactor') |
||
630 | |||
631 | if self.kind in ['seqGate', 'priorityAndGate']: |
||
632 | properties['eventSequence'] = [ |
||
633 | child.client_id for child in self.children_left2right()] |
||
634 | |||
635 | xml_node = faulttree_classes[self.kind](**properties) |
||
636 | |||
637 | # serialize children |
||
638 | logger.debug( |
||
639 | 'Added node "%s" with properties %s' % |
||
640 | (self.kind, properties)) |
||
641 | for edge in self.outgoing.filter(deleted=False): |
||
642 | xml_node.children.append(edge.target.to_xml(xmltype)) |
||
643 | |||
644 | return xml_node |
||
645 | |||
646 | def allows_property(self, name): |
||
647 | ''' This method allows to check if the given property is supported for this node. |
||
648 | The basic idea here is that property.js has some understanding of what is expected for |
||
649 | particular node types in the JSON. If we do not follow this understanding, they get |
||
650 | very 'exceptional'. The JSON renderer is not checking this, but the XML import must be picky. |
||
651 | ''' |
||
652 | propdetails = notations.by_kind[ |
||
653 | self.graph.kind]['nodes'][ |
||
654 | self.kind]['properties'] |
||
655 | if name not in propdetails.keys(): |
||
656 | logger.debug( |
||
657 | '%s is not allowed in %s' % |
||
658 | (str(name), str( |
||
659 | self.kind))) |
||
660 | return False |
||
661 | else: |
||
662 | return True |
||
663 | |||
664 | View Code Duplication | def get_property(self, key, default=None): |
|
665 | try: |
||
666 | return self.properties.get(key=key).get_value() |
||
667 | except ObjectDoesNotExist: |
||
668 | try: |
||
669 | prop = notations.by_kind[ |
||
670 | self.graph.kind]['nodes'][ |
||
671 | self.kind]['properties'][key] |
||
672 | if prop is None: |
||
673 | logger.warning( |
||
674 | 'Notation configuration has empty default for node property ' + |
||
675 | key) |
||
676 | result = default |
||
677 | else: |
||
678 | result = prop['default'] |
||
679 | logger.debug( |
||
680 | 'Node has no property "%s", using default "%s"' % |
||
681 | (key, str(result))) |
||
682 | return result |
||
683 | except KeyError: |
||
684 | logger.debug( |
||
685 | 'No default given in notation, using given default "%s" instead' % |
||
686 | default) |
||
687 | return default |
||
688 | except MultipleObjectsReturned: |
||
689 | logger.error( |
||
690 | "ERROR: Property %s in node %u exists in multiple instances" % |
||
691 | (key, self.pk)) |
||
692 | raise MultipleObjectsReturned() |
||
693 | |||
694 | View Code Duplication | def set_attr(self, key, value): |
|
695 | """ |
||
696 | Method: set_attr |
||
697 | |||
698 | Use this method to set a node's attribute. It looks in the node object and its related properties for an |
||
699 | attribute with the given name and changes it. If non exist, a new property is added saving this attribute. |
||
700 | |||
701 | Parameters: |
||
702 | {string} key - The name of the attribute. |
||
703 | {attr} value - The new value that should be stored. |
||
704 | |||
705 | TODO: Deprecate this method, set_attrs() should only be used to have an efficient modification signal handling. |
||
706 | """ |
||
707 | # Catch attribute setting before object saving cases |
||
708 | assert(self.pk) |
||
709 | if hasattr(self, key): |
||
710 | # Native node attribute, such as X or Y |
||
711 | setattr(self, key, value) |
||
712 | self.save() |
||
713 | else: |
||
714 | # Node property |
||
715 | prop, created = self.properties.get_or_create( |
||
716 | key=key, defaults={ |
||
717 | 'node': self}) |
||
718 | prop.save_value(value) |
||
719 | |||
720 | def set_attrs(self, d): |
||
721 | ''' |
||
722 | Set node attributes according to the provided dictionary. |
||
723 | |||
724 | TODO: Replace by true bulk insert implementation. |
||
725 | ''' |
||
726 | for key, value in d.iteritems(): |
||
727 | self.set_attr(key, value) |
||
728 | post_save.send(sender=self.__class__, instance=self) |
||
729 | |||
730 | def same_as(self, node): |
||
731 | ''' |
||
732 | Checks if this node is equal to the given one in terms of properties. |
||
733 | This is a very expensive operation that is only intended for testing purposes. |
||
734 | ''' |
||
735 | logger.debug(self.to_dict()) |
||
736 | logger.debug(node.to_dict()) |
||
737 | if self.kind != node.kind or self.x != node.x or self.y != node.y: |
||
738 | return False |
||
739 | for my_property in self.properties.all().filter(deleted=False): |
||
740 | found_match = False |
||
741 | for their_property in node.properties.all().filter(deleted=False): |
||
742 | if my_property.same_as(their_property): |
||
743 | found_match = True |
||
744 | break |
||
745 | if not found_match: |
||
746 | return False |
||
747 | return True |
||
748 | |||
749 | |||
750 | @receiver(post_save, sender=Node) |
||
751 | @receiver(pre_delete, sender=Node) |
||
752 | def graph_modify(sender, instance, **kwargs): |
||
753 | instance.graph.modified = datetime.datetime.now() |
||
754 | instance.graph.save() |
||
755 | # updating project modification date |
||
756 | instance.graph.project.modified = instance.graph.modified |
||
757 | instance.graph.project.save() |
||
758 |