Passed
Push — master ( 2280ab...e4f397 )
by Ramon
07:58 queued 03:51
created

bika.lims.jsonapi   F

Complexity

Total Complexity 62

Size/Duplication

Total Lines 246
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 62
eloc 170
dl 0
loc 246
rs 3.44
c 0
b 0
f 0

8 Functions

Rating   Name   Duplication   Size   Complexity  
F load_field_values() 0 47 15
B load_brain_metadata() 0 17 7
A get_include_fields() 0 12 3
A handle_errors() 0 14 2
A load_method_values() 0 7 3
A get_include_methods() 0 12 4
D resolve_request_lookup() 0 37 12
F set_fields_from_request() 0 54 16

How to fix   Complexity   

Complexity

Complex classes like bika.lims.jsonapi 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
# -*- coding: utf-8 -*-
2
#
3
# This file is part of SENAITE.CORE.
4
#
5
# SENAITE.CORE is free software: you can redistribute it and/or modify it under
6
# the terms of the GNU General Public License as published by the Free Software
7
# Foundation, version 2.
8
#
9
# This program is distributed in the hope that it will be useful, but WITHOUT
10
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12
# details.
13
#
14
# You should have received a copy of the GNU General Public License along with
15
# this program; if not, write to the Free Software Foundation, Inc., 51
16
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
#
18
# Copyright 2018-2019 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
from Products.Archetypes.config import TOOL_NAME
22
from Products.CMFCore.utils import getToolByName
23
from bika.lims.utils import to_utf8
24
from bika.lims import logger
25
26
import json
27
import Missing
28
import six
29
import sys, traceback
30
31
32
def handle_errors(f):
33
    """ simple JSON error handler
34
    """
35
    import traceback
36
    from plone.jsonapi.core.helpers import error
37
38
    def decorator(*args, **kwargs):
39
        try:
40
            return f(*args, **kwargs)
41
        except Exception:
42
            var = traceback.format_exc()
43
            return error(var)
44
45
    return decorator
46
47
48
def get_include_fields(request):
49
    """Retrieve include_fields values from the request
50
    """
51
    include_fields = []
52
    rif = request.get("include_fields", "")
53
    if "include_fields" in request:
54
        include_fields = [x.strip()
55
                          for x in rif.split(",")
56
                          if x.strip()]
57
    if "include_fields[]" in request:
58
        include_fields = request['include_fields[]']
59
    return include_fields
60
61
62
def load_brain_metadata(proxy, include_fields):
63
    """Load values from the catalog metadata into a list of dictionaries
64
    """
65
    ret = {}
66
    for index in proxy.indexes():
67
        if index not in proxy:
68
            continue
69
        if include_fields and index not in include_fields:
70
            continue
71
        val = getattr(proxy, index)
72
        if val != Missing.Value:
73
            try:
74
                json.dumps(val)
75
            except:
76
                continue
77
            ret[index] = val
78
    return ret
79
80
81
def load_field_values(instance, include_fields):
82
    """Load values from an AT object schema fields into a list of dictionaries
83
    """
84
    ret = {}
85
    schema = instance.Schema()
86
    val = None
87
    for field in schema.fields():
88
        fieldname = field.getName()
89
        if include_fields and fieldname not in include_fields:
90
            continue
91
        try:
92
            val = field.get(instance)
93
        except AttributeError:
94
            # If this error is raised, make a look to the add-on content
95
            # expressions used to obtain their data.
96
            print "AttributeError:", sys.exc_info()[1]
97
            print "Unreachable object. Maybe the object comes from an Add-on"
98
            print traceback.format_exc()
99
100
        if val:
101
            field_type = field.type
102
            # If it a proxy field, we should know to the type of the proxied
103
            # field
104
            if field_type == 'proxy':
105
                actual_field = field.get_proxy(instance)
106
                field_type = actual_field.type
107
            if field_type == "blob" or field_type == 'file':
108
                continue
109
            # I put the UID of all references here in *_uid.
110
            if field_type == 'reference':
111
                if type(val) in (list, tuple):
112
                    ret[fieldname + "_uid"] = [v.UID() for v in val]
113
                    val = [to_utf8(v.Title()) for v in val]
114
                else:
115
                    ret[fieldname + "_uid"] = val.UID()
116
                    val = to_utf8(val.Title())
