Passed
Push — 2.x ( ca0980...375d4d )
by Ramon
08:08
created

bika.lims.content.analysisservice   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 601
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 50
eloc 381
dl 0
loc 601
rs 8.4
c 0
b 0
f 0

28 Methods

Rating   Name   Duplication   Size   Complexity  
A AnalysisService.getContainers() 0 9 1
A AnalysisService.getAvailableMethods() 0 4 1
A AnalysisService.getAvailableMethodUIDs() 0 8 1
A AnalysisService.get_methods_calculations() 0 8 3
A AnalysisService.getServiceDependants() 0 16 4
A AnalysisService.getMethods() 0 6 1
A AnalysisService.query_available_methods() 0 11 1
A AnalysisService.getRawCalculation() 0 10 2
A AnalysisService.getRawInstrument() 0 12 3
A AnalysisService.getRawMethods() 0 6 1
A AnalysisService.getMethod() 0 12 3
A AnalysisService._default_calculation_vocabulary() 0 12 2
A AnalysisService.getRawInstruments() 0 4 1
A AnalysisService.getCalculation() 0 8 2
A AnalysisService._instruments_vocabulary() 0 19 5
A AnalysisService.getInstrument() 0 12 3
A AnalysisService._default_method_vocabulary() 0 13 2
A AnalysisService.getRawMethod() 0 14 3
A AnalysisService._methods_vocabulary() 0 7 1
A AnalysisService._renameAfterCreation() 0 4 1
A AnalysisService.query_available_instruments() 0 11 1
A AnalysisService.getInstruments() 0 6 1
A AnalysisService.getServiceDependencies() 0 9 2
A AnalysisService.query_available_calculations() 0 11 1
A AnalysisService.getServiceDependenciesUIDs() 0 6 1
A AnalysisService._default_instrument_vocabulary() 0 10 1
A AnalysisService.getServiceDependantsUIDs() 0 4 1
A AnalysisService.getPreservations() 0 10 1

How to fix   Complexity   

Complexity

