Completed
Push — master ( 65c723...b6585a )
by Jordi
76:39 queued 72:29
created

AnalysisSpecificationWidget.process_form()   D

Complexity

Conditions 12

Size

Total Lines 81
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 48
dl 0
loc 81
rs 4.8
c 0
b 0
f 0
cc 12
nop 6

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 bika.lims.browser.widgets.analysisspecificationwidget.AnalysisSpecificationWidget.process_form() 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
import collections
22
23
from AccessControl import ClassSecurityInfo
24
from bika.lims import api
25
from bika.lims import bikaMessageFactory as _
26
from bika.lims import logger
27
from bika.lims.api.security import check_permission
28
from bika.lims.browser.bika_listing import BikaListingView
29
from bika.lims.config import MAX_OPERATORS
30
from bika.lims.config import MIN_OPERATORS
31
from bika.lims.permissions import FieldEditSpecification
32
from bika.lims.utils import get_image
33
from bika.lims.utils import get_link
34
from bika.lims.utils import to_choices
35
from plone.memoize import view
36
from Products.Archetypes.Registry import registerWidget
37
from Products.Archetypes.Widget import TypesWidget
38
39
40
class AnalysisSpecificationView(BikaListingView):
41
    """Listing table to display Analysis Specifications
42
    """
43
44
    def __init__(self, context, request):
45
        super(AnalysisSpecificationView, self).__init__(context, request)
46
47
        self.catalog = "bika_setup_catalog"
48
        self.contentFilter = {
49
            "portal_type": "AnalysisService",
50
            "is_active": True,
51
            "sort_on": "sortable_title",
52
            "sort_order": "ascending",
53
        }
54
        self.context_actions = {}
55
56
        self.show_column_toggles = False
57
        self.show_select_column = True
58
        self.show_select_all_checkbox = False
59
        self.pagesize = 999999
60
        self.allow_edit = True
61
        self.show_search = True
62
        self.omit_form = True
63
        self.fetch_transitions_on_select = False
64
65
        # Categories
66
        if self.show_categories_enabled():
67
            self.categories = []
68
            self.show_categories = True
69
            self.expand_all_categories = False
70
71
        # Operator Choices
72
        self.min_operator_choices = to_choices(MIN_OPERATORS)
73
        self.max_operator_choices = to_choices(MAX_OPERATORS)
74
75
        self.columns = collections.OrderedDict((
76
            ("Title", {
77
                "title": _("Service"),
78
                "index": "sortable_title",
79
                "sortable": False}),
80
            ("Keyword", {
81
                "title": _("Keyword"),
82
                "sortable": False}),
83
            ("Methods", {
84
                "title": _("Methods"),
85
                "sortable": False}),
86
            ("Unit", {
87
                "title": _("Unit"),
88
                "sortable": False}),
89
            ("warn_min", {
90
                "title": _("Min warn"),
91
                "sortable": False}),
92
            ("min_operator", {
93
                "title": _("Min operator"),
94
                "type": "choices",
95
                "sortable": False}),
96
            ("min", {
97
                "title": _("Min"),
98
                "sortable": False}),
99
            ("max_operator", {
100
                "title": _("Max operator"),
101
                "type": "choices",
102
                "sortable": False}),
103
            ("max", {
104
                "title": _("Max"),
105
                "sortable": False}),
106
            ("warn_max", {
107
                "title": _("Max warn"),
108
                "sortable": False}),
109
            ("hidemin", {
110
                "title": _("< Min"),
111
                "sortable": False}),
112
            ("hidemax", {
113
                "title": _("> Max"),
114
                "sortable": False}),
115
            ("rangecomment", {
116
                "title": _("Range comment"),
117
                "sortable": False,
118
                "type": "remarks",
119
                "toggle": False}),
120
        ))
121
122
        self.review_states = [
123
            {
124
                "id": "default",
125
                "title": _("All"),
126
                "contentFilter": {},
127
                "transitions": [{"id": "disallow-all-possible-transitions"}],
128
                "columns": self.columns.keys(),
129
            },
130
        ]
131
132
    def update(self):
133
        """Update hook
134
        """
135
        super(AnalysisSpecificationView, self).update()
136
        self.allow_edit = self.is_edit_allowed()
137
        self.specification = self.context.getResultsRangeDict()
138
        self.dynamic_spec = self.context.getDynamicAnalysisSpec()
139
140
    @view.memoize
141
    def is_edit_allowed(self):
142
        """Check if edit is allowed
143
        """
144
        return check_permission(FieldEditSpecification, self.context)
