Passed
Pull Request — 2.x (#1918)
by Ramon
05:22
created

senaite.core.browser.widgets.referencewidget   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 273
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 40
eloc 170
dl 0
loc 273
rs 9.2
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
C ReferenceWidget.process_form() 0 18 9
A ReferenceWidget.get_combogrid_options() 0 21 2
A ajaxReferenceWidgetSearch.to_data_rows() 0 5 2
A ajaxReferenceWidgetSearch.get_data_record() 0 23 4
A ajaxReferenceWidgetSearch.get_field_names() 0 18 5
A ajaxReferenceWidgetSearch.__call__() 0 11 1
B ReferenceWidget.get_base_query() 0 23 7
A ReferenceWidget.get_search_url() 0 11 1
A ajaxReferenceWidgetSearch.search() 0 9 2
A ReferenceWidget.initial_uid_field_value() 0 8 4
A ajaxReferenceWidgetSearch.to_json_payload() 0 16 1
A ajaxReferenceWidgetSearch.num_page() 0 5 1
A ajaxReferenceWidgetSearch.num_rows_page() 0 5 1

How to fix   Complexity   

Complexity

Complex classes like senaite.core.browser.widgets.referencewidget 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-2021 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
import json
22
import six
23
24
from AccessControl import ClassSecurityInfo
25
from bika.lims import api
26
from bika.lims import bikaMessageFactory as _
27
from bika.lims import logger
28
from bika.lims.browser import BrowserView
29
from bika.lims.interfaces import IReferenceWidgetVocabulary
30
from bika.lims.utils import to_unicode as _u
31
from plone import protect
32
from Products.Archetypes.Registry import registerWidget
33
from Products.Archetypes.Widget import StringWidget
34
from senaite.app.supermodel.model import SuperModel
35
from zope.component import getAdapters
36
37
38
class ReferenceWidget(StringWidget):
39
    _properties = StringWidget._properties.copy()
40
    _properties.update({
41
        "macro": "senaite_widgets/referencewidget",
42
        "helper_js": ("senaite_widgets/referencewidget.js",),
43
        "helper_css": ("senaite_widgets/referencewidget.css",),
44
45
        "url": "referencewidget_search",
46
        "catalog_name": "portal_catalog",
47
48
        # base_query can be a dict or a callable returning a dict
49
        "base_query": {},
50
51
        # This will be faster if the columnNames are catalog indexes
52
        "colModel": [
53
            {"columnName": "Title", "width": "30", "label": _(
54
                "Title"), "align": "left"},
55
            {"columnName": "Description", "width": "70", "label": _(
56
                "Description"), "align": "left"},
57
            # UID is required in colModel
58
            {"columnName": "UID", "hidden": True},
59
        ],
60
61
        # Default field to put back into input elements
62
        "ui_item": "Title",
63
        "search_fields": ("Title",),
64
        "discard_empty": [],
65
        "popup_width": "550px",
66
        "showOn": False,
67
        "searchIcon": True,
68
        "minLength": "0",
69
        "delay": "500",
70
        "resetButton": False,
71
        "sord": "asc",
72
        "sidx": "Title",
73
        "force_all": False,
74
        "portal_types": {},
75
    })
76
    security = ClassSecurityInfo()
77
78
    security.declarePublic('process_form')
79
80
    def process_form(self, instance, field, form, empty_marker=None,
81
                     emptyReturnsMarker=False):
82
        """Return a UID so that ReferenceField understands.
83
        """
84
        fieldName = field.getName()
85
        if fieldName + "_uid" in form:
86
            uid = form.get(fieldName + "_uid", "")
87
            if field.multiValued and\
88
                    (isinstance(uid, str) or isinstance(uid, unicode)):
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable unicode does not seem to be defined.
Loading history...
89
                uid = uid.split(",")
90
        elif fieldName in form:
91
            uid = form.get(fieldName, "")
92
            if field.multiValued and\
93
                    (isinstance(uid, str) or isinstance(uid, unicode)):
94
                uid = uid.split(",")
95
        else:
96
            uid = None
97
        return uid, {}
98
99
    def get_search_url(self, context):
100
        """Prepare an absolute search url for the combobox
101
        """
102
        # ensure we have an absolute url for the current context
103
        url = api.get_url(context)
104
        # normalize portal factory urls
105
        url = url.split("portal_factory")[0]
106
        # ensure the search path does not contain already the url
107
        search_path = self.url.split(url)[-1]
108
        # return the absolute search url
109
        return "/".join([url, search_path])
110
111
    def get_combogrid_options(self, context, fieldName):
112
        colModel = self.colModel
113
        if "UID" not in [x["columnName"] for x in colModel]:
114
            colModel.append({"columnName": "UID", "hidden": True})
115
116
        options = {
117
            "url": self.get_search_url(context),
118
            "colModel": colModel,
119
            "showOn": self.showOn,
120
            "width": self.popup_width,
121
            "sord": self.sord,
122
            "sidx": self.sidx,
123
            "force_all": self.force_all,
124
            "search_fields": self.search_fields,
125
            "discard_empty": self.discard_empty,
126
            "minLength": self.minLength,
127
            "resetButton": self.resetButton,
128
            "searchIcon": self.searchIcon,
129
            "delay": self.delay,
130
        }
131
        return json.dumps(options)
132
133
    def get_base_query(self, context, fieldName):
134
        base_query = self.base_query
135
        if callable(base_query):
136
            try:
137
                base_query = base_query(context, self, fieldName)
138
            except TypeError:
139
                base_query = base_query()
140
        if base_query and isinstance(base_query, six.string_types):
141
            base_query = json.loads(base_query)
142
143
        # portal_type: use field allowed types
144
        field = context.Schema().getField(fieldName)
145
        allowed_types = getattr(field, "allowed_types", None)
146
        allowed_types_method = getattr(field, "allowed_types_method", None)
147
        if allowed_types_method:
148
            meth = getattr(context, allowed_types_method)
149
            allowed_types = meth(field)
150
        # If field has no allowed_types defined, use widget"s portal_type prop
151
        base_query["portal_type"] = allowed_types \
152
            if allowed_types \
153
            else self.portal_types
154
155
        return json.dumps(base_query)
156
157
    def initial_uid_field_value(self, value):
158
        if type(value) in (list, tuple):
159
            ret = ",".join([v.UID() for v in value])
160
        elif type(value) in [str, ]:
161
            ret = value
162
        else:
163
            ret = value.UID() if value else value
164
        return ret
165
166
167
registerWidget(ReferenceWidget, title="Reference Widget")
168
169
170
class ajaxReferenceWidgetSearch(BrowserView):
171
    """ Source for jquery combo dropdown box
172
    """
173
174
    @property
175
    def num_page(self):
176
        """Returns the number of page to render
177
        """
178
        return api.to_int(self.request.get("page", None), default=1)
179
180
    @property
181
    def num_rows_page(self):
182
        """Returns the number of rows per page to render
183
        """
184
        return api.to_int(self.request.get("rows", None), default=10)
185
186
    def get_field_names(self):
187
        """Return the field names to get values for
188
        """
189
        col_model = self.request.get("colModel", None)
190
        if not col_model:
191
            return ["UID",]
192
193
        names = []
194
        col_model = json.loads(_u(col_model))
195
        if isinstance(col_model, (list, tuple)):
196
            names = map(lambda c: c.get("columnName", "").strip(), col_model)
197
198
        # UID is used by reference widget to know the object that the user
199
        # selected from the popup list
200
        if "UID" not in names:
201
            names.append("UID")
202
203
        return filter(None, names)
204
205
    def get_data_record(self, brain, field_names):
206
        """Returns a dict with the column values for the given brain
207
        """
208
        record = {}
209
        model = None
210
211
        for field_name in field_names:
212
            # First try to get the value directly from the brain
213
            value = getattr(brain, field_name, None)
214
215
            # No metadata for this column name
216
            if value is None:
217
                logger.warn("Not a metadata field: {}".format(field_name))
218
                model = model or SuperModel(brain)
219
                value = model.get(field_name, None)
220
                if callable(value):
221
                    value = value()
222
223
            # ' ' instead of '' because empty div fields don't render
224
            # correctly in combo results table
225
            record[field_name] = value or " "
226
227
        return record
228
229
    def search(self):
230
        """Returns the list of brains that match with the request criteria
231
        """
232
        brains = []
233
        # TODO Legacy
234
        for name, adapter in getAdapters((self.context, self.request),
235
                                         IReferenceWidgetVocabulary):
236
            brains.extend(adapter())
237
        return brains
238
239
    def to_data_rows(self, brains):
240
        """Returns a list of dictionaries representing the values of each brain
241
        """
242
        fields = self.get_field_names()
243
        return map(lambda brain: self.get_data_record(brain, fields), brains)
244
245
    def to_json_payload(self, data_rows):
246
        """Returns the json payload
247
        """
248
        num_rows = len(data_rows)
249
        num_page = self.num_page
250
        num_rows_page = self.num_rows_page
251
252
        pages = num_rows / num_rows_page
253
        pages += divmod(num_rows, num_rows_page)[1] and 1 or 0
254
        start = (num_page - 1) * num_rows_page
255
        end = num_page * num_rows_page
256
        payload = {"page": num_page,
257
                   "total": pages,
258
                   "records": num_rows,
259
                   "rows": data_rows[start:end]}
260
        return json.dumps(payload)
261
262
    def __call__(self):
263
        protect.CheckAuthenticator(self.request)
264
265
        # Do the search
266
        brains = self.search()
267
268
        # Generate the data rows to display
269
        data_rows = self.to_data_rows(brains)
270
271
        # Return the payload
272
        return self.to_json_payload(data_rows)
273