Complex classes like bika.lims.content.analysisservice 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-2024 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
import itertools
22
23
from AccessControl import ClassSecurityInfo
24
from bika.lims import api
25
from bika.lims import bikaMessageFactory as _
26
from bika.lims.browser.fields import InterimFieldsField
27
from bika.lims.browser.fields import PartitionSetupField
28
from bika.lims.browser.fields import UIDReferenceField
29
from bika.lims.browser.fields.partitionsetupfield import getContainers
30
from bika.lims.browser.widgets.partitionsetupwidget import PartitionSetupWidget
31
from bika.lims.browser.widgets.recordswidget import RecordsWidget
32
from senaite.core.browser.widgets.referencewidget import ReferenceWidget
33
from bika.lims.catalog import SETUP_CATALOG
34
from bika.lims.config import PROJECTNAME
35
from bika.lims.content.abstractbaseanalysis import AbstractBaseAnalysis
36
from bika.lims.content.abstractbaseanalysis import schema
37
from bika.lims.interfaces import IAnalysisService
38
from bika.lims.interfaces import IDeactivable
39
from Products.Archetypes.atapi import PicklistWidget
40
from Products.Archetypes.Field import StringField
41
from Products.Archetypes.public import BooleanField
42
from Products.Archetypes.public import BooleanWidget
43
from Products.Archetypes.public import DisplayList
44
from Products.Archetypes.public import Schema
45
from Products.Archetypes.public import SelectionWidget
46
from Products.Archetypes.public import registerType
47
from Products.Archetypes.Widget import StringWidget
48
from senaite.core.browser.fields.records import RecordsField
49
from zope.interface import implements
50
51
Methods = UIDReferenceField(
52
    "Methods",
53
    schemata="Method",
54
    required=0,
55
    multiValued=1,
56
    vocabulary="_methods_vocabulary",
57
    allowed_types=("Method", ),
58
    accessor="getRawMethods",
59
    widget=PicklistWidget(
60
        label=_("Methods"),
61
        description=_(
62
            "Available methods to perform the test"),
63
    )
64
)
65
66
Instruments = UIDReferenceField(
67
    "Instruments",
68
    schemata="Method",
69
    required=0,
70
    multiValued=1,
71
    vocabulary="_instruments_vocabulary",
72
    allowed_types=("Instrument", ),
73
    accessor="getRawInstruments",
74
    widget=PicklistWidget(
75
        label=_("Instruments"),
76
        description=_("Available instruments based on the selected methods."),
77
    )
78
)
79
80
# XXX: HIDDEN -> TO BE REMOVED
81
UseDefaultCalculation = BooleanField(
82
    "UseDefaultCalculation",
83
    schemata="Method",
84
    default=True,
85
    widget=BooleanWidget(
86
        visible=False,
87
        label=_("Use the Default Calculation of Method"),
88
        description=_(
89
            "Select if the calculation to be used is the calculation set by "
90
            "default in the default method. If unselected, the calculation "
91
            "can be selected manually"),
92
    )
93
)
94
95
Calculation = UIDReferenceField(
96
    "Calculation",
97
    schemata="Method",
98
    required=0,
99
    relationship="AnalysisServiceCalculation",
100
    vocabulary="_default_calculation_vocabulary",
101
    allowed_types=("Calculation", ),
102
    accessor="getRawCalculation",
103
    widget=SelectionWidget(
104
        format="select",
105
        label=_("Calculation"),
106
        description=_("Calculation to be assigned to this content."),
107
        catalog_name=SETUP_CATALOG,
108
        base_query={"is_active": True},
109
    )
110
)
111
112
InterimFields = InterimFieldsField(
113
    "InterimFields",
114
    schemata="Result Options",
115
    widget=RecordsWidget(
116
        label=_("Result variables"),
117
        description=_("Additional result values"),
118
    )
119
)
120
121
# XXX: HIDDEN -> TO BE REMOVED
122
Separate = BooleanField(
123
    "Separate",
124
    schemata="Container and Preservation",
125
    default=False,
126
    required=0,
127
    widget=BooleanWidget(
128
        visible=False,
129
        label=_("Separate Container"),
130
        description=_("Check this box to ensure a separate sample container "
131
                      "is used for this analysis service"),
132
    )
133
)
134
135
# XXX: HIDDEN -> TO BE REMOVED
136
Preservation = UIDReferenceField(
137
    "Preservation",
138
    schemata="Container and Preservation",
139
    allowed_types=("SamplePreservation",),
140
    vocabulary="getPreservations",
141
    required=0,
142
    multiValued=0,
143
    widget=ReferenceWidget(
144
        visible=False,
145
        label=_("Default Preservation"),
146
        description=_(
147
            "Select a default preservation for this analysis service. If the "
148
            "preservation depends on the sample type combination, specify a "
149
            "preservation per sample type in the table below"),
150
        catalog_name=SETUP_CATALOG,
151
        base_query={"is_active": True},
152
    )
153
)
154
155
# XXX: HIDDEN -> TO BE REMOVED
156
Container = UIDReferenceField(
157
    "Container",
158
    schemata="Container and Preservation",
159
    allowed_types=("Container", "ContainerType"),
160
    vocabulary="getContainers",
161
    required=0,
162
    multiValued=0,
163
    widget=ReferenceWidget(
164
        visible=False,
165
        label=_("Default Container"),
166
        description=_(
167
            "Select the default container to be used for this analysis "
168
            "service. If the container to be used depends on the sample type "
169
            "and preservation combination, specify the container in the "
170
            "sample type table below"),
171
        catalog_name=SETUP_CATALOG,
172
        base_query={"is_active": True},
173
    )
174
)
175
176
# XXX: HIDDEN -> TO BE REMOVED
177
PartitionSetup = PartitionSetupField(
178
    "PartitionSetup",
179
    schemata="Container and Preservation",
180
    widget=PartitionSetupWidget(
181
        visible=False,
182
        label=_("Preservation per sample type"),
183
        description=_(
184
            "Please specify preservations that differ from the analysis "
185
            "service's default preservation per sample type here."),
186
    )
187
)
188
189
# Allow/disallow the capture of text as the result of the analysis
190
DefaultResult = StringField(
191
    "DefaultResult",
192
    schemata="Result Options",
193
    validators=('service_defaultresult_validator',),
194
    widget=StringWidget(
195
        label=_("Default result"),
196
        description=_(
197
            "Default result to display on result entry"
198
        )
199
    )
200
)
201
202
Conditions = RecordsField(
203
    "Conditions",
204
    schemata="Advanced",
205
    type="conditions",
206
    subfields=(
207
        "title",
208
        "description",
209
        "type",
210
        "choices",
211
        "default",
212
        "required",
213
    ),
214
    required_subfields=(
215
        "title",
216
        "type",
217
    ),
218
    subfield_labels={
219
        "title": _("Title"),
220
        "description": _("Description"),
221
        "type": _("Control type"),
222
        "choices": _("Choices"),
223
        "default": _("Default value"),
224
        "required": _("Required"),
225
    },
226
    subfield_descriptions={
227
        "choices": _("Please use the following format for select options: "
228
                     "key1:value1|key2:value2|...|keyN:valueN"),
229
    },
230
    subfield_types={
231
        "title": "string",
232
        "description": "string",
233
        "type": "string",
234
        "default": "string",
235
        "choices": "string",
236
        "required": "boolean",
237
    },
238
    subfield_sizes={
239
        "title": 20,
240
        "description": 50,
241
        "type": 1,
242
        "choices": 30,
243
        "default": 20,
244
    },
245
    subfield_validators={
246
        "title": "service_conditions_validator",
247
    },
248
    subfield_maxlength={
249
        "title": 50,
250
        "description": 200,
251
    },
252
    subfield_vocabularies={
253
        "type": DisplayList((
254
            ('', ''),
255
            ('text', _('Text')),
256
            ('number', _('Number')),
257
            ('checkbox', _('Checkbox')),
258
            ('select', _('Select')),
259
            ('file', _('File upload')),
260
        )),
261
    },
262
    widget=RecordsWidget(
263
        label=_("Analysis conditions"),
264
        description=_(
265
            "Conditions to ask for this analysis on sample registration. For "
266
            "instance, laboratory may want the user to input the temperature, "
267
            "the ramp and flow when a thermogravimetric (TGA) analysis is "
268
            "selected on sample registration. The information provided will be "
269
            "later considered by the laboratory personnel when performing the "
270
            "test."
271
        ),
272
    )
273
)
274
275
276
schema = schema.copy() + Schema((
277
    Methods,
278
    Instruments,
279
    UseDefaultCalculation,
280
    Calculation,
281
    InterimFields,
282
    Separate,
283
    Preservation,
284
    Container,
285
    PartitionSetup,
286
    DefaultResult,
287
    Conditions,
288
))
289
290
# Move default method field after available methods field
291
schema.moveField("Method", after="Methods")
292
# Move default instrument field after available instruments field
293
schema.moveField("Instrument", after="Instruments")
294
# Move default result field after Result Options
295
schema.moveField("DefaultResult", after="ResultOptions")
296
297
298
class AnalysisService(AbstractBaseAnalysis):
299
    """Analysis Service Content Holder
300
    """
