Passed
Push — master ( f31c62...c3750e )
by Jordi
07:28 queued 03:00
created

AnalysisService.getMethodUIDs()   A

Complexity

Conditions 1

Size

Total Lines 11
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 11
rs 10
c 0
b 0
f 0
cc 1
nop 1
1
# -*- coding: utf-8 -*-
2
#
3
# This file is part of SENAITE.CORE
4
#
5
# Copyright 2018 by it's authors.
6
# Some rights reserved. See LICENSE.rst, CONTRIBUTORS.rst.
7
8
from AccessControl import ClassSecurityInfo
9
from bika.lims import PMF
10
from bika.lims import api
11
from bika.lims import bikaMessageFactory as _
12
from bika.lims.browser.fields import InterimFieldsField
13
from bika.lims.browser.fields import UIDReferenceField
14
from bika.lims.browser.widgets.partitionsetupwidget import PartitionSetupWidget
15
from bika.lims.browser.widgets.recordswidget import RecordsWidget
16
from bika.lims.browser.widgets.referencewidget import ReferenceWidget
17
from bika.lims.config import PROJECTNAME
18
from bika.lims.content.abstractbaseanalysis import AbstractBaseAnalysis
19
from bika.lims.content.abstractbaseanalysis import schema
20
from bika.lims.interfaces import IAnalysisService
21
from bika.lims.interfaces import IDeactivable
22
from bika.lims.interfaces import IHaveIdentifiers
23
from bika.lims.utils import to_utf8 as _c
24
from magnitude import mg
25
from Products.Archetypes.public import BooleanField
26
from Products.Archetypes.public import BooleanWidget
27
from Products.Archetypes.public import DisplayList
28
from Products.Archetypes.public import MultiSelectionWidget
29
from Products.Archetypes.public import Schema
30
from Products.Archetypes.public import SelectionWidget
31
from Products.Archetypes.public import registerType
32
from Products.Archetypes.Registry import registerField
33
from Products.ATExtensions.ateapi import RecordsField
34
from Products.CMFCore.utils import getToolByName
35
from zope.interface import implements
36
37
38
def getContainers(instance,
39
                  minvol=None,
40
                  allow_blank=True,
41
                  show_container_types=True,
42
                  show_containers=True):
43
    """ Containers vocabulary
44
45
    This is a separate class so that it can be called from ajax to filter
46
    the container list, as well as being used as the AT field vocabulary.
47
48
    Returns a tuple of tuples: ((object_uid, object_title), ())
49
50
    If the partition is flagged 'Separate', only containers are displayed.
51
    If the Separate flag is false, displays container types.
52
53
    XXX bsc = self.portal.bika_setup_catalog
54
    XXX obj = bsc(getKeyword='Moist')[0].getObject()
55
    XXX u'Container Type: Canvas bag' in obj.getContainers().values()
56
    XXX True
57
58
    """
59
60
    bsc = getToolByName(instance, 'bika_setup_catalog')
61
62
    items = [['', _('Any')]] if allow_blank else []
63
64
    containers = []
65
    for container in bsc(portal_type='Container', sort_on='sortable_title'):
66
        container = container.getObject()
67
68
        # verify container capacity is large enough for required sample volume.
69
        if minvol is not None:
70
            capacity = container.getCapacity()
71
            try:
72
                capacity = capacity.split(' ', 1)
73
                capacity = mg(float(capacity[0]), capacity[1])
74
                if capacity < minvol:
75
                    continue
76
            except (ValueError, TypeError):
77
                # if there's a unit conversion error, allow the container
78
                # to be displayed.
79
                pass
80
81
        containers.append(container)
82
83
    if show_containers:
84
        # containers with no containertype first
85
        for container in containers:
86
            if not container.getContainerType():
87
                items.append([container.UID(), container.Title()])
88
89
    ts = getToolByName(instance, 'translation_service').translate
90
    cat_str = _c(ts(_('Container Type')))
91
    containertypes = [c.getContainerType() for c in containers]
92
    containertypes = dict([(ct.UID(), ct.Title())
93
                           for ct in containertypes if ct])
94
    for ctype_uid, ctype_title in containertypes.items():