145
146
    @view.memoize
147
    def show_categories_enabled(self):
148
        """Check in the setup if categories are enabled
149
        """
150
        return self.context.bika_setup.getCategoriseAnalysisServices()
151
152
    def get_editable_columns(self):
153
        """Return editable fields
154
        """
155
        columns = ["min", "max", "warn_min", "warn_max", "hidemin", "hidemax",
156
                   "rangecomment", "min_operator", "max_operator"]
157
        return columns
158
159
    def get_required_columns(self):
160
        """Return required editable fields
161
        """
162
        columns = []
163
        return columns
164
165
    @view.memoize
166
    def get_dynamic_analysisspecs(self):
167
        if not self.dynamic_spec:
168
            return {}
169
        return self.dynamic_spec.get_by_keyword()
170
171
    def folderitems(self):
172
        """TODO: Refactor to non-classic mode
173
        """
174
        items = super(AnalysisSpecificationView, self).folderitems()
175
        self.categories.sort()
176
        return items
177
178
    def folderitem(self, obj, item, index):
179
        """Service triggered each time an item is iterated in folderitems.
180
181
        The use of this service prevents the extra-loops in child objects.
182
183
        :obj: the instance of the class to be foldered
184
        :item: dict containing the properties of the object to be used by
185
            the template
186
        :index: current index of the item
187
        """
188
        # ensure we have an object and not a brain
189
        obj = api.get_object(obj)
190
        url = api.get_url(obj)
191
        title = api.get_title(obj)
192
        keyword = obj.getKeyword()
193
194
        # dynamic analysisspecs
195
        dspecs = self.get_dynamic_analysisspecs()
196
        dspec = dspecs.get(keyword)
197
        # show the dynamic specification icon next to the Keyword
198
        if dspec:
199
            item["before"]["Keyword"] = get_image(
200
                "dynamic_analysisspec.png",
201
                title=_("Found Dynamic Analysis Specification for '{}' in '{}'"
202
                        .format(keyword, self.dynamic_spec.Title())))
203
204
        # get the category
205
        if self.show_categories_enabled():
206
            category = obj.getCategoryTitle()
207
            if category not in self.categories:
208
                self.categories.append(category)
209
            item["category"] = category
210
211
        item["Title"] = title
212
        item["replace"]["Title"] = get_link(url, value=title)
213
        item["choices"]["min_operator"] = self.min_operator_choices
214
        item["choices"]["max_operator"] = self.max_operator_choices
215
        item["allow_edit"] = self.get_editable_columns()
216
        item["required"] = self.get_required_columns()
217
218
        spec = self.specification.get(keyword, {})
219
220
        item["selected"] = spec and True or False
221
        item["min_operator"] = spec.get("min_operator", "geq")
222
        item["min"] = spec.get("min", "")
223
        item["max_operator"] = spec.get("max_operator", "leq")
224
        item["max"] = spec.get("max", "")
225
        item["warn_min"] = spec.get("warn_min", "")
226
        item["warn_max"] = spec.get("warn_max", "")
227
        item["hidemin"] = spec.get("hidemin", "")
228
        item["hidemax"] = spec.get("hidemax", "")
229
        item["rangecomment"] = spec.get("rangecomment", "")
230
231
        # Add methods
232
        methods = obj.getMethods()
233
        if methods:
234
            links = map(
235
                lambda m: get_link(
236
                    m.absolute_url(), value=m.Title(), css_class="link"),
237
                methods)
238
            item["replace"]["Methods"] = ", ".join(links)
239
        else:
240
            item["methods"] = ""
241
242
        # Icons
243
        after_icons = ""
244
        if obj.getAccredited():
245
            after_icons += get_image(
246
                "accredited.png", title=_("Accredited"))
247
        if obj.getAttachmentOption() == "r":
248
            after_icons += get_image(
249
                "attach_reqd.png", title=_("Attachment required"))
250
        if obj.getAttachmentOption() == "n":
251
            after_icons += get_image(
252
                "attach_no.png", title=_("Attachment not permitted"))
253
        if after_icons:
254
            item["after"]["Title"] = after_icons
255
256
        return item
257
258
259
class AnalysisSpecificationWidget(TypesWidget):
260
    """Analysis Specification Widget
261
    """
262
    _properties = TypesWidget._properties.copy()
263
    _properties.update({
264
        "macro": "bika_widgets/analysisspecificationwidget",
265
    })
266
267
    security = ClassSecurityInfo()
268
269
    security.declarePublic("process_form")
270
271
    def process_form(self, instance, field, form, empty_marker=None,
272
                     emptyReturnsMarker=False):