301
    implements(IAnalysisService, IDeactivable)
302
    security = ClassSecurityInfo()
303
    schema = schema
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable schema does not seem to be defined.
Loading history...
304
    _at_rename_after_creation = True
305
306
    def _renameAfterCreation(self, check_auto_id=False):
307
        from senaite.core.idserver import renameAfterCreation
308
309
        return renameAfterCreation(self)
310
311
    def getMethods(self):
312
        """Returns the assigned methods
313
314
        :returns: List of method objects
315
        """
316
        return self.getField("Methods").get(self)
317
318
    def getRawMethods(self):
319
        """Returns the assigned method UIDs
320
321
        :returns: List of method UIDs
322
        """
323
        return self.getField("Methods").getRaw(self)
324
325
    def getMethod(self):
326
        """Get the default method
327
        """
328
        field = self.getField("Method")
329
        method = field.get(self)
330
        if not method:
331
            return None
332
        # check if the method is in the selected methods
333
        methods = self.getMethods()
334
        if method not in methods:
335
            return None
336
        return method
337
338
    def getRawMethod(self):
339
        """Returns the UID of the default method
340
341
        :returns: method UID
342
        """
343
        field = self.getField("Method")
344
        method = field.getRaw(self)
345
        if not method:
346
            return None
347
        # check if the method is in the selected methods
