Passed
Push — master ( 9f75f8...b2bee7 )
by Jordi
07:35
created

ResultsRangeDict.error()   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 3
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
# 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
from AccessControl import ClassSecurityInfo
22
from bika.lims import api
23
from bika.lims import bikaMessageFactory as _
24
from bika.lims.browser.fields import UIDReferenceField
25
from bika.lims.browser.widgets import AnalysisSpecificationWidget
26
from bika.lims.config import PROJECTNAME
27
from bika.lims.content.bikaschema import BikaSchema
28
from bika.lims.interfaces import IAnalysisSpec, IDeactivable
29
from Products.Archetypes import atapi
30
from Products.Archetypes.public import BaseFolder
31
from Products.Archetypes.public import ComputedField
32
from Products.Archetypes.public import ComputedWidget
33
from Products.Archetypes.public import ReferenceWidget
34
from Products.Archetypes.public import Schema
35
from Products.Archetypes.utils import DisplayList
36
from Products.ATContentTypes.lib.historyaware import HistoryAwareMixin
37
from Products.ATExtensions.field.records import RecordsField
38
from Products.CMFCore.utils import getToolByName
39
from Products.CMFPlone.utils import safe_unicode
40
from zope.i18n import translate
41
from zope.interface import implements
42
43
44
schema = Schema((
45
46
    UIDReferenceField(
47
        'SampleType',
48
        vocabulary="getSampleTypes",
49
        allowed_types=('SampleType',),
50
        widget=ReferenceWidget(
51
            checkbox_bound=0,
52
            label=_("Sample Type"),
53
        ),
54
    ),
55
56
    ComputedField(
57
        'SampleTypeTitle',
58
        expression="context.getSampleType().Title() if context.getSampleType() else ''",
59
        widget=ComputedWidget(
60
            visible=False,
61
        ),
62
    ),
63
64
    ComputedField(
65
        'SampleTypeUID',
66
        expression="context.getSampleType().UID() if context.getSampleType() else ''",
67
        widget=ComputedWidget(
68
            visible=False,
69
        ),
70
    ),
71
)) + BikaSchema.copy() + Schema((
72
73
    RecordsField(
74
        'ResultsRange',
75
        # schemata = 'Specifications',
76
        required=1,
77
        type='resultsrange',
78
        subfields=(
79
            'keyword',
80
            'min_operator',
81
            'min',
82
            'max_operator',
83
            'max',
84
            'warn_min',
85
            'warn_max',
86
            'hidemin',
87
            'hidemax',
88
            'rangecomment'
89
        ),
90
        required_subfields=('keyword',),
91
        subfield_validators={
92
            'min': 'analysisspecs_validator',
93
            'max': 'analysisspecs_validator',
94
        },
95
        subfield_labels={
96
            'keyword': _('Analysis Service'),
97
            'min_operator': _('Min operator'),
98
            'min': _('Min'),
99
            'max_operator': _('Max operator'),
100
            'max': _('Max'),
101
            'warn_min': _('Min warn'),
102
            'warn_max': _('Max warn'),
103
            'hidemin': _('< Min'),
104
            'hidemax': _('> Max'),
105
            'rangecomment': _('Range Comment'),
106
        },
107
        widget=AnalysisSpecificationWidget(
108
            checkbox_bound=0,
109
            label=_("Specifications"),
110
            description=_(
111
                "'Min' and 'Max' values indicate a valid results range. Any "
112
                "result outside this results range will raise an alert. 'Min "
113
                "warn' and 'Max warn' values indicate a shoulder range. Any "
114
                "result outside the results range but within the shoulder "
115
                "range will raise a less severe alert. If the result is out of "
116
                "range, the value set for '< Min' or '< Max' will be displayed "
117
                "in lists and results reports instead of the real result.")
118
        ),
119
    ),
120
121
    ComputedField(
122
        'ClientUID',
123
        expression="context.aq_parent.UID()",
124
        widget=ComputedWidget(
125
            visible=False,
126
        ),
127
    ),
128
))
129
130
schema['description'].widget.visible = True
131
schema['title'].required = True
132
133
134
class AnalysisSpec(BaseFolder, HistoryAwareMixin):
135
    """Analysis Specification
136
    """
137
    implements(IAnalysisSpec, IDeactivable)
138
    security = ClassSecurityInfo()
139
    schema = schema
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable schema does not seem to be defined.
Loading history...
140
    displayContentsTab = False
