Passed
Pull Request — 2.x (#1857)
by Ramon
08:37 queued 03:08
created

bika.lims.content.analysisservice   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 601
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 54
eloc 380
dl 0
loc 601
rs 6.4799
c 0
b 0
f 0

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