117
            elif field_type == 'boolean':
118
                val = True if val else False
119
            elif field_type == 'text':
120
                val = to_utf8(val)
121
122
        try:
123
            json.dumps(val)
124
        except:
125
            val = str(val)
126
        ret[fieldname] = val
127
    return ret
128
129
130
def get_include_methods(request):
131
    """Retrieve include_methods values from the request
132
    """
133
    include_methods = request.get("include_methods[]")
134
    if not include_methods:
135
        include_methods = request.get("include_methods", [])
136
137
    if isinstance(include_methods, six.string_types):
138
        include_methods = include_methods.split(",")
139
        include_methods = map(lambda me: me.strip(), include_methods)
140
141
    return filter(None, include_methods)
142
143
144
def load_method_values(instance, include_methods):
145
    ret = {}
146
    for method in include_methods:
147
        if hasattr(instance, method):
148
            val = getattr(instance, method)()
149
            ret[method] = val
150
    return ret
151
152
153
def resolve_request_lookup(context, request, fieldname):
154
    if fieldname not in request:
155
        return []
156
    brains = []
157
    at = getToolByName(context, TOOL_NAME, None)
158
    entries = request[fieldname] if type(request[fieldname]) in (list, tuple) \
159
        else [request[fieldname], ]
160
    for entry in entries:
161
        contentFilter = {}
162
        for value in entry.split("|"):
163
            if ":" in value:
164
                index, value = value.split(":", 1)
165
            else:
166
                index, value = 'id', value
167
            if index in contentFilter:
168
                v = contentFilter[index]
169
                v = v if type(v) in (list, tuple) else [v, ]
170
                v.append(value)
171
                contentFilter[index] = v
172
            else:
173
                contentFilter[index] = value
174
        # search all possible catalogs
175
        if 'portal_type' in contentFilter:
176
            catalogs = at.getCatalogsByType(contentFilter['portal_type'])
177
        elif 'uid' in contentFilter:
178
            # If a uid is found, the object could be searched for its own uid
179
            uc = getToolByName(context, 'uid_catalog')
180
            return uc(UID=contentFilter['uid'])
181
182
        else:
183
            catalogs = [getToolByName(context, 'portal_catalog'), ]
184
        for catalog in catalogs:
185
            _brains = catalog(contentFilter)
186
            if _brains:
187
                brains.extend(_brains)
188
                break
189
    return brains
190
191
192
def set_fields_from_request(obj, request):
193
    """Search request for keys that match field names in obj,
194
    and call field mutator with request value.
195
196
    The list of fields for which schema mutators were found
197
    is returned.
198
    """
199
    schema = obj.Schema()
200
    # fields contains all schema-valid field values from the request.
201
    fields = {}
202
    for fieldname, value in request.items():
203
        if fieldname not in schema:
204
            continue
205
        field = schema[fieldname]
206
        widget = field.getWidgetName()
207
        if widget in ["ReferenceWidget"]:
208
            brains = []
209
            if value:
210
                brains = resolve_request_lookup(obj, request, fieldname)
211
                if not brains:
212
                    logger.warning(
213
                        "JSONAPI: Can't resolve reference: {} {}"
214
                        .format(fieldname, value))
215
                    return []
216
            if schema[fieldname].multiValued:
217
                value = [b.UID for b in brains] if brains else []
218
            else:
219
                value = brains[0].UID if brains else None
220
        fields[fieldname] = value
221
    # Write fields.
222
    for fieldname, value in fields.items():
223
        field = schema[fieldname]
224
        fieldtype = field.getType()
225
        if fieldtype == 'Products.Archetypes.Field.BooleanField':
226
            if value.lower() in ('0', 'false', 'no') or not value:
227
                value = False
228
            else:
229
                value = True
230
        elif fieldtype in ['Products.ATExtensions.field.records.RecordsField',
231
                           'Products.ATExtensions.field.records.RecordField']:
232
            try:
233
                value = eval(value)
234
            except:
235
                logger.warning(
236
                    "JSONAPI: " + fieldname + ": Invalid "
237
                    "JSON/Python variable")
238
                return []
239
        mutator = field.getMutator(obj)
240
        if mutator:
241
            mutator(value)
242
        else:
243
            field.set(obj, value)
244
    obj.reindexObject()
245
    return fields.keys()
246