273
        """Return a list of dictionaries fit for AnalysisSpecsResultsField
274
           consumption.
275
276
        If neither hidemin nor hidemax are specified, only services which have
277
        float()able entries in result,min and max field will be included. If
278
        hidemin and/or hidemax specified, results might contain empty min
279
        and/or max fields.
280
        """
281
        values = []
282
283
        # selected services
284
        service_uids = form.get("uids", [])
285
286
        # return immediately if now services were selected
287
        if not service_uids:
288
            return values, {}
289
290
        # dynamic analysis specification
291
        dynamic_spec = {}
292
        if instance.getDynamicAnalysisSpec():
293
            dynamic_spec = instance.getDynamicAnalysisSpec().get_by_keyword()
294
295
        for uid in service_uids:
296
            s_min = self._get_spec_value(form, uid, "min")
297
            s_max = self._get_spec_value(form, uid, "max")
298
299
            if not s_min and not s_max:
300
                service = api.get_object_by_uid(uid)
301
                keyword = service.getKeyword()
302
                if not dynamic_spec.get(keyword):
303
                    # If user has not set value neither for min nor max, omit
304
                    # this record. Otherwise, since 'min' and 'max' are defined
305
                    # as mandatory subfields, the following message will appear
306
                    # after submission: "Specifications is required, please
307
                    # correct."
308
                    continue
309
                s_min = 0
310
                s_max = 0
311
312
            # TODO: disallow this case in the UI
313
            if s_min and s_max:
314
                if float(s_min) > float(s_max):
315
                    logger.warn("Min({}) > Max({}) is not allowed"
316
                                .format(s_min, s_max))
317
                    continue
318
319
            min_operator = self._get_spec_value(
320
                form, uid, "min_operator", check_floatable=False)
321
            max_operator = self._get_spec_value(
322
                form, uid, "max_operator", check_floatable=False)
323
324
            service = api.get_object_by_uid(uid)
325
            subfield_values = {
326
                "keyword": service.getKeyword(),
327
                "uid": uid,
328
                "min_operator": min_operator,
329
                "min": s_min,
330
                "max_operator": max_operator,
331
                "max": s_max,
332
                "warn_min": self._get_spec_value(form, uid, "warn_min"),
333
                "warn_max": self._get_spec_value(form, uid, "warn_max"),
334
                "hidemin": self._get_spec_value(form, uid, "hidemin"),
335
                "hidemax": self._get_spec_value(form, uid, "hidemax"),
336
                "rangecomment": self._get_spec_value(form, uid, "rangecomment",
337
                                                     check_floatable=False)
338
            }
339
340
            # Include values from other subfields that might be added
341
            # by other add-ons independently via SchemaModifier
342
            for subfield in field.subfields:
343
                if subfield not in subfield_values.keys():
344
                    subfield_values.update({
345
                        subfield: self._get_spec_value(form, uid, subfield)
346
                    })
347
348
            values.append(subfield_values)
349
350
351
        return values, {}
352
353 View Code Duplication
    def _get_spec_value(self, form, uid, key, check_floatable=True,
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
354
                        default=''):
355
        """Returns the value assigned to the passed in key for the analysis
356
        service uid from the passed in form.
357
358
        If check_floatable is true, will return the passed in default if the
359
        obtained value is not floatable
360
361
        :param form: form being submitted
362
        :param uid: uid of the Analysis Service the specification relates
363
        :param key: id of the specs param to get (e.g. 'min')
364
        :param check_floatable: check if the value is floatable
365
        :param default: fallback value that will be returned by default
366
        :type default: str, None
367
        """
368
        if not form or not uid:
369
            return default
370
        values = form.get(key, None)
371
        if not values or len(values) == 0:
372
            return default
373
        value = values[0].get(uid, default)
374
        if not check_floatable:
375
            return value
376
        return api.is_floatable(value) and value or default
377
378
    security.declarePublic("AnalysisSpecificationResults")
379
380
    def AnalysisSpecificationResults(self, field, allow_edit=False):
381
        """Render Analyses Specifications Table
382
        """
383
        instance = getattr(self, "instance", field.aq_parent)
384
        table = api.get_view("table_analysis_specifications",
385
                             context=instance,
386
                             request=self.REQUEST)
387
        # Call listing hooks
388
        table.update()
389
        table.before_render()
390
        return table.ajax_contents_table()
391
392
393
registerWidget(AnalysisSpecificationWidget,
394
               title="Analysis Specification Results",
395
               description=("Analysis Specification Results"))
396