141
142
    _at_rename_after_creation = True
143
144
    def _renameAfterCreation(self, check_auto_id=False):
145
        from bika.lims.idserver import renameAfterCreation
146
        renameAfterCreation(self)
147
148
    def Title(self):
149
        """ Return the title if possible, else return the Sample type.
150
        Fall back on the instance's ID if there's no sample type or title.
151
        """
152
        title = ''
153
        if self.title:
154
            title = self.title
155
        else:
156
            sampletype = self.getSampleType()
157
            if sampletype:
158
                title = sampletype.Title()
159
        return safe_unicode(title).encode('utf-8')
160
161
    def contextual_title(self):
162
        parent = self.aq_parent
163
        if parent == self.bika_setup.bika_analysisspecs:
164
            return self.title + " (" + translate(_("Lab")) + ")"
165
        else:
166
            return self.title + " (" + translate(_("Client")) + ")"
167
168
    security.declarePublic('getResultsRangeDict')
169
170
    def getResultsRangeDict(self):
171
        """Return a dictionary with the specification fields for each
172
           service. The keys of the dictionary are the keywords of each
173
           analysis service. Each service contains a dictionary in which
174
           each key is the name of the spec field:
175
           specs['keyword'] = {'min': value,
176
                               'max': value,
177
                               'warnmin': value,
178
                               ... }
179
        """
180
        specs = {}
181
        subfields = self.Schema()['ResultsRange'].subfields
182
        for spec in self.getResultsRange():
183
            keyword = spec['keyword']
184
            specs[keyword] = {}
185
            for key in subfields:
186
                if key not in ['uid', 'keyword']:
187
                    specs[keyword][key] = spec.get(key, '')
188
        return specs
189
190
    security.declarePublic('getRemainingSampleTypes')
191
192
    def getSampleTypes(self, active_only=True):
193
        """Return all sampletypes
194
        """
195
        catalog = api.get_tool("bika_setup_catalog")
196
        query = {
197
            "portal_type": "SampleType",
198
            # N.B. The `sortable_title` index sorts case sensitive. Since there
199
            #      is no sort key for sample types, it makes more sense to sort
200
            #      them alphabetically in the selection
201
            "sort_on": "title",
202
            "sort_order": "ascending"
203
        }
204
        results = catalog(query)
205
        if active_only:
206
            results = filter(api.is_active, results)
207
        sampletypes = map(
208
            lambda brain: (brain.UID, brain.Title), results)
209
        return DisplayList(sampletypes)
210
211
    def getClientUID(self):
212
        return self.aq_parent.UID()
213
214
215
atapi.registerType(AnalysisSpec, PROJECTNAME)
216
217
218
class ResultsRangeDict(dict):
219
220
    def __init__(self, *arg, **kw):
221
        super(ResultsRangeDict, self).__init__(*arg, **kw)
222
        self["min"] = self.min
223
        self["max"] = self.max
224
        self["error"] = self.error
225
        self["warn_min"] = self.warn_min
226
        self["warn_max"] = self.warn_max
227
        self["min_operator"] = self.min_operator
228
        self["max_operator"] = self.max_operator
229
230
    @property
231
    def min(self):
232
        return self.get("min", '')
233
234
    @property
235
    def max(self):
236
        return self.get("max", '')
237
238
    @property
239
    def error(self):
240
        return self.get("error", '')
241
242
    @property
243
    def warn_min(self):
244
        return self.get("warn_min", self.min)
245
246
    @property
247
    def warn_max(self):
248
        return self.get('warn_max', self.max)
249
250
    @property
251
    def min_operator(self):
252
        return self.get('min_operator', 'geq')
253
254
    @property
255
    def max_operator(self):
256
        return self.get('max_operator', 'leq')
257
258
    @min.setter
259
    def min(self, value):
260
        self["min"] = value
261
262
    @max.setter
263
    def max(self, value):
264
        self["max"] = value
265
266
    @warn_min.setter
267
    def warn_min(self, value):
268
        self['warn_min'] = value
269
270
    @warn_max.setter
271
    def warn_max(self, value):
272
        self['warn_max'] = value
273
274
    @min_operator.setter
275
    def min_operator(self, value):
276
        self['min_operator'] = value
277
278
    @max_operator.setter
279
    def max_operator(self, value):
280
        self['max_operator'] = value
281