ore.models.node.Node.to_xml()   F
last analyzed

Complexity

Conditions 18

Size

Total Lines 123
Code Lines 74

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 74
dl 0
loc 123
rs 1.2
c 0
b 0
f 0
cc 18
nop 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like ore.models.node.Node.to_xml() 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)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable unicode does not seem to be defined.
Loading history...
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()
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable unicode does not seem to be defined.
Loading history...
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)]
0 ignored issues
show
introduced by
The variable classes does not seem to be defined for all execution paths.
Loading history...
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))
0 ignored issues
show
introduced by
The variable xml_node does not seem to be defined for all execution paths.
Loading history...
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):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
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):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
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