348
        methods = self.getRawMethods()
349
        if method not in methods:
350
            return None
351
        return method
352
353
    def getInstruments(self):
354
        """Returns the assigned instruments
355
356
        :returns: List of instrument objects
357
        """
358
        return self.getField("Instruments").get(self)
359
360
    def getRawInstruments(self):
361
        """List of assigned Instrument UIDs
362
        """
363
        return self.getField("Instruments").getRaw(self)
364
365
    def getInstrument(self):
366
        """Return the default instrument
367
        """
368
        field = self.getField("Instrument")
369
        instrument = field.get(self)
370
        if not instrument:
371
            return None
372
        # check if the instrument is in the selected instruments
373
        instruments = self.getInstruments()
374
        if instrument not in instruments:
375
            return None
376
        return instrument
377
378
    def getRawInstrument(self):
379
        """Return the UID of the default instrument
380
        """
381
        field = self.getField("Instrument")
382
        instrument = field.getRaw(self)
383
        if not instrument:
384
            return None
385
        # check if the instrument is in the selected instruments
386
        instruments = self.getRawInstruments()
387
        if instrument not in instruments:
388
            return None
389
        return instrument
390
391
    def getCalculation(self):
392
        """Get the default calculation
393
        """
394
        field = self.getField("Calculation")
395
        calculation = field.get(self)
396
        if not calculation:
397
            return None
398
        return calculation
399
400
    def getRawCalculation(self):
401
        """Returns the UID of the assigned calculation
402
403
        :returns: Calculation UID
404
        """
405
        field = self.getField("Calculation")
406
        calculation = field.getRaw(self)
407
        if not calculation:
408
            return None
409
        return calculation
410
411
    def getServiceDependencies(self):
412
        """Return calculation dependencies of the service
413
414
        :return: a list of analysis services objects.
415
        """
416
        calc = self.getCalculation()
417
        if calc:
418
            return calc.getCalculationDependencies(flat=True)
419
        return []
420
421
    def getServiceDependenciesUIDs(self):
422
        """Return calculation dependency UIDs of the service
423
424
        :return: a list of uids
425
        """
426
        return map(api.get_uid, self.getServiceDependencies())
427
428
    def getServiceDependants(self):
429
        """Return services depending on us
430
        """
431
        catalog = api.get_tool(SETUP_CATALOG)
432
        active_calcs = catalog(portal_type="Calculation", is_active=True)
433
        calculations = map(api.get_object, active_calcs)
434
        dependants = []
435
        for calc in calculations:
436
            calc_dependants = calc.getDependentServices()
437
            if self in calc_dependants:
438
                calc_dependencies = calc.getCalculationDependants()
439
                dependants = dependants + calc_dependencies
440
        dependants = list(set(dependants))
441
        if self in dependants:
442
            dependants.remove(self)
443
        return dependants
444
445
    def getServiceDependantsUIDs(self):
446
        """Return service UIDs depending on us
447
        """
448
        return map(api.get_uid, self.getServiceDependants())
449
450
    def query_available_methods(self):
451
        """Return all available methods
452
        """
453
        catalog = api.get_tool(SETUP_CATALOG)
454
        query = {
455
            "portal_type": "Method",
456
            "is_active": True,
457
            "sort_on": "sortable_title",
458
            "sort_order": "ascending",
459
        }
460
        return catalog(query)
461
462
    def _methods_vocabulary(self):
463
        """Vocabulary used for methods field
464
        """
465
        methods = self.query_available_methods()
466
        items = [(api.get_uid(m), api.get_title(m)) for m in methods]
467
        dlist = DisplayList(items)
468
        return dlist
469
470
    def _default_method_vocabulary(self):
471
        """Vocabulary used for default method field
472
        """
473
        # check if we selected methods
474
        methods = self.getMethods()
475
        if not methods:
476
            # query all available methods
477
            methods = self.query_available_methods()