95
        ctype_title = _c(ctype_title)
96
        if show_container_types:
97
            items.append([ctype_uid, "%s: %s" % (cat_str, ctype_title)])
98
        if show_containers:
99
            for container in containers:
100
                ctype = container.getContainerType()
101
                if ctype and ctype.UID() == ctype_uid:
102
                    items.append([container.UID(), container.Title()])
103
104
    items = tuple(items)
105
    return items
106
107
108
class PartitionSetupField(RecordsField):
109
    _properties = RecordsField._properties.copy()
110
    _properties.update({
111
        'subfields': (
112
            'sampletype',
113
            'separate',
114
            'preservation',
115
            'container',
116
            'vol',
117
            # 'retentionperiod',
118
        ),
119
        'subfield_labels': {
120
            'sampletype': _('Sample Type'),
121
            'separate': _('Separate Container'),
122
            'preservation': _('Preservation'),
123
            'container': _('Container'),
124
            'vol': _('Required Volume'),
125
            # 'retentionperiod': _('Retention Period'),
126
        },
127
        'subfield_types': {
128
            'separate': 'boolean',
129
            'vol': 'string',
130
            'container': 'selection',
131
            'preservation': 'selection',
132
        },
133
        'subfield_vocabularies': {
134
            'sampletype': 'SampleTypes',
135
            'preservation': 'Preservations',
136
            'container': 'Containers',
137
        },
138
        'subfield_sizes': {
139
            'sampletype': 1,
140
            'preservation': 6,
141
            'vol': 8,
142
            'container': 6,
143
            # 'retentionperiod':10,
144
        }
145
    })
146
    security = ClassSecurityInfo()
147
148
    security.declarePublic('SampleTypes')
149
150
    def SampleTypes(self, instance=None):
151
        instance = instance or self
152
        bsc = getToolByName(instance, 'bika_setup_catalog')
153
        items = []
154
        for st in bsc(portal_type='SampleType',
155
                      is_active=True,
156
                      sort_on='sortable_title'):
157
            st = st.getObject()
158
            title = st.Title()
159
            items.append((st.UID(), title))
160
        items = [['', '']] + list(items)
161
        return DisplayList(items)
162
163
    security.declarePublic('Preservations')
164
165
    def Preservations(self, instance=None):
166
        instance = instance or self
167
        bsc = getToolByName(instance, 'bika_setup_catalog')
168
        items = [[c.UID, c.title] for c in
169
                 bsc(portal_type='Preservation',
170
                     is_active=True,
171
                     sort_on='sortable_title')]
172
        items = [['', _('Any')]] + list(items)
173
        return DisplayList(items)
174
175
    security.declarePublic('Containers')
176
177
    def Containers(self, instance=None):
178
        instance = instance or self
179
        items = getContainers(instance, allow_blank=True)
180
        return DisplayList(items)
