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