ore.models.properties.Property.get_value()   C
last analyzed

Complexity

Conditions 11

Size

Total Lines 26
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 19
dl 0
loc 26
rs 5.4
c 0
b 0
f 0
cc 11
nop 2

How to fix   Complexity   

Complexity

Complex classes like ore.models.properties.Property.get_value() 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
import logging
2
import json
3
4
from django.db import models
5
6
from .node import Node
7
from .edge import Edge
8
from .node_group import NodeGroup
9
from .notations import by_kind
10
11
logger = logging.getLogger('ore')
12
13
14
class Property(models.Model):
15
16
    """
17
    Class: Property
18
19
    This class models generic properties (i.e. attributes) of the nodes of any diagram notation. They are basically
20
    key-value tuple that allow the dynamic addition or deletion of whole sets of new property names without having to
21
    alter the schema of nodes.
22
23
    Fields:
24
     {str}    key      - the name of the property
25
     {json}   value    - the value of the property
26
     {<Node>} node     - link to the node that owns the property
27
     {bool}   deleted  - flag indicating whether this property is deleted or not. Simplifies the restoration of this
28
                        property by just having to toggle this flag (default: False)
29
    """
30
    class Meta:
31
        app_label = 'ore'
32
33
    key = models.CharField(max_length=255)
34
    value = models.TextField()
35
    node = models.ForeignKey(
36
        Node,
37
        related_name='properties',
38
        blank=True,
39
        null=True,
40
        default=None)
41
    edge = models.ForeignKey(
42
        Edge,
43
        related_name='properties',
44
        blank=True,
45
        null=True,
46
        default=None)
47
    node_group = models.ForeignKey(
48
        NodeGroup,
49
        related_name='properties',
50
        blank=True,
51
        null=True,
52
        default=None)
53
    deleted = models.BooleanField(default=False)
54
55
    def __unicode__(self):
56
        return '%s%s: %s' % (
57
            '[DELETED] ' if self.deleted else '', self.key, self.value)
58
59
    @property
60
    def graph(self):
61
        '''
62
            Return the graph this property (indirectly) belongs to.
63
        '''
64
        if self.node:
65
            return self.node.graph
66
        elif self.edge:
67
            return self.edge.graph
68
        elif self.node_group:
69
            return self.node_group.graph
70
        assert(False)
71
72
    @classmethod
73
    def value_type(cls, key, obj):
74
        '''
75
            Return the expected value data type of the property 'key' in
76
            an node / edge / node group object. Data types are as used
77
            in the notations files.
78
79
            There is no fallback here, so this function will crash if some utilized
80
            property name is not covered in the notations file. This is good at test suite runs,
81
            and leads to a 500 in production if somebody tries to inject arbitrary property keys
82
            or misformated property values through the API.
83
        '''
84
        # Check for property keys that are not part of the notations file
85
        # TODO: Make this an issue for the JS guys, so that the notations
86
        #       file really has all possible properties defined
87
        if key in ['x', 'y', 'key']:
88
            return 'numeric'
89
        try:
90
            if isinstance(obj, Node):
91
                return by_kind[obj.graph.kind][
92
                    'nodes'][obj.kind]['properties'][key]['kind']
93
            elif isinstance(obj, Edge):
94
                return by_kind[obj.graph.kind][
95
                    'edges']['properties'][key]['kind']
96
            elif isinstance(obj, NodeGroup):
97
                return by_kind[obj.graph.kind][
98
                    'nodeGroups']['properties'][key]['kind']
99
        except Exception as e:
100
            text = "Invalid property key '{0}' being used for {1}, exception thrown: {2}".format(key, obj, e)
101
            logger.error(text)
102
            raise Exception(text)
103
104
    def object(self):
105
        if self.node:
106
            return self.node
107
        elif self.edge:
108
            return self.edge
109
        elif self.node_group:
110
            return self.node_group
111
        assert(False)
112
113
    def get_value(self, from_text=None):
114
        '''
115
            Returns the current property value, or the given text value, in native representation.
116
        '''
117
        if from_text:
118
            val = from_text
119
        else:
120
            val = self.value
121
        val_type = Property.value_type(self.key, self.object())
122
        if val_type == 'text' or val_type == 'textfield':
123
            return unicode(val)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable unicode does not seem to be defined.
Loading history...
124
        elif val_type == 'compound' or val_type == 'range':
125
            if val.startswith('"') and val.endswith('"'):
126
                # Illformed legacy data stored in older versions
127
                # We prefer to trust the notation type specification
128
                logger.warning("Illformed value in property %u" % self.pk)
129
                val = val[1:-1]
130
            return json.loads(val)
131
        elif val_type == 'numeric':
132
            return int(val)
133
        elif val_type == 'bool':
134
            # Value may be string or a real boolean value
135
            return str(val).lower() == 'true'
136
        elif val_type == 'transfer':
137
            return int(val)
138
        assert(False)
139
140
    def save_value(self, new_value):
141
        '''
142
            Saves the given value, converts from native representation before.
143
        '''
144
        val_type = Property.value_type(self.key, self.object())
145
        if val_type == 'text' or val_type == 'textfield':
146
            self.value = unicode(new_value)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable unicode does not seem to be defined.
Loading history...
147
        elif val_type == 'compound' or val_type == 'range':
148
            if isinstance(new_value, basestring):
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable basestring does not seem to be defined.
Loading history...
149
                # Somebody, preferably some importer, forgot to interpret
150
                # its JSON data and just passed the raw string
151
                # Since this is the only position in the code where property
152
                # types are checked anyway, we try to be gentle here.
153
                self.value = new_value
154
            else:
155
                self.value = json.dumps(new_value)
156
        elif val_type == 'numeric':
157
            self.value = str(new_value)
158
        elif val_type == 'bool':
159
            self.value = str(new_value)
160
        elif val_type == 'transfer':
161
            self.value = str(new_value)
162
        else:
163
            raise Exception("Unknown property value type '%s' being used for %s" % (val_type, str(self)))
164
        self.save()
165
166
    def same_as(self, prop):
167
        '''
168
            Checks if this property is equal to the given one.
169
        '''
170
        if self.key != prop.key:
171
            return False
172
        same_val = (self.get_value() == prop.get_value())
173
        if not same_val:
174
            return False
175
        return True
176