181
182
183
registerField(PartitionSetupField, title="", description="")
184
185
186
# If this flag is true, then analyses created from this service will be linked
187
# to their own Sample Partition, and no other analyses will be linked to that
188
# partition.
189
Separate = BooleanField(
190
    'Separate',
191
    schemata='Container and Preservation',
192
    default=False,
193
    required=0,
194
    widget=BooleanWidget(
195
        label=_("Separate Container"),
196
        description=_("Check this box to ensure a separate sample container is "
197
                      "used for this analysis service"),
198
    )
199
)
200
201
# The preservation for this service; If multiple services share the same
202
# preservation, then it's possible that they can be performed on the same
203
# sample partition.
204
Preservation = UIDReferenceField(
205
    'Preservation',
206
    schemata='Container and Preservation',
207
    allowed_types=('Preservation',),
208
    vocabulary='getPreservations',
209
    required=0,
210
    multiValued=0,
211
    widget=ReferenceWidget(
212
        checkbox_bound=0,
213
        label=_("Default Preservation"),
214
        description=_(
215
            "Select a default preservation for this analysis service. If the "
216
            "preservation depends on the sample type combination, specify a "
217
            "preservation per sample type in the table below"),
218
        catalog_name='bika_setup_catalog',
219
        base_query={'is_active': True},
220
    )
221
)
222
223
# The container or containertype for this service's analyses can be specified.
224
# If multiple services share the same container or containertype, then it's
225
# possible that their analyses can be performed on the same partitions
226
Container = UIDReferenceField(
227
    'Container',
228
    schemata='Container and Preservation',
229
    allowed_types=('Container', 'ContainerType'),
230
    vocabulary='getContainers',
231
    required=0,
232
    multiValued=0,
233
    widget=ReferenceWidget(
234
        checkbox_bound=0,
235
        label=_("Default Container"),
236
        description=_(
237
            "Select the default container to be used for this analysis "
238
            "service. If the container to be used depends on the sample type "
239
            "and preservation combination, specify the container in the "
240
            "sample type table below"),
241
        catalog_name='bika_setup_catalog',
242
        base_query={'is_active': True},
243
    )
244
)
245
246
# This is a list of dictionaries which contains the PartitionSetupWidget
247
# settings.  This is used to decide how many distinct physical partitions
248
# will be created, which containers/preservations they will use, and which
249
# analyases can be performed on each partition.
250
PartitionSetup = PartitionSetupField(
251
    'PartitionSetup',
252
    schemata='Container and Preservation',
253
    widget=PartitionSetupWidget(
254
        label=PMF("Preservation per sample type"),
255
        description=_(
256
            "Please specify preservations that differ from the analysis "
257
            "service's default preservation per sample type here."),
258
    )
259
)
260
261
# Allow/Disallow to set the calculation manually
262
# Behavior controlled by javascript depending on Instruments field:
263
# - If no instruments available, hide and uncheck
264
# - If at least one instrument selected then checked, but not readonly
265
# See browser/js/bika.lims.analysisservice.edit.js
266
UseDefaultCalculation = BooleanField(
267
    'UseDefaultCalculation',
268
    schemata="Method",
269
    default=True,
270
    widget=BooleanWidget(
271
        label=_("Use the Default Calculation of Method"),
272
        description=_(
273
            "Select if the calculation to be used is the calculation set by "
274
            "default in the default method. If unselected, the calculation "
275
            "can be selected manually"),
276
    )
277
)
278
279
# Manual methods associated to the AS
280
# List of methods capable to perform the Analysis Service. The
281
# Methods selected here are displayed in the Analysis Request
282
# Add view, closer to this Analysis Service if selected.
283
# Use getAvailableMethods() to retrieve the list with methods both
284
# from selected instruments and manually entered.
285
# Behavior controlled by js depending on ManualEntry/Instrument:
286
# - If InstrumentEntry not checked, show
287
# See browser/js/bika.lims.analysisservice.edit.js
288
Methods = UIDReferenceField(
289
    'Methods',
290
    schemata="Method",
291
    required=0,
292
    multiValued=1,
293
    vocabulary='_getAvailableMethodsDisplayList',
294
    allowed_types=('Method',),
295
    accessor="getMethodUIDs",
296
    widget=MultiSelectionWidget(
297
        label=_("Methods"),
298
        description=_(
299
            "The tests of this type of analysis can be performed by using "
300
            "more than one method with the 'Manual entry of results' option "
301
            "enabled. A selection list with the methods selected here is "
302
            "populated in the manage results view for each test of this type "
303
            "of analysis. Note that only methods with 'Allow manual entry' "
304
            "option enabled are displayed here; if you want the user to be "
305
            "able to assign a method that requires instrument entry, enable "
306
            "the 'Instrument assignment is allowed' option."),
307
    )
308
)
309
310
# Instruments associated to the AS
311
# List of instruments capable to perform the Analysis Service. The
312
# Instruments selected here are displayed in the Analysis Request
313
# Add view, closer to this Analysis Service if selected.
314
# - If InstrumentEntry not checked, hide and unset
315
# - If InstrumentEntry checked, set the first selected and show
316
Instruments = UIDReferenceField(
317
    'Instruments',
318
    schemata="Method",
319
    required=0,
320
    multiValued=1,
321
    vocabulary='_getAvailableInstrumentsDisplayList',
322
    allowed_types=('Instrument',),
323
    accessor="getInstrumentUIDs",
324
    widget=MultiSelectionWidget(
325
        label=_("Instruments"),
326
        description=_(
327
            "More than one instrument can be used in a test of this type of "
328
            "analysis. A selection list with the instruments selected here is "
329
            "populated in the results manage view for each test of this type "
330
            "of analysis. The available instruments in the selection list "
331
            "will change in accordance with the method selected by the user "
332
            "for that test in the manage results view. Although a method can "
333
            "have more than one instrument assigned, the selection list is "
334
            "only populated with the instruments that are both set here and "
335
            "allowed for the selected method."),
336
    )
337
)
338
339
# Calculation to be used. This field is used in Analysis Service Edit view.
340
#
341
# AnalysisService defines a setter to maintain back-references on the
342
# calculation, so that calculations can easily lookup their dependants based
343
# on this field's value.
344
#
345
#  The default calculation is the one linked to the default method Behavior
346
# controlled by js depending on UseDefaultCalculation:
347
# - If UseDefaultCalculation is set to False, show this field
348
# - If UseDefaultCalculation is set to True, show this field
349
#  See browser/js/bika.lims.analysisservice.edit.js
350
Calculation = UIDReferenceField(
351
    "Calculation",
352
    schemata="Method",
353
    required=0,
354
    vocabulary="_getAvailableCalculationsDisplayList",
355
    allowed_types=("Calculation",),
356
    accessor="getCalculationUID",
357
    widget=SelectionWidget(
358
        format="select",
359
        label=_("Calculation"),
360
        description=_("Calculation to be assigned to this content."),
361
        catalog_name="bika_setup_catalog",
362
        base_query={"is_active": True},
363
    )
364
)
365
366
# InterimFields are defined in Calculations, Services, and Analyses.
367
# In Analysis Services, the default values are taken from Calculation.
368
# In Analyses, the default values are taken from the Analysis Service.
369
# When instrument results are imported, the values in analysis are overridden
370
# before the calculation is performed.
371
InterimFields = InterimFieldsField(
372
    'InterimFields',
373
    schemata='Method',
374
    widget=RecordsWidget(
375
        label=_("Additional Values"),
376
        description=_(
377
            "Values can be entered here which will override the defaults "
378
            "specified in the Calculation Interim Fields."),
379
    )
380
)
381
382
schema = schema.copy() + Schema((
383
    Separate,
384
    Preservation,
385
    Container,
386
    PartitionSetup,
387
    Methods,
388
    Instruments,
389
    UseDefaultCalculation,
390
    Calculation,
391
    InterimFields,
392
))
393
394
# Re-order some fields from AbstractBaseAnalysis schema.
395
# Adding them to the Schema(()) above does not work.
396
schema.moveField('ManualEntryOfResults', after='PartitionSetup')
397
schema.moveField('Methods', after='ManualEntryOfResults')
398
schema.moveField('InstrumentEntryOfResults', after='Methods')
399
schema.moveField('Instruments', after='InstrumentEntryOfResults')
400
schema.moveField('Instrument', after='Instruments')
401
schema.moveField('Method', after='Instrument')
402
schema.moveField('Calculation', after='UseDefaultCalculation')
403
schema.moveField('DuplicateVariation', after='Calculation')
404
schema.moveField('Accredited', after='Calculation')
405
schema.moveField('InterimFields', after='Calculation')
406
407
408
class AnalysisService(AbstractBaseAnalysis):
409
    implements(IAnalysisService, IHaveIdentifiers, IDeactivable)