478
        items = [(api.get_uid(m), api.get_title(m)) for m in methods]
479
        dlist = DisplayList(items).sortedByValue()
480
        # allow to leave this field empty
481
        dlist.add("", _("None"))
482
        return dlist
483
484
    def query_available_instruments(self):
485
        """Return all available Instruments
486
        """
487
        catalog = api.get_tool(SETUP_CATALOG)
488
        query = {
489
            "portal_type": "Instrument",
490
            "is_active": True,
491
            "sort_on": "sortable_title",
492
            "sort_order": "ascending",
493
        }
494
        return catalog(query)
495
496
    def _instruments_vocabulary(self):
497
        """Vocabulary used for instruments field
498
        """
499
        instruments = []
500
        # When methods are selected, display only instruments from the methods
501
        methods = self.getMethods()
502
        for method in methods:
503
            for instrument in method.getInstruments():
504
                if instrument in instruments:
505
                    continue
506
                instruments.append(instrument)
507
508
        if not methods:
509
            # query all available instruments when no methods are selected
510
            instruments = self.query_available_instruments()
511
512
        items = [(api.get_uid(i), api.get_title(i)) for i in instruments]
513
        dlist = DisplayList(items)
514
        return dlist
515
516
    def _default_instrument_vocabulary(self):
517
        """Vocabulary used for default instrument field
518
        """
519
        # check if we selected instruments
520
        instruments = self.getInstruments()
521
        items = [(api.get_uid(i), api.get_title(i)) for i in instruments]
522
        dlist = DisplayList(items).sortedByValue()
523
        # allow to leave this field empty
524
        dlist.add("", _("None"))
525
        return dlist
526
527
    def query_available_calculations(self):
528
        """Return all available calculations
529
        """
530
        catalog = api.get_tool(SETUP_CATALOG)
531
        query = {
532
            "portal_type": "Calculation",
533
            "is_active": True,
534
            "sort_on": "sortable_title",
535
            "sort_order": "ascending",
536
        }
537
        return catalog(query)
538
539
    def get_methods_calculations(self):
540
        """Return calculations assigned to the selected methods
541
        """
542
        methods = self.getMethods()
543
        if not methods:
544
            return None
545
        methods_calcs = map(lambda m: m.getCalculations(), methods)
546
        return list(itertools.chain(*methods_calcs))
547
548
    def _default_calculation_vocabulary(self):
549
        """Vocabulary used for the default calculation field
550
        """
551
        calculations = self.get_methods_calculations()
552
        if calculations is None:
553
            # query all available calculations
554
            calculations = self.query_available_calculations()
555
        items = [(api.get_uid(c), api.get_title(c)) for c in calculations]
556
        # allow to leave this field empty
557
        dlist = DisplayList(items).sortedByValue()
558
        dlist.add("", _("None"))
559
        return dlist
560
561
    # XXX DECIDE IF NEEDED
562
    # --------------------
563
564
    def getContainers(self, instance=None):
565
        # On first render, the containers must be filtered
566
        instance = instance or self
567
        separate = self.getSeparate()
568
        containers = getContainers(instance,
569
                                   allow_blank=True,
570
                                   show_container_types=not separate,
571
                                   show_containers=separate)
572
        return DisplayList(containers)
573
574
    def getPreservations(self):
575
        query = {
576
            "portal_type": "SamplePreservation",
577
            "is_active": True,
578
            "sort_on": "sortable_title",
579
            "sort_order": "ascending",
580
        }
581
        brains = api.search(query, SETUP_CATALOG)
582
        items = [(brain.UID, brain.Title) for brain in brains]
583
        return DisplayList(items)
584
585
    def getAvailableMethods(self):
586
        """Returns the methods available for this analysis.
587
        """
588
        return self.getMethods()
589
590
    def getAvailableMethodUIDs(self):
591
        """Returns the UIDs of the available methods
592
593
        Used here:
594
        bika.lims.catalog.indexers.bikasetup.method_available_uid
595
        """
596
        # N.B. we return a copy of the list to avoid accidental writes
597
        return self.getRawMethods()[:]
598
599
600
registerType(AnalysisService, PROJECTNAME)
601