Passed
Push — 2.x ( e89acb...622fea )
by Jordi
06:34
created

senaite.core.browser.widgets.referencewidget   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 318
Duplicated Lines 14.15 %

Importance

Changes 0
Metric Value
wmc 43
eloc 170
dl 45
loc 318
rs 8.96
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A ReferenceWidget.get_search_index() 0 20 4
B ReferenceWidget.process_form() 0 21 6
A ReferenceWidget.get_query() 0 27 4
A ReferenceWidget.get_value() 0 17 4
A ReferenceWidget.get_catalog() 0 19 3
A ReferenceWidget.get_columns() 0 31 5
A ReferenceWidget.get_base_query() 0 15 4
A ReferenceWidget.get_multi_valued() 0 12 2
A ReferenceWidget.get_display_template() 0 20 3
B ReferenceWidget.get_render_data() 29 29 5
A ReferenceWidget.render_reference() 16 16 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

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:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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-2023 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
import re
22
import json
23
import string
24
25
import six
26
from bika.lims import api
27
from bika.lims import bikaMessageFactory as _
28
from bika.lims import logger
29
from Products.Archetypes.Registry import registerWidget
30
from senaite.core.browser.widgets.queryselect import QuerySelectWidget
31
32
DEFAULT_SEARCH_CATALOG = "uid_catalog"
33
DISPLAY_TEMPLATE = "<a href='${url}' _target='blank'>${Title}</a>"
34
IGNORE_COLUMNS = ["UID"]
35
36
37
class ReferenceWidget(QuerySelectWidget):
38
    """UID Reference Widget
39
    """
40
    # CSS class that is picked up by the ReactJS component
41
    klass = u"senaite-uidreference-widget-input"
42
43
    _properties = QuerySelectWidget._properties.copy()
44
    _properties.update({
45
46
        "value_key": "uid",
47
        "value_query_index": "UID",
48
49
        # BBB: OLD PROPERTIES
50
        "url": "referencewidget_search",
51
        "catalog_name": "portal_catalog",
52
        # base_query can be a dict or a callable returning a dict
53
        "base_query": {},
54
        # columns to display in the search dropdown
55
        "colModel": [
56
            {"columnName": "Title", "width": "30", "label": _(
57
                "Title"), "align": "left"},
58
            {"columnName": "Description", "width": "70", "label": _(
59
                "Description"), "align": "left"},
60
            # UID is required in colModel
61
            {"columnName": "UID", "hidden": True},
62
        ],
63
        "ui_item": "Title",
64
        "search_fields": ("Title",),
65
        "discard_empty": [],
66
        "popup_width": "550px",
67
        "showOn": False,
68
        "searchIcon": True,
69
        "minLength": "0",
70
        "delay": "500",
71
        "resetButton": False,
72
        "sord": "asc",
73
        "sidx": "Title",
74
        "force_all": False,
75
        "portal_types": {},
76
    })
77
78
    def process_form(self, instance, field, form, empty_marker=None,
79
                     emptyReturnsMarker=False, validating=True):
80
        """Convert the stored UIDs from the text field for the UID reference field
81
        """
82
        value = form.get(field.getName(), "")
83
84
        if api.is_string(value):
85
            uids = value.split("\r\n")
86
        elif isinstance(value, (list, tuple, set)):
87
            uids = filter(api.is_uid, value)
88
        elif api.is_object(value):
89
            uids = [api.get_uid(value)]
90
        else:
91
            uids = []
92
93
        # handle custom setters that expect only a UID, e.g. setSpecification
94
        multi_valued = getattr(field, "multiValued", self.multi_valued)
95
        if not multi_valued:
96
            uids = uids[0] if len(uids) > 0 else ""
97
98
        return uids, {}
99
100
    def get_multi_valued(self, context, field, default=None):
101
        """Lookup if the field is single or multi valued
102
103
        :param context: The current context of the field
104
        :param field: The current field of the widget
105
        :param default: The default property value
106
        :returns: True if the field is multi valued, otherwise False
107
        """
108
        multi_valued = getattr(field, "multiValued", None)
109
        if multi_valued is None:
110
            return default
111
        return multi_valued
112
113
    def get_display_template(self, context, field, default=None):
114
        """Lookup the display template
115
116
        :param context: The current context of the field
117
        :param field: The current field of the widget
118
        :param default: The default property value
119
        :returns: Template that is interpolated by the JS widget with the
120
                  mapped values found in records
121
        """
122
        # check if the new `display_template` property is set
123
        prop = getattr(self, "display_template", None)
124
        if prop is not None:
125
            return prop
126
127
        # BBB: ui_item
128
        ui_item = getattr(self, "ui_item", None),
129
        if ui_item is not None:
130
            return "<a href='${url}' _target='blank'>${%s}</a>" % ui_item
131
132
        return default
133
134
    def get_catalog(self, context, field, default=None):
135
        """Lookup the catalog to query
136
137
        :param context: The current context of the field
138
        :param field: The current field of the widget
139
        :param default: The default property value
140
        :returns: Catalog name to query
141
        """
142
        # check if the new `catalog` property is set
143
        prop = getattr(self, "catalog", None)
144
        if prop is not None:
145
            return prop
146
147
        # BBB: catalog_name
148
        catalog_name = getattr(self, "catalog_name", None),
149
150
        if catalog_name is None:
151
            return DEFAULT_SEARCH_CATALOG
152
        return catalog_name
153
154
    def get_query(self, context, field, default=None):
155
        """Lookup the catalog query
156
157
        :param context: The current context of the field
158
        :param field: The current field of the widget
159
        :param default: The default property value
160
        :returns: Base catalog query
161
        """
162
        prop = getattr(self, "query", None)
163
        if prop:
164
            return prop
165
166
        base_query = self.get_base_query(context, field)
167
168
        # extend portal_type filter
169
        allowed_types = getattr(field, "allowed_types", None)
170
        allowed_types_method = getattr(field, "allowed_types_method", None)
171
        if allowed_types_method:
172
            meth = getattr(context, allowed_types_method)
173
            allowed_types = meth(field)
174
175
        if api.is_string(allowed_types):
176
            allowed_types = [allowed_types]
177
178
        base_query["portal_type"] = list(allowed_types)
179
180
        return base_query
181
182
    def get_base_query(self, context, field):
183
        """BBB: Get the base query from the widget
184
185
        NOTE: Base query can be a callable
186
        """
187
        base_query = getattr(self, "base_query", {})
188
        if callable(base_query):
189
            try:
190
                base_query = base_query(context, self, field.getName())
191
            except TypeError:
192
                base_query = base_query()
193
        if api.is_string(base_query):
194
            base_query = json.loads(base_query)
195
196
        return base_query
197
198
    def get_columns(self, context, field, default=None):
199
        """Lookup the columns to show in the results popup
200
201
        :param context: The current context of the field
202
        :param field: The current field of the widget
203
        :param default: The default property value
204
        :returns: List column records to display
205
        """
206
        prop = getattr(self, "columns", [])
207
        if len(prop) > 0:
208
            return prop
209
210
        # BBB: colModel
211
        col_model = getattr(self, "colModel", [])
212
213
        if not col_model:
214
            return default
215
216
        columns = []
217
        for col in col_model:
218
            name = col.get("columnName")
219
            # skip ignored columns
220
            if name in IGNORE_COLUMNS:
221
                continue
222
            columns.append({
223
                "name": name,
224
                "width": col.get("width", "50%"),
225
                "align": col.get("align", "left"),
226
                "label": col.get("label", ""),
227
            })
228
        return columns
229
230
    def get_search_index(self, context, field, default=None):
231
        """Lookup the search index for fulltext searches
232
233
        :param context: The current context of the field
234
        :param field: The current field of the widget
235
        :param default: The default property value
236
        :returns: ZCText compatible search index
237
        """
238
        prop = getattr(self, "search_index", None)
239
        if prop is not None:
240
            return prop
241
242
        # BBB: search_fields
243
        search_fields = getattr(self, "search_fields", [])
244
        if not isinstance(search_fields, (tuple, list)):
245
            search_fields = filter(None, [search_fields])
246
        if len(search_fields) > 0:
247
            return search_fields[0]
248
249
        return default
250
251
    def get_value(self, context, field, value=None):
252
        """Extract the value from the request or get it from the field
253
254
        :param context: The current context of the field
255
        :param field: The current field of the widget
256
        :param value: The current set value
257
        :returns: List of UIDs
258
        """
259
        # the value might come from the request, e.g. on object creation
260
        if isinstance(value, six.string_types):
261
            value = filter(None, value.split("\r\n"))
262
        # we handle always lists in the templates
263
        if value is None:
264
            return []
265
        if not isinstance(value, (list, tuple)):
266
            value = [value]
267
        return map(api.get_uid, value)
268
269 View Code Duplication
    def get_render_data(self, context, field, uid, template):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
270
        """Provides the needed data to render the display template from the UID
271
272
        :returns: Dictionary with data needed to render the display template
273
        """
274
        regex = r"\{(.*?)\}"
275
        names = re.findall(regex, template)
276
277
        try:
278
            obj = api.get_object(uid)
279
        except api.APIError:
280
            logger.error("No object found for field '{}' with UID '{}'".format(
281
                field.getName(), uid))
282
            return {}
283
284
        data = {
285
            "uid": api.get_uid(obj),
286
            "url": api.get_url(obj),
287
            "Title": api.get_title(obj),
288
            "Description": api.get_description(obj),
289
        }
290
        for name in names:
291
            if name not in data:
292
                value = getattr(obj, name, None)
293
                if callable(value):
294
                    value = value()
295
                data[name] = value
296
297
        return data
298
299 View Code Duplication
    def render_reference(self, context, field, uid):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
300
        """Returns a rendered HTML element for the reference
301
        """
302
        display_template = self.get_display_template(context, field, uid)
303
        template = string.Template(display_template)
304
        try:
305
            data = self.get_render_data(context, field, uid, display_template)
306
        except ValueError as e:
307
            # Current user might not have privileges to view this object
308
            logger.error(e.message)
309
            return ""
310
311
        if not data:
312
            return ""
313
314
        return template.safe_substitute(data)
315
316
317
registerWidget(ReferenceWidget, title="Reference Widget")
318