410
    security = ClassSecurityInfo()
411
    schema = schema
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable schema does not seem to be defined.
Loading history...
412
    displayContentsTab = False
413
    _at_rename_after_creation = True
414
415
    def _renameAfterCreation(self, check_auto_id=False):
416
        from bika.lims.idserver import renameAfterCreation
417
418
        return renameAfterCreation(self)
419
420
    @security.public
421
    def getCalculationTitle(self):
422
        """Used to populate catalog values
423
        """
424
        calculation = self.getCalculation()
425
        if calculation:
426
            return calculation.Title()
427
428
    @security.public
429
    def getCalculation(self):
430
        """Returns the assigned calculation
431
432
        :returns: Calculation object
433
        """
434
        return self.getField("Calculation").get(self)
435
436
    @security.public
437
    def getCalculationUID(self):
438
        """Returns the UID of the assigned calculation
439
440
        NOTE: This is the default accessor of the `Calculation` schema field
441
        and needed for the selection widget to render the selected value
442
        properly in _view_ mode.
443
444
        :returns: Calculation UID
445
        """
446
        calculation = self.getCalculation()
447
        if not calculation:
448
            return None
449
        return api.get_uid(calculation)
450
451
    @security.public
452
    def getContainers(self, instance=None):
453
        # On first render, the containers must be filtered
454
        instance = instance or self
455
        separate = self.getSeparate()
456
        containers = getContainers(instance,
457
                                   allow_blank=True,
458
                                   show_container_types=not separate,
459
                                   show_containers=separate)
460
        return DisplayList(containers)
461
462
    def getPreservations(self):
463
        bsc = getToolByName(self, 'bika_setup_catalog')
464
        items = [(o.UID, o.Title) for o in
465
                 bsc(portal_type='Preservation', is_active=True)]
466
        items.sort(lambda x, y: cmp(x[1], y[1]))
467
        return DisplayList(list(items))
468
469
    @security.public
470
    def getAvailableMethods(self):
471
        """ Returns the methods available for this analysis.
472
            If the service has the getInstrumentEntryOfResults(), returns
473
            the methods available from the instruments capable to perform
474
            the service, as well as the methods set manually for the
475
            analysis on its edit view. If getInstrumentEntryOfResults()
476
            is unset, only the methods assigned manually to that service
477
            are returned.
478
        """
479
        methods = self.getMethods()
480
        muids = [m.UID() for m in methods]
481
        if self.getInstrumentEntryOfResults():
482
            # Add the methods from the instruments capable to perform
483
            # this analysis service
484
            for ins in self.getInstruments():
485
                for method in ins.getMethods():
486
                    if method and method.UID() not in muids:
487
                        methods.append(method)
488
                        muids.append(method.UID())
489
490
        return methods
491
492
    @security.public
493
    def getAvailableMethodUIDs(self):
494
        """
495
        Returns the UIDs of the available methods. it is used as a
496
        vocabulary to fill the selection list of 'Methods' field.
497
        """
498
        return [m.UID() for m in self.getAvailableMethods()]
499
500
    @security.public
501
    def getMethods(self):
502
        """Returns the assigned methods
503
504
        If you want to obtain the available methods to assign to the service,
505
        use getAvailableMethodUIDs.
506
507
        :returns: List of method objects
508
        """
509
        return self.getField("Methods").get(self)
510
511
    @security.public
512
    def getMethodUIDs(self):
513
        """Returns the UIDs of the assigned methods
514
515
        NOTE: This is the default accessor of the `Methods` schema field
516
        and needed for the multiselection widget to render the selected values
517
        properly in _view_ mode.
518
519
        :returns: List of method UIDs
520
        """
521
        return map(api.get_uid, self.getMethods())
522
523
    @security.public
524
    def getInstruments(self):
525
        """Returns the assigned instruments
526
527
        :returns: List of instrument objects
528
        """
529
        return self.getField("Instruments").get(self)
530
531
    @security.public
532
    def getInstrumentUIDs(self):
533
        """Returns the UIDs of the assigned instruments
534
535
        NOTE: This is the default accessor of the `Instruments` schema field
536
        and needed for the multiselection widget to render the selected values
537
        properly in _view_ mode.
538
539
        :returns: List of instrument UIDs
540
        """
541
        return map(api.get_uid, self.getInstruments())
542
543
    @security.public
544
    def getAvailableInstruments(self):
545
        """ Returns the instruments available for this service.
546
            If the service has the getInstrumentEntryOfResults(), returns
547
            the instruments capable to perform this service. Otherwhise,
548
            returns an empty list.
549
        """
550
        instruments = self.getInstruments() \
551
            if self.getInstrumentEntryOfResults() is True \
552
            else None
553
        return instruments if instruments else []
554
555
    @security.private
556
    def _getAvailableMethodsDisplayList(self):
557
        """ Returns a DisplayList with the available Methods
558
            registered in Bika-Setup. Only active Methods and those
559
            with Manual Entry field active are fetched.
560
            Used to fill the Methods MultiSelectionWidget when 'Allow
561
            Instrument Entry of Results is not selected'.
562
        """
563
        bsc = getToolByName(self, 'bika_setup_catalog')
564
        items = [(i.UID, i.Title)
565
                 for i in bsc(portal_type='Method',
566
                              is_active=True)
567
                 if i.getObject().isManualEntryOfResults()]
568
        items.sort(lambda x, y: cmp(x[1], y[1]))
569
        items.insert(0, ('', _("None")))
570
        return DisplayList(list(items))
571
572
    @security.private
573
    def _getAvailableCalculationsDisplayList(self):
574
        """ Returns a DisplayList with the available Calculations
575
            registered in Bika-Setup. Only active Calculations are
576
            fetched. Used to fill the Calculation field
577
        """
578
        bsc = getToolByName(self, 'bika_setup_catalog')
579
        items = [(i.UID, i.Title)
580
                 for i in bsc(portal_type='Calculation',
581
                              is_active=True)]
582
        items.sort(lambda x, y: cmp(x[1], y[1]))
583
        items.insert(0, ('', _("None")))
584
        return DisplayList(list(items))
585
586 View Code Duplication
    @security.private
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
587
    def _getAvailableInstrumentsDisplayList(self):
588
        """ Returns a DisplayList with the available Instruments
589
            registered in Bika-Setup. Only active Instruments are
590
            fetched. Used to fill the Instruments MultiSelectionWidget
591
        """
592
        bsc = getToolByName(self, 'bika_setup_catalog')
593
        items = [(i.UID, i.Title)
594
                 for i in bsc(portal_type='Instrument',
595
                              is_active=True)]
596
        items.sort(lambda x, y: cmp(x[1], y[1]))
597
        return DisplayList(list(items))
598
599
    @security.public
600
    def getServiceDependencies(self):
601
        """
602
        This methods returns a list with the analyses services dependencies.
603
        :return: a list of analysis services objects.
604
        """
605
        calc = self.getCalculation()
606
        if calc:
607
            return calc.getCalculationDependencies(flat=True)
608
        return []
609
610
    @security.public
611
    def getServiceDependenciesUIDs(self):
612
        """
613
        This methods returns a list with the service dependencies UIDs
614
        :return: a list of uids
615
        """
616
        deps = self.getServiceDependencies()
617
        deps_uids = [service.UID() for service in deps]
618
        return deps_uids
619
620
    @security.public
621
    def getServiceDependants(self):
622
        bsc = getToolByName(self, 'bika_setup_catalog')
623
        active_calcs = bsc(portal_type='Calculation', is_active=True)
624
        calculations = [c.getObject() for c in active_calcs]
625
        dependants = []
626
        for calc in calculations:
627
            calc_dependants = calc.getDependentServices()
628
            if self in calc_dependants:
629
                calc_dependencies = calc.getCalculationDependants()
630
                dependants = dependants + calc_dependencies
631
        dependants = list(set(dependants))
632
        if self in dependants:
633
            dependants.remove(self)
634
        return dependants
635
636
    @security.public
637
    def getServiceDependantsUIDs(self):
638
        deps = self.getServiceDependants()
639
        deps_uids = [service.UID() for service in deps]
640
        return deps_uids
641
642
    @security.public
643
    def after_deactivate_transition_event(self):
644
        """Method triggered after a 'deactivate' transition for the current
645
        AnalysisService is performed. Removes this service from the Analysis
646
        Profiles or Analysis Request Templates where is assigned.
647
        This function is called automatically by
648
        bika.lims.workflow.AfterTransitionEventHandler
649
        """
650
        # Remove the service from profiles to which is assigned
651
        profiles = self.getBackReferences('AnalysisProfileAnalysisService')
652
        for profile in profiles:
653
            profile.remove_service(self)
654
655
        # Remove the service from templates to which is assigned
656
        bsc = api.get_tool('bika_setup_catalog')
657
        templates = bsc(portal_type='ARTemplate')
658
        for template in templates:
659
            template = api.get_object(template)
660
            template.remove_service(self)
661
662
663
registerType(AnalysisService, PROJECTNAME)
664