|
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
|
|
|
import re |
|
9
|
|
|
import sys |
|
10
|
|
|
|
|
11
|
|
|
from AccessControl import ClassSecurityInfo |
|
12
|
|
|
from Products.ATContentTypes.lib.historyaware import HistoryAwareMixin |
|
13
|
|
|
from Products.ATExtensions.ateapi import RecordsField |
|
14
|
|
|
from Products.Archetypes.public import (BaseFolder, DisplayList, |
|
15
|
|
|
ReferenceField, Schema, |
|
16
|
|
|
SelectionWidget, StringField, |
|
17
|
|
|
registerType) |
|
18
|
|
|
from Products.Archetypes.references import HoldingReference |
|
19
|
|
|
from Products.CMFCore.utils import getToolByName |
|
20
|
|
|
from Products.CMFPlone.utils import _createObjectByType, safe_unicode |
|
21
|
|
|
from bika.lims import api, logger |
|
22
|
|
|
from bika.lims import bikaMessageFactory as _ |
|
23
|
|
|
from bika.lims.browser.fields import UIDReferenceField |
|
24
|
|
|
from bika.lims.browser.fields.remarksfield import RemarksField |
|
25
|
|
|
from bika.lims.browser.widgets import RemarksWidget |
|
26
|
|
|
from bika.lims.catalog.analysis_catalog import CATALOG_ANALYSIS_LISTING |
|
27
|
|
|
from bika.lims.config import PROJECTNAME, WORKSHEET_LAYOUT_OPTIONS |
|
28
|
|
|
from bika.lims.content.bikaschema import BikaSchema |
|
29
|
|
|
from bika.lims.idserver import renameAfterCreation |
|
30
|
|
|
from bika.lims.interfaces import (IAnalysisRequest, IDuplicateAnalysis, |
|
31
|
|
|
IReferenceAnalysis, IReferenceSample, |
|
32
|
|
|
IRoutineAnalysis, IWorksheet) |
|
33
|
|
|
from bika.lims.interfaces.analysis import IRequestAnalysis |
|
34
|
|
|
from bika.lims.permissions import EditWorksheet, ManageWorksheets |
|
35
|
|
|
from bika.lims.utils import changeWorkflowState, tmpID, to_int |
|
36
|
|
|
from bika.lims.utils import to_utf8 as _c |
|
37
|
|
|
from bika.lims.workflow import doActionFor, skip, isTransitionAllowed, \ |
|
38
|
|
|
ActionHandlerPool, push_reindex_to_actions_pool |
|
39
|
|
|
from zope.interface import implements |
|
40
|
|
|
|
|
41
|
|
|
ALL_ANALYSES_TYPES = "all" |
|
42
|
|
|
ALLOWED_ANALYSES_TYPES = ["a", "b", "c", "d"] |
|
43
|
|
|
|
|
44
|
|
|
|
|
45
|
|
|
schema = BikaSchema.copy() + Schema(( |
|
46
|
|
|
|
|
47
|
|
|
UIDReferenceField( |
|
48
|
|
|
'WorksheetTemplate', |
|
49
|
|
|
allowed_types=('WorksheetTemplate',), |
|
50
|
|
|
), |
|
51
|
|
|
|
|
52
|
|
|
RecordsField( |
|
53
|
|
|
'Layout', |
|
54
|
|
|
required=1, |
|
55
|
|
|
subfields=('position', 'type', 'container_uid', 'analysis_uid'), |
|
56
|
|
|
subfield_types={'position': 'int'}, |
|
57
|
|
|
), |
|
58
|
|
|
|
|
59
|
|
|
# all layout info lives in Layout; Analyses is used for back references. |
|
60
|
|
|
ReferenceField( |
|
61
|
|
|
'Analyses', |
|
62
|
|
|
required=1, |
|
63
|
|
|
multiValued=1, |
|
64
|
|
|
allowed_types=('Analysis', 'DuplicateAnalysis', 'ReferenceAnalysis', 'RejectAnalysis'), |
|
65
|
|
|
relationship='WorksheetAnalysis', |
|
66
|
|
|
), |
|
67
|
|
|
|
|
68
|
|
|
StringField( |
|
69
|
|
|
'Analyst', |
|
70
|
|
|
searchable=True, |
|
71
|
|
|
), |
|
72
|
|
|
|
|
73
|
|
|
ReferenceField( |
|
74
|
|
|
'Method', |
|
75
|
|
|
required=0, |
|
76
|
|
|
vocabulary_display_path_bound=sys.maxint, |
|
77
|
|
|
vocabulary='_getMethodsVoc', |
|
78
|
|
|
allowed_types=('Method',), |
|
79
|
|
|
relationship='WorksheetMethod', |
|
80
|
|
|
referenceClass=HoldingReference, |
|
81
|
|
|
widget=SelectionWidget( |
|
82
|
|
|
format='select', |
|
83
|
|
|
label=_("Method"), |
|
84
|
|
|
visible=False, |
|
85
|
|
|
), |
|
86
|
|
|
), |
|
87
|
|
|
|
|
88
|
|
|
# TODO Remove. Instruments must be assigned directly to each analysis. |
|
89
|
|
|
ReferenceField( |
|
90
|
|
|
'Instrument', |
|
91
|
|
|
required=0, |
|
92
|
|
|
allowed_types=('Instrument',), |
|
93
|
|
|
vocabulary='_getInstrumentsVoc', |
|
94
|
|
|
relationship='WorksheetInstrument', |
|
95
|
|
|
referenceClass=HoldingReference, |
|
96
|
|
|
), |
|
97
|
|
|
|
|
98
|
|
|
RemarksField( |
|
99
|
|
|
'Remarks', |
|
100
|
|
|
searchable=True, |
|
101
|
|
|
widget=RemarksWidget( |
|
102
|
|
|
label=_("Remarks"), |
|
103
|
|
|
), |
|
104
|
|
|
), |
|
105
|
|
|
|
|
106
|
|
|
StringField( |
|
107
|
|
|
'ResultsLayout', |
|
108
|
|
|
default='1', |
|
109
|
|
|
vocabulary=WORKSHEET_LAYOUT_OPTIONS, |
|
110
|
|
|
), |
|
111
|
|
|
), |
|
112
|
|
|
) |
|
113
|
|
|
|
|
114
|
|
|
schema['id'].required = 0 |
|
115
|
|
|
schema['id'].widget.visible = False |
|
116
|
|
|
schema['title'].required = 0 |
|
117
|
|
|
schema['title'].widget.visible = {'edit': 'hidden', 'view': 'invisible'} |
|
118
|
|
|
|
|
119
|
|
|
|
|
120
|
|
|
class Worksheet(BaseFolder, HistoryAwareMixin): |
|
121
|
|
|
"""A worksheet is a logical group of Analyses accross ARs |
|
122
|
|
|
""" |
|
123
|
|
|
security = ClassSecurityInfo() |
|
124
|
|
|
implements(IWorksheet) |
|
125
|
|
|
displayContentsTab = False |
|
126
|
|
|
schema = schema |
|
|
|
|
|
|
127
|
|
|
|
|
128
|
|
|
_at_rename_after_creation = True |
|
129
|
|
|
|
|
130
|
|
|
def _renameAfterCreation(self, check_auto_id=False): |
|
131
|
|
|
from bika.lims.idserver import renameAfterCreation |
|
132
|
|
|
renameAfterCreation(self) |
|
133
|
|
|
|
|
134
|
|
|
def Title(self): |
|
135
|
|
|
return safe_unicode(self.getId()).encode('utf-8') |
|
136
|
|
|
|
|
137
|
|
|
def setLayout(self, value): |
|
138
|
|
|
""" |
|
139
|
|
|
Sets the worksheet layout, keeping it sorted by position |
|
140
|
|
|
:param value: the layout to set |
|
141
|
|
|
""" |
|
142
|
|
|
new_layout = sorted(value, key=lambda k: k['position']) |
|
143
|
|
|
self.getField('Layout').set(self, new_layout) |
|
144
|
|
|
|
|
145
|
|
|
def addAnalyses(self, analyses): |
|
146
|
|
|
"""Adds a collection of analyses to the Worksheet at once |
|
147
|
|
|
""" |
|
148
|
|
|
actions_pool = ActionHandlerPool.get_instance() |
|
149
|
|
|
actions_pool.queue_pool() |
|
150
|
|
|
for analysis in analyses: |
|
151
|
|
|
self.addAnalysis(api.get_object(analysis)) |
|
152
|
|
|
actions_pool.resume() |
|
153
|
|
|
|
|
154
|
|
|
def addAnalysis(self, analysis, position=None): |
|
155
|
|
|
"""- add the analysis to self.Analyses(). |
|
156
|
|
|
- position is overruled if a slot for this analysis' parent exists |
|
157
|
|
|
- if position is None, next available pos is used. |
|
158
|
|
|
""" |
|
159
|
|
|
|
|
160
|
|
|
# TODO Workflow - Move these initial checks to the assign guard |
|
161
|
|
|
# Cannot add an analysis if not open, unless a retest |
|
162
|
|
|
if api.get_review_status(self) != "open": |
|
163
|
|
|
retracted = analysis.getRetestOf() |
|
164
|
|
|
if retracted not in self.getAnalyses(): |
|
165
|
|
|
return |
|
166
|
|
|
|
|
167
|
|
|
# Cannot add an analysis that is assigned already |
|
168
|
|
|
if analysis.getWorksheet(): |
|
169
|
|
|
return |
|
170
|
|
|
|
|
171
|
|
|
# Just in case |
|
172
|
|
|
analyses = self.getAnalyses() |
|
173
|
|
|
if analysis in analyses: |
|
174
|
|
|
analyses = filter(lambda an: an != analysis, analyses) |
|
175
|
|
|
self.setAnalyses(analyses) |
|
176
|
|
|
self.updateLayout() |
|
177
|
|
|
|
|
178
|
|
|
# Cannot add an analysis if the assign transition is not possible |
|
179
|
|
|
# We need to bypass the guard's check for current context! |
|
180
|
|
|
api.get_request().set("ws_uid", api.get_uid(self)) |
|
181
|
|
|
if not isTransitionAllowed(analysis, "assign"): |
|
182
|
|
|
return |
|
183
|
|
|
|
|
184
|
|
|
# Assign the instrument from the worksheet to the analysis, if possible |
|
185
|
|
|
instrument = self.getInstrument() |
|
186
|
|
|
if instrument and analysis.isInstrumentAllowed(instrument): |
|
187
|
|
|
# TODO Analysis Instrument + Method assignment |
|
188
|
|
|
methods = instrument.getMethods() |
|
189
|
|
|
if methods: |
|
190
|
|
|
# Set the first method assigned to the selected instrument |
|
191
|
|
|
analysis.setMethod(methods[0]) |
|
192
|
|
|
analysis.setInstrument(instrument) |
|
193
|
|
|
elif not instrument: |
|
194
|
|
|
# If the ws doesn't have an instrument try to set the method |
|
195
|
|
|
method = self.getMethod() |
|
196
|
|
|
if method and analysis.isMethodAllowed(method): |
|
197
|
|
|
analysis.setMethod(method) |
|
198
|
|
|
|
|
199
|
|
|
# Transition analysis to "assigned" |
|
200
|
|
|
actions_pool = ActionHandlerPool.get_instance() |
|
201
|
|
|
actions_pool.queue_pool() |
|
202
|
|
|
doActionFor(analysis, "assign") |
|
203
|
|
|
self.setAnalyses(analyses + [analysis]) |
|
204
|
|
|
self.addToLayout(analysis, position) |
|
205
|
|
|
|
|
206
|
|
|
# Try to rollback the worksheet to prevent inconsistencies |
|
207
|
|
|
doActionFor(self, "rollback_to_open") |
|
208
|
|
|
|
|
209
|
|
|
# Reindex Worksheet |
|
210
|
|
|
idxs = ["getAnalysesUIDs"] |
|
211
|
|
|
push_reindex_to_actions_pool(self, idxs=idxs) |
|
212
|
|
|
|
|
213
|
|
|
# Reindex Analysis Request, if any |
|
214
|
|
|
if IRequestAnalysis.providedBy(analysis): |
|
215
|
|
|
idxs = ['assigned_state', 'getDueDate'] |
|
216
|
|
|
push_reindex_to_actions_pool(analysis.getRequest(), idxs=idxs) |
|
217
|
|
|
|
|
218
|
|
|
# Resume the actions pool |
|
219
|
|
|
actions_pool.resume() |
|
220
|
|
|
|
|
221
|
|
|
def removeAnalysis(self, analysis): |
|
222
|
|
|
""" Unassigns the analysis passed in from the worksheet. |
|
223
|
|
|
Delegates to 'unassign' transition for the analysis passed in |
|
224
|
|
|
""" |
|
225
|
|
|
# We need to bypass the guard's check for current context! |
|
226
|
|
|
api.get_request().set("ws_uid", api.get_uid(self)) |
|
227
|
|
|
if analysis.getWorksheet() == self: |
|
228
|
|
|
doActionFor(analysis, "unassign") |
|
229
|
|
|
|
|
230
|
|
|
def addToLayout(self, analysis, position=None): |
|
231
|
|
|
""" Adds the analysis passed in to the worksheet's layout |
|
232
|
|
|
""" |
|
233
|
|
|
# TODO Redux |
|
234
|
|
|
layout = self.getLayout() |
|
235
|
|
|
container_uid = self.get_container_for(analysis) |
|
236
|
|
|
if IRequestAnalysis.providedBy(analysis) and \ |
|
237
|
|
|
not IDuplicateAnalysis.providedBy(analysis): |
|
238
|
|
|
container_uids = map(lambda slot: slot['container_uid'], layout) |
|
239
|
|
|
if container_uid in container_uids: |
|
240
|
|
|
position = [int(slot['position']) for slot in layout if |
|
241
|
|
|
slot['container_uid'] == container_uid][0] |
|
242
|
|
|
elif not position: |
|
243
|
|
|
used_positions = [0, ] + [int(slot['position']) for slot in |
|
244
|
|
|
layout] |
|
245
|
|
|
position = [pos for pos in range(1, max(used_positions) + 2) |
|
246
|
|
|
if pos not in used_positions][0] |
|
247
|
|
|
|
|
248
|
|
|
an_type = self.get_analysis_type(analysis) |
|
249
|
|
|
self.setLayout(layout + [{'position': position, |
|
250
|
|
|
'type': an_type, |
|
251
|
|
|
'container_uid': container_uid, |
|
252
|
|
|
'analysis_uid': api.get_uid(analysis)}, ]) |
|
253
|
|
|
|
|
254
|
|
|
def purgeLayout(self): |
|
255
|
|
|
""" Purges the layout of not assigned analyses |
|
256
|
|
|
""" |
|
257
|
|
|
uids = map(api.get_uid, self.getAnalyses()) |
|
258
|
|
|
layout = filter(lambda slot: slot.get("analysis_uid", None) in uids, |
|
259
|
|
|
self.getLayout()) |
|
260
|
|
|
self.setLayout(layout) |
|
261
|
|
|
|
|
262
|
|
|
def _getMethodsVoc(self): |
|
263
|
|
|
""" |
|
264
|
|
|
This function returns the registered methods in the system as a |
|
265
|
|
|
vocabulary. |
|
266
|
|
|
""" |
|
267
|
|
|
bsc = getToolByName(self, 'bika_setup_catalog') |
|
268
|
|
|
items = [(i.UID, i.Title) |
|
269
|
|
|
for i in bsc(portal_type='Method', |
|
270
|
|
|
is_active=True)] |
|
271
|
|
|
items.sort(lambda x, y: cmp(x[1], y[1])) |
|
272
|
|
|
items.insert(0, ('', _("Not specified"))) |
|
273
|
|
|
return DisplayList(list(items)) |
|
274
|
|
|
|
|
275
|
|
|
def _getInstrumentsVoc(self): |
|
276
|
|
|
""" |
|
277
|
|
|
This function returns the registered instruments in the system as a |
|
278
|
|
|
vocabulary. The instruments are filtered by the selected method. |
|
279
|
|
|
""" |
|
280
|
|
|
cfilter = {'portal_type': 'Instrument', 'is_active': True} |
|
281
|
|
|
if self.getMethod(): |
|
282
|
|
|
cfilter['getMethodUIDs'] = {"query": self.getMethod().UID(), |
|
283
|
|
|
"operator": "or"} |
|
284
|
|
|
bsc = getToolByName(self, 'bika_setup_catalog') |
|
285
|
|
|
items = [('', 'No instrument')] + [ |
|
286
|
|
|
(o.UID, o.Title) for o in |
|
287
|
|
|
bsc(cfilter)] |
|
288
|
|
|
o = self.getInstrument() |
|
289
|
|
|
if o and o.UID() not in [i[0] for i in items]: |
|
290
|
|
|
items.append((o.UID(), o.Title())) |
|
291
|
|
|
items.sort(lambda x, y: cmp(x[1], y[1])) |
|
292
|
|
|
return DisplayList(list(items)) |
|
293
|
|
|
|
|
294
|
|
|
def addReferenceAnalyses(self, reference, services, slot=None): |
|
295
|
|
|
""" Creates and add reference analyses to the slot by using the |
|
296
|
|
|
reference sample and service uids passed in. |
|
297
|
|
|
If no destination slot is defined, the most suitable slot will be used, |
|
298
|
|
|
typically a new slot at the end of the worksheet will be added. |
|
299
|
|
|
:param reference: reference sample to which ref analyses belong |
|
300
|
|
|
:param service_uids: he uid of the services to create analyses from |
|
301
|
|
|
:param slot: slot where reference analyses must be stored |
|
302
|
|
|
:return: the list of reference analyses added |
|
303
|
|
|
""" |
|
304
|
|
|
service_uids = list() |
|
305
|
|
|
for service in services: |
|
306
|
|
|
if api.is_uid(service): |
|
307
|
|
|
service_uids.append(service) |
|
308
|
|
|
else: |
|
309
|
|
|
service_uids.append(api.get_uid(service)) |
|
310
|
|
|
service_uids = list(set(service_uids)) |
|
311
|
|
|
|
|
312
|
|
|
# Cannot add a reference analysis if not open |
|
313
|
|
|
if api.get_workflow_status_of(self) != "open": |
|
314
|
|
|
return [] |
|
315
|
|
|
|
|
316
|
|
|
slot_to = to_int(slot) |
|
317
|
|
|
if slot_to < 0: |
|
318
|
|
|
return [] |
|
319
|
|
|
|
|
320
|
|
|
if not slot_to: |
|
321
|
|
|
# Find the suitable slot to add these references |
|
322
|
|
|
slot_to = self.get_suitable_slot_for_reference(reference) |
|
323
|
|
|
return self.addReferenceAnalyses(reference, service_uids, slot_to) |
|
324
|
|
|
|
|
325
|
|
|
processed = list() |
|
326
|
|
|
for analysis in self.get_analyses_at(slot_to): |
|
327
|
|
|
if api.get_review_status(analysis) != "retracted": |
|
328
|
|
|
service = analysis.getAnalysisService() |
|
329
|
|
|
processed.append(api.get_uid(service)) |
|
330
|
|
|
query = dict(portal_type="AnalysisService", UID=service_uids, |
|
331
|
|
|
sort_on="sortable_title") |
|
332
|
|
|
services = filter(lambda service: api.get_uid(service) not in processed, |
|
333
|
|
|
api.search(query, "bika_setup_catalog")) |
|
334
|
|
|
|
|
335
|
|
|
# Ref analyses from the same slot must have the same group id |
|
336
|
|
|
ref_gid = self.nextRefAnalysesGroupID(reference) |
|
337
|
|
|
ref_analyses = list() |
|
338
|
|
|
for service in services: |
|
339
|
|
|
service_obj = api.get_object(service) |
|
340
|
|
|
ref_analysis = self.add_reference_analysis(reference, service_obj, |
|
341
|
|
|
slot_to, ref_gid) |
|
342
|
|
|
if not ref_analysis: |
|
343
|
|
|
continue |
|
344
|
|
|
ref_analyses.append(ref_analysis) |
|
345
|
|
|
return ref_analyses |
|
346
|
|
|
|
|
347
|
|
|
def add_reference_analysis(self, reference, service, slot, ref_gid=None): |
|
348
|
|
|
""" |
|
349
|
|
|
Creates a reference analysis in the destination slot (dest_slot) passed |
|
350
|
|
|
in, by using the reference and service_uid. If the analysis |
|
351
|
|
|
passed in is not an IReferenceSample or has dependent services, returns |
|
352
|
|
|
None. If no reference analyses group id (refgid) is set, the value will |
|
353
|
|
|
be generated automatically. |
|
354
|
|
|
:param reference: reference sample to create an analysis from |
|
355
|
|
|
:param service: the service object to create an analysis from |
|
356
|
|
|
:param slot: slot where the reference analysis must be stored |
|
357
|
|
|
:param refgid: the reference analyses group id to be set |
|
358
|
|
|
:return: the reference analysis or None |
|
359
|
|
|
""" |
|
360
|
|
|
if not reference or not service: |
|
361
|
|
|
return None |
|
362
|
|
|
|
|
363
|
|
|
if not IReferenceSample.providedBy(reference): |
|
364
|
|
|
logger.warning('Cannot create reference analysis from a non ' |
|
365
|
|
|
'reference sample: {}'.format(reference.getId())) |
|
366
|
|
|
return None |
|
367
|
|
|
|
|
368
|
|
|
calc = service.getCalculation() |
|
369
|
|
|
if calc and calc.getDependentServices(): |
|
370
|
|
|
logger.warning('Cannot create reference analyses with dependent' |
|
371
|
|
|
'services: {}'.format(service.getId())) |
|
372
|
|
|
return None |
|
373
|
|
|
|
|
374
|
|
|
# Create the reference analysis |
|
375
|
|
|
ref_analysis = reference.addReferenceAnalysis(service) |
|
376
|
|
|
if not ref_analysis: |
|
377
|
|
|
logger.warning("Unable to create a reference analysis for " |
|
378
|
|
|
"reference '{0}' and service '{1}'" |
|
379
|
|
|
.format(reference.getId(), service.getKeyword())) |
|
380
|
|
|
return None |
|
381
|
|
|
|
|
382
|
|
|
# Set ReferenceAnalysesGroupID (same id for the analyses from |
|
383
|
|
|
# the same Reference Sample and same Worksheet) |
|
384
|
|
|
gid = ref_gid and ref_gid or self.nextRefAnalysesGroupID(reference) |
|
385
|
|
|
ref_analysis.setReferenceAnalysesGroupID(gid) |
|
386
|
|
|
|
|
387
|
|
|
# Add the reference analysis into the worksheet |
|
388
|
|
|
self.setAnalyses(self.getAnalyses() + [ref_analysis, ]) |
|
389
|
|
|
self.addToLayout(ref_analysis, slot) |
|
390
|
|
|
|
|
391
|
|
|
# Reindex |
|
392
|
|
|
ref_analysis.reindexObject(idxs=["getAnalyst", "getWorksheetUID", |
|
393
|
|
|
"getReferenceAnalysesGroupID"]) |
|
394
|
|
|
self.reindexObject(idxs=["getAnalysesUIDs"]) |
|
395
|
|
|
return ref_analysis |
|
396
|
|
|
|
|
397
|
|
|
def nextRefAnalysesGroupID(self, reference): |
|
398
|
|
|
""" Returns the next ReferenceAnalysesGroupID for the given reference |
|
399
|
|
|
sample. Gets the last reference analysis registered in the system |
|
400
|
|
|
for the specified reference sample and increments in one unit the |
|
401
|
|
|
suffix. |
|
402
|
|
|
""" |
|
403
|
|
|
prefix = reference.id + "-" |
|
404
|
|
|
if not IReferenceSample.providedBy(reference): |
|
405
|
|
|
# Not a ReferenceSample, so this is a duplicate |
|
406
|
|
|
prefix = reference.id + "-D" |
|
407
|
|
|
bac = getToolByName(reference, 'bika_analysis_catalog') |
|
408
|
|
|
ids = bac.Indexes['getReferenceAnalysesGroupID'].uniqueValues() |
|
409
|
|
|
rr = re.compile("^" + prefix + "[\d+]+$") |
|
410
|
|
|
ids = [int(i.split(prefix)[1]) for i in ids if i and rr.match(i)] |
|
411
|
|
|
ids.sort() |
|
412
|
|
|
_id = ids[-1] if ids else 0 |
|
413
|
|
|
suffix = str(_id + 1).zfill(int(3)) |
|
414
|
|
|
if not IReferenceSample.providedBy(reference): |
|
415
|
|
|
# Not a ReferenceSample, so this is a duplicate |
|
416
|
|
|
suffix = str(_id + 1).zfill(2) |
|
417
|
|
|
return '%s%s' % (prefix, suffix) |
|
418
|
|
|
|
|
419
|
|
|
def addDuplicateAnalyses(self, src_slot, dest_slot=None): |
|
420
|
|
|
""" Creates and add duplicate analyes from the src_slot to the dest_slot |
|
421
|
|
|
If no destination slot is defined, the most suitable slot will be used, |
|
422
|
|
|
typically a new slot at the end of the worksheet will be added. |
|
423
|
|
|
:param src_slot: slot that contains the analyses to duplicate |
|
424
|
|
|
:param dest_slot: slot where the duplicate analyses must be stored |
|
425
|
|
|
:return: the list of duplicate analyses added |
|
426
|
|
|
""" |
|
427
|
|
|
# Duplicate analyses can only be added if the state of the ws is open |
|
428
|
|
|
# unless we are adding a retest |
|
429
|
|
|
if api.get_workflow_status_of(self) != "open": |
|
430
|
|
|
return [] |
|
431
|
|
|
|
|
432
|
|
|
slot_from = to_int(src_slot, 0) |
|
433
|
|
|
if slot_from < 1: |
|
434
|
|
|
return [] |
|
435
|
|
|
|
|
436
|
|
|
slot_to = to_int(dest_slot, 0) |
|
437
|
|
|
if slot_to < 0: |
|
438
|
|
|
return [] |
|
439
|
|
|
|
|
440
|
|
|
if not slot_to: |
|
441
|
|
|
# Find the suitable slot to add these duplicates |
|
442
|
|
|
slot_to = self.get_suitable_slot_for_duplicate(slot_from) |
|
443
|
|
|
return self.addDuplicateAnalyses(src_slot, slot_to) |
|
444
|
|
|
|
|
445
|
|
|
processed = map(lambda an: api.get_uid(an.getAnalysis()), |
|
446
|
|
|
self.get_analyses_at(slot_to)) |
|
447
|
|
|
src_analyses = list() |
|
448
|
|
|
for analysis in self.get_analyses_at(slot_from): |
|
449
|
|
|
if api.get_uid(analysis) in processed: |
|
450
|
|
|
if api.get_workflow_status_of(analysis) != "retracted": |
|
451
|
|
|
continue |
|
452
|
|
|
src_analyses.append(analysis) |
|
453
|
|
|
ref_gid = None |
|
454
|
|
|
duplicates = list() |
|
455
|
|
|
for analysis in src_analyses: |
|
456
|
|
|
duplicate = self.add_duplicate_analysis(analysis, slot_to, ref_gid) |
|
457
|
|
|
if not duplicate: |
|
458
|
|
|
continue |
|
459
|
|
|
# All duplicates from the same slot must have the same group id |
|
460
|
|
|
ref_gid = ref_gid or duplicate.getReferenceAnalysesGroupID() |
|
461
|
|
|
duplicates.append(duplicate) |
|
462
|
|
|
return duplicates |
|
463
|
|
|
|
|
464
|
|
|
def add_duplicate_analysis(self, src_analysis, destination_slot, |
|
465
|
|
|
ref_gid=None): |
|
466
|
|
|
""" |
|
467
|
|
|
Creates a duplicate of the src_analysis passed in. If the analysis |
|
468
|
|
|
passed in is not an IRoutineAnalysis, is retracted or has dependent |
|
469
|
|
|
services, returns None.If no reference analyses group id (ref_gid) is |
|
470
|
|
|
set, the value will be generated automatically. |
|
471
|
|
|
:param src_analysis: analysis to create a duplicate from |
|
472
|
|
|
:param destination_slot: slot where duplicate analysis must be stored |
|
473
|
|
|
:param ref_gid: the reference analysis group id to be set |
|
474
|
|
|
:return: the duplicate analysis or None |
|
475
|
|
|
""" |
|
476
|
|
|
if not src_analysis: |
|
477
|
|
|
return None |
|
478
|
|
|
|
|
479
|
|
|
if not IRoutineAnalysis.providedBy(src_analysis): |
|
480
|
|
|
logger.warning('Cannot create duplicate analysis from a non ' |
|
481
|
|
|
'routine analysis: {}'.format(src_analysis.getId())) |
|
482
|
|
|
return None |
|
483
|
|
|
|
|
484
|
|
|
if api.get_review_status(src_analysis) == 'retracted': |
|
485
|
|
|
logger.warning('Cannot create duplicate analysis from a retracted' |
|
486
|
|
|
'analysis: {}'.format(src_analysis.getId())) |
|
487
|
|
|
return None |
|
488
|
|
|
|
|
489
|
|
|
# TODO Workflow - Duplicate Analyses - Consider duplicates with deps |
|
490
|
|
|
# Removing this check from here and ensuring that duplicate.getSiblings |
|
491
|
|
|
# returns the analyses sorted by priority (duplicates from same |
|
492
|
|
|
# AR > routine analyses from same AR > duplicates from same WS > |
|
493
|
|
|
# routine analyses from same WS) should be almost enough |
|
494
|
|
|
calc = src_analysis.getCalculation() |
|
495
|
|
|
if calc and calc.getDependentServices(): |
|
496
|
|
|
logger.warning('Cannot create duplicate analysis from an' |
|
497
|
|
|
'analysis with dependent services: {}' |
|
498
|
|
|
.format(src_analysis.getId())) |
|
499
|
|
|
return None |
|
500
|
|
|
|
|
501
|
|
|
# Create the duplicate |
|
502
|
|
|
duplicate = _createObjectByType("DuplicateAnalysis", self, tmpID()) |
|
503
|
|
|
duplicate.setAnalysis(src_analysis) |
|
504
|
|
|
|
|
505
|
|
|
# Set ReferenceAnalysesGroupID (same id for the analyses from |
|
506
|
|
|
# the same Reference Sample and same Worksheet) |
|
507
|
|
|
if not ref_gid: |
|
508
|
|
|
ref_gid = self.nextRefAnalysesGroupID(duplicate.getRequest()) |
|
509
|
|
|
duplicate.setReferenceAnalysesGroupID(ref_gid) |
|
510
|
|
|
|
|
511
|
|
|
# Add the duplicate into the worksheet |
|
512
|
|
|
self.addToLayout(duplicate, destination_slot) |
|
513
|
|
|
self.setAnalyses(self.getAnalyses() + [duplicate, ]) |
|
514
|
|
|
|
|
515
|
|
|
# Reindex |
|
516
|
|
|
duplicate.reindexObject(idxs=["getAnalyst", "getWorksheetUID", |
|
517
|
|
|
"getReferenceAnalysesGroupID"]) |
|
518
|
|
|
self.reindexObject(idxs=["getAnalysesUIDs"]) |
|
519
|
|
|
return duplicate |
|
520
|
|
|
|
|
521
|
|
|
def get_suitable_slot_for_duplicate(self, src_slot): |
|
522
|
|
|
"""Returns the suitable position for a duplicate analysis, taking into |
|
523
|
|
|
account if there is a WorksheetTemplate assigned to this worksheet. |
|
524
|
|
|
|
|
525
|
|
|
By default, returns a new slot at the end of the worksheet unless there |
|
526
|
|
|
is a slot defined for a duplicate of the src_slot in the worksheet |
|
527
|
|
|
template layout not yet used. |
|
528
|
|
|
|
|
529
|
|
|
:param src_slot: |
|
530
|
|
|
:return: suitable slot position for a duplicate of src_slot |
|
531
|
|
|
""" |
|
532
|
|
|
slot_from = to_int(src_slot, 0) |
|
533
|
|
|
if slot_from < 1: |
|
534
|
|
|
return -1 |
|
535
|
|
|
|
|
536
|
|
|
# Are the analyses from src_slot suitable for duplicates creation? |
|
537
|
|
|
container = self.get_container_at(slot_from) |
|
538
|
|
|
if not container or not IAnalysisRequest.providedBy(container): |
|
539
|
|
|
# We cannot create duplicates from analyses other than routine ones, |
|
540
|
|
|
# those that belong to an Analysis Request. |
|
541
|
|
|
return -1 |
|
542
|
|
|
|
|
543
|
|
|
occupied = self.get_slot_positions(type='all') |
|
544
|
|
|
wst = self.getWorksheetTemplate() |
|
545
|
|
|
if not wst: |
|
546
|
|
|
# No worksheet template assigned, add a new slot at the end of |
|
547
|
|
|
# the worksheet with the duplicate there |
|
548
|
|
|
slot_to = max(occupied) + 1 |
|
549
|
|
|
return slot_to |
|
550
|
|
|
|
|
551
|
|
|
# If there is a match with the layout defined in the Worksheet |
|
552
|
|
|
# Template, use that slot instead of adding a new one at the end of |
|
553
|
|
|
# the worksheet |
|
554
|
|
|
layout = wst.getLayout() |
|
555
|
|
|
for pos in layout: |
|
556
|
|
|
if pos['type'] != 'd' or to_int(pos['dup']) != slot_from: |
|
557
|
|
|
continue |
|
558
|
|
|
slot_to = int(pos['pos']) |
|
559
|
|
|
if slot_to in occupied: |
|
560
|
|
|
# Not an empty slot |
|
561
|
|
|
continue |
|
562
|
|
|
|
|
563
|
|
|
# This slot is empty, use it instead of adding a new |
|
564
|
|
|
# slot at the end of the worksheet |
|
565
|
|
|
return slot_to |
|
566
|
|
|
|
|
567
|
|
|
# Add a new slot at the end of the worksheet, but take into account |
|
568
|
|
|
# that a worksheet template is assigned, so we need to take care to |
|
569
|
|
|
# not override slots defined by its layout |
|
570
|
|
|
occupied.append(len(layout)) |
|
571
|
|
|
slot_to = max(occupied) + 1 |
|
572
|
|
|
return slot_to |
|
573
|
|
|
|
|
574
|
|
|
def get_suitable_slot_for_reference(self, reference): |
|
575
|
|
|
"""Returns the suitable position for reference analyses, taking into |
|
576
|
|
|
account if there is a WorksheetTemplate assigned to this worksheet. |
|
577
|
|
|
|
|
578
|
|
|
By default, returns a new slot at the end of the worksheet unless there |
|
579
|
|
|
is a slot defined for a reference of the same type (blank or control) |
|
580
|
|
|
in the worksheet template's layout that hasn't been used yet. |
|
581
|
|
|
|
|
582
|
|
|
:param reference: ReferenceSample the analyses will be created from |
|
583
|
|
|
:return: suitable slot position for reference analyses |
|
584
|
|
|
""" |
|
585
|
|
|
if not IReferenceSample.providedBy(reference): |
|
586
|
|
|
return -1 |
|
587
|
|
|
|
|
588
|
|
|
occupied = self.get_slot_positions(type='all') or [0] |
|
589
|
|
|
wst = self.getWorksheetTemplate() |
|
590
|
|
|
if not wst: |
|
591
|
|
|
# No worksheet template assigned, add a new slot at the end of the |
|
592
|
|
|
# worksheet with the reference analyses there |
|
593
|
|
|
slot_to = max(occupied) + 1 |
|
594
|
|
|
return slot_to |
|
595
|
|
|
|
|
596
|
|
|
# If there is a match with the layout defined in the Worksheet Template, |
|
597
|
|
|
# use that slot instead of adding a new one at the end of the worksheet |
|
598
|
|
|
slot_type = reference.getBlank() and 'b' or 'c' |
|
599
|
|
|
layout = wst.getLayout() |
|
600
|
|
|
|
|
601
|
|
|
for pos in layout: |
|
602
|
|
|
if pos['type'] != slot_type: |
|
603
|
|
|
continue |
|
604
|
|
|
slot_to = int(pos['pos']) |
|
605
|
|
|
if slot_to in occupied: |
|
606
|
|
|
# Not an empty slot |
|
607
|
|
|
continue |
|
608
|
|
|
|
|
609
|
|
|
# This slot is empty, use it instead of adding a new slot at the end |
|
610
|
|
|
# of the worksheet |
|
611
|
|
|
return slot_to |
|
612
|
|
|
|
|
613
|
|
|
# Add a new slot at the end of the worksheet, but take into account |
|
614
|
|
|
# that a worksheet template is assigned, so we need to take care to |
|
615
|
|
|
# not override slots defined by its layout |
|
616
|
|
|
occupied.append(len(layout)) |
|
617
|
|
|
slot_to = max(occupied) + 1 |
|
618
|
|
|
return slot_to |
|
619
|
|
|
|
|
620
|
|
|
def get_duplicates_for(self, analysis): |
|
621
|
|
|
"""Returns the duplicates from the current worksheet that were created |
|
622
|
|
|
by using the analysis passed in as the source |
|
623
|
|
|
|
|
624
|
|
|
:param analysis: routine analyses used as the source for the duplicates |
|
625
|
|
|
:return: a list of duplicates generated from the analysis passed in |
|
626
|
|
|
""" |
|
627
|
|
|
if not analysis: |
|
628
|
|
|
return list() |
|
629
|
|
|
uid = api.get_uid(analysis) |
|
630
|
|
|
return filter(lambda dup: api.get_uid(dup.getAnalysis()) == uid, |
|
|
|
|
|
|
631
|
|
|
self.getDuplicateAnalyses()) |
|
632
|
|
|
|
|
633
|
|
|
def get_analyses_at(self, slot): |
|
634
|
|
|
"""Returns the list of analyses assigned to the slot passed in, sorted by |
|
635
|
|
|
the positions they have within the slot. |
|
636
|
|
|
|
|
637
|
|
|
:param slot: the slot where the analyses are located |
|
638
|
|
|
:type slot: int |
|
639
|
|
|
:return: a list of analyses |
|
640
|
|
|
""" |
|
641
|
|
|
|
|
642
|
|
|
# ensure we have an integer |
|
643
|
|
|
slot = to_int(slot) |
|
644
|
|
|
|
|
645
|
|
|
if slot < 1: |
|
646
|
|
|
return list() |
|
647
|
|
|
|
|
648
|
|
|
analyses = list() |
|
649
|
|
|
layout = self.getLayout() |
|
650
|
|
|
|
|
651
|
|
|
for pos in layout: |
|
652
|
|
|
layout_slot = to_int(pos['position']) |
|
653
|
|
|
uid = pos['analysis_uid'] |
|
654
|
|
|
if layout_slot != slot or not uid: |
|
655
|
|
|
continue |
|
656
|
|
|
analyses.append(api.get_object_by_uid(uid)) |
|
657
|
|
|
|
|
658
|
|
|
return analyses |
|
659
|
|
|
|
|
660
|
|
|
def get_container_at(self, slot): |
|
661
|
|
|
"""Returns the container object assigned to the slot passed in |
|
662
|
|
|
|
|
663
|
|
|
:param slot: the slot where the analyses are located |
|
664
|
|
|
:type slot: int |
|
665
|
|
|
:return: the container (analysis request, reference sample, etc.) |
|
666
|
|
|
""" |
|
667
|
|
|
|
|
668
|
|
|
# ensure we have an integer |
|
669
|
|
|
slot = to_int(slot) |
|
670
|
|
|
|
|
671
|
|
|
if slot < 1: |
|
672
|
|
|
return None |
|
673
|
|
|
|
|
674
|
|
|
layout = self.getLayout() |
|
675
|
|
|
|
|
676
|
|
|
for pos in layout: |
|
677
|
|
|
layout_slot = to_int(pos['position']) |
|
678
|
|
|
uid = pos['container_uid'] |
|
679
|
|
|
if layout_slot != slot or not uid: |
|
680
|
|
|
continue |
|
681
|
|
|
return api.get_object_by_uid(uid) |
|
682
|
|
|
|
|
683
|
|
|
return None |
|
684
|
|
|
|
|
685
|
|
|
def get_slot_positions(self, type='a'): |
|
686
|
|
|
"""Returns a list with the slots occupied for the type passed in. |
|
687
|
|
|
|
|
688
|
|
|
Allowed type of analyses are: |
|
689
|
|
|
|
|
690
|
|
|
'a' (routine analysis) |
|
691
|
|
|
'b' (blank analysis) |
|
692
|
|
|
'c' (control) |
|
693
|
|
|
'd' (duplicate) |
|
694
|
|
|
'all' (all analyses) |
|
695
|
|
|
|
|
696
|
|
|
:param type: type of the analysis |
|
697
|
|
|
:return: list of slot positions |
|
698
|
|
|
""" |
|
699
|
|
|
if type not in ALLOWED_ANALYSES_TYPES and type != ALL_ANALYSES_TYPES: |
|
700
|
|
|
return list() |
|
701
|
|
|
|
|
702
|
|
|
layout = self.getLayout() |
|
703
|
|
|
slots = list() |
|
704
|
|
|
|
|
705
|
|
|
for pos in layout: |
|
706
|
|
|
if type != ALL_ANALYSES_TYPES and pos['type'] != type: |
|
707
|
|
|
continue |
|
708
|
|
|
slots.append(to_int(pos['position'])) |
|
709
|
|
|
|
|
710
|
|
|
# return a unique list of sorted slot positions |
|
711
|
|
|
return sorted(set(slots)) |
|
712
|
|
|
|
|
713
|
|
|
def get_slot_position(self, container, type='a'): |
|
714
|
|
|
"""Returns the slot where the analyses from the type and container passed |
|
715
|
|
|
in are located within the worksheet. |
|
716
|
|
|
|
|
717
|
|
|
:param container: the container in which the analyses are grouped |
|
718
|
|
|
:param type: type of the analysis |
|
719
|
|
|
:return: the slot position |
|
720
|
|
|
:rtype: int |
|
721
|
|
|
""" |
|
722
|
|
|
if not container or type not in ALLOWED_ANALYSES_TYPES: |
|
723
|
|
|
return None |
|
724
|
|
|
uid = api.get_uid(container) |
|
725
|
|
|
layout = self.getLayout() |
|
726
|
|
|
|
|
727
|
|
|
for pos in layout: |
|
728
|
|
|
if pos['type'] != type or pos['container_uid'] != uid: |
|
729
|
|
|
continue |
|
730
|
|
|
return to_int(pos['position']) |
|
731
|
|
|
return None |
|
732
|
|
|
|
|
733
|
|
|
def get_analysis_type(self, instance): |
|
734
|
|
|
"""Returns the string used in slots to differentiate amongst analysis |
|
735
|
|
|
types |
|
736
|
|
|
""" |
|
737
|
|
|
if IDuplicateAnalysis.providedBy(instance): |
|
738
|
|
|
return 'd' |
|
739
|
|
|
elif IReferenceAnalysis.providedBy(instance): |
|
740
|
|
|
return instance.getReferenceType() |
|
741
|
|
|
elif IRoutineAnalysis.providedBy(instance): |
|
742
|
|
|
return 'a' |
|
743
|
|
|
return None |
|
744
|
|
|
|
|
745
|
|
|
def get_container_for(self, instance): |
|
746
|
|
|
"""Returns the container id used in slots to group analyses |
|
747
|
|
|
""" |
|
748
|
|
|
if IReferenceAnalysis.providedBy(instance): |
|
749
|
|
|
return api.get_uid(instance.getSample()) |
|
750
|
|
|
return instance.getRequestUID() |
|
751
|
|
|
|
|
752
|
|
|
def get_slot_position_for(self, instance): |
|
753
|
|
|
"""Returns the slot where the instance passed in is located. If not |
|
754
|
|
|
found, returns None |
|
755
|
|
|
""" |
|
756
|
|
|
uid = api.get_uid(instance) |
|
757
|
|
|
slot = filter(lambda s: s['analysis_uid'] == uid, self.getLayout()) |
|
758
|
|
|
if not slot: |
|
759
|
|
|
return None |
|
760
|
|
|
return to_int(slot[0]['position']) |
|
761
|
|
|
|
|
762
|
|
|
def resolve_available_slots(self, worksheet_template, type='a'): |
|
763
|
|
|
"""Returns the available slots from the current worksheet that fits |
|
764
|
|
|
with the layout defined in the worksheet_template and type of analysis |
|
765
|
|
|
passed in. |
|
766
|
|
|
|
|
767
|
|
|
Allowed type of analyses are: |
|
768
|
|
|
|
|
769
|
|
|
'a' (routine analysis) |
|
770
|
|
|
'b' (blank analysis) |
|
771
|
|
|
'c' (control) |
|
772
|
|
|
'd' (duplicate) |
|
773
|
|
|
|
|
774
|
|
|
:param worksheet_template: the worksheet template to match against |
|
775
|
|
|
:param type: type of analyses to restrict that suit with the slots |
|
776
|
|
|
:return: a list of slots positions |
|
777
|
|
|
""" |
|
778
|
|
|
if not worksheet_template or type not in ALLOWED_ANALYSES_TYPES: |
|
779
|
|
|
return list() |
|
780
|
|
|
|
|
781
|
|
|
ws_slots = self.get_slot_positions(type) |
|
782
|
|
|
layout = worksheet_template.getLayout() |
|
783
|
|
|
slots = list() |
|
784
|
|
|
|
|
785
|
|
|
for row in layout: |
|
786
|
|
|
# skip rows that do not match with the given type |
|
787
|
|
|
if row['type'] != type: |
|
788
|
|
|
continue |
|
789
|
|
|
|
|
790
|
|
|
slot = to_int(row['pos']) |
|
791
|
|
|
|
|
792
|
|
|
if slot in ws_slots: |
|
793
|
|
|
# We only want those that are empty |
|
794
|
|
|
continue |
|
795
|
|
|
|
|
796
|
|
|
slots.append(slot) |
|
797
|
|
|
return slots |
|
798
|
|
|
|
|
799
|
|
|
def _apply_worksheet_template_routine_analyses(self, wst): |
|
800
|
|
|
"""Add routine analyses to worksheet according to the worksheet template |
|
801
|
|
|
layout passed in w/o overwriting slots that are already filled. |
|
802
|
|
|
|
|
803
|
|
|
If the template passed in has an instrument assigned, only those |
|
804
|
|
|
routine analyses that allows the instrument will be added. |
|
805
|
|
|
|
|
806
|
|
|
If the template passed in has a method assigned, only those routine |
|
807
|
|
|
analyses that allows the method will be added |
|
808
|
|
|
|
|
809
|
|
|
:param wst: worksheet template used as the layout |
|
810
|
|
|
:returns: None |
|
811
|
|
|
""" |
|
812
|
|
|
bac = api.get_tool("bika_analysis_catalog") |
|
813
|
|
|
services = wst.getService() |
|
814
|
|
|
wst_service_uids = [s.UID() for s in services] |
|
815
|
|
|
query = { |
|
816
|
|
|
"portal_type": "Analysis", |
|
817
|
|
|
"getServiceUID": wst_service_uids, |
|
818
|
|
|
"review_state": "unassigned", |
|
819
|
|
|
"isSampleReceived": True, |
|
820
|
|
|
"is_active": True, |
|
821
|
|
|
"sort_on": "getPrioritySortkey" |
|
822
|
|
|
} |
|
823
|
|
|
# Filter analyses their Analysis Requests have been received |
|
824
|
|
|
analyses = bac(query) |
|
825
|
|
|
|
|
826
|
|
|
# No analyses, nothing to do |
|
827
|
|
|
if not analyses: |
|
828
|
|
|
return |
|
829
|
|
|
|
|
830
|
|
|
# Available slots for routine analyses. Sort reverse, cause we need a |
|
831
|
|
|
# stack for sequential assignment of slots |
|
832
|
|
|
available_slots = self.resolve_available_slots(wst, 'a') |
|
833
|
|
|
available_slots.sort(reverse=True) |
|
834
|
|
|
|
|
835
|
|
|
# If there is an instrument assigned to this Worksheet Template, take |
|
836
|
|
|
# only the analyses that allow this instrument into consideration. |
|
837
|
|
|
instrument = wst.getInstrument() |
|
838
|
|
|
|
|
839
|
|
|
# If there is method assigned to the Worksheet Template, take only the |
|
840
|
|
|
# analyses that allow this method into consideration. |
|
841
|
|
|
method = wst.getRestrictToMethod() |
|
842
|
|
|
|
|
843
|
|
|
# This worksheet is empty? |
|
844
|
|
|
num_routine_analyses = len(self.getRegularAnalyses()) |
|
845
|
|
|
|
|
846
|
|
|
# Group Analyses by Analysis Requests |
|
847
|
|
|
ar_analyses = dict() |
|
848
|
|
|
ar_slots = dict() |
|
849
|
|
|
ar_fixed_slots = dict() |
|
850
|
|
|
|
|
851
|
|
|
for brain in analyses: |
|
852
|
|
|
obj = api.get_object(brain) |
|
853
|
|
|
arid = obj.getRequestID() |
|
854
|
|
|
|
|
855
|
|
|
if instrument and not obj.isInstrumentAllowed(instrument): |
|
856
|
|
|
# Exclude those analyses for which the worksheet's template |
|
857
|
|
|
# instrument is not allowed |
|
858
|
|
|
continue |
|
859
|
|
|
|
|
860
|
|
|
if method and not obj.isMethodAllowed(method): |
|
861
|
|
|
# Exclude those analyses for which the worksheet's template |
|
862
|
|
|
# method is not allowed |
|
863
|
|
|
continue |
|
864
|
|
|
|
|
865
|
|
|
slot = ar_slots.get(arid, None) |
|
866
|
|
|
if not slot: |
|
867
|
|
|
# We haven't processed other analyses that belong to the same |
|
868
|
|
|
# Analysis Request as the current one. |
|
869
|
|
|
if len(available_slots) == 0 and num_routine_analyses == 0: |
|
870
|
|
|
# No more slots available for this worksheet/template, so |
|
871
|
|
|
# we cannot add more analyses to this WS. Also, there is no |
|
872
|
|
|
# chance to process a new analysis with an available slot. |
|
873
|
|
|
break |
|
874
|
|
|
|
|
875
|
|
|
if num_routine_analyses == 0: |
|
876
|
|
|
# This worksheet is empty, but there are slots still |
|
877
|
|
|
# available, assign the next available slot to this analysis |
|
878
|
|
|
slot = available_slots.pop() |
|
879
|
|
|
else: |
|
880
|
|
|
# This worksheet is not empty and there are slots still |
|
881
|
|
|
# available. |
|
882
|
|
|
slot = self.get_slot_position(obj.getRequest()) |
|
883
|
|
|
if slot: |
|
884
|
|
|
# Prefixed slot position |
|
885
|
|
|
ar_fixed_slots[arid] = slot |
|
886
|
|
|
if arid not in ar_analyses: |
|
887
|
|
|
ar_analyses[arid] = list() |
|
888
|
|
|
ar_analyses[arid].append(obj) |
|
889
|
|
|
continue |
|
890
|
|
|
|
|
891
|
|
|
# This worksheet does not contain any other analysis |
|
892
|
|
|
# belonging to the same Analysis Request as the current |
|
893
|
|
|
if len(available_slots) == 0: |
|
894
|
|
|
# There is the chance to process a new analysis that |
|
895
|
|
|
# belongs to an Analysis Request that is already |
|
896
|
|
|
# in this worksheet. |
|
897
|
|
|
continue |
|
898
|
|
|
|
|
899
|
|
|
# Assign the next available slot |
|
900
|
|
|
slot = available_slots.pop() |
|
901
|
|
|
|
|
902
|
|
|
ar_slots[arid] = slot |
|
903
|
|
|
if arid not in ar_analyses: |
|
904
|
|
|
ar_analyses[arid] = list() |
|
905
|
|
|
ar_analyses[arid].append(obj) |
|
906
|
|
|
|
|
907
|
|
|
# Sort the analysis requests by sortable_title, so the ARs will appear |
|
908
|
|
|
# sorted in natural order. Since we will add the analysis with the |
|
909
|
|
|
# exact slot where they have to be displayed, we need to sort the slots |
|
910
|
|
|
# too and assign them to each group of analyses in natural order |
|
911
|
|
|
sorted_ar_ids = sorted(ar_analyses.keys()) |
|
912
|
|
|
slots = sorted(ar_slots.values(), reverse=True) |
|
913
|
|
|
|
|
914
|
|
|
# Add regular analyses |
|
915
|
|
|
for ar_id in sorted_ar_ids: |
|
916
|
|
|
slot = ar_fixed_slots.get(ar_id, None) |
|
917
|
|
|
if not slot: |
|
918
|
|
|
slot = slots.pop() |
|
919
|
|
|
ar_ans = ar_analyses[ar_id] |
|
920
|
|
|
for ar_an in ar_ans: |
|
921
|
|
|
self.addAnalysis(ar_an, slot) |
|
922
|
|
|
|
|
923
|
|
|
def _apply_worksheet_template_duplicate_analyses(self, wst): |
|
924
|
|
|
"""Add duplicate analyses to worksheet according to the worksheet template |
|
925
|
|
|
layout passed in w/o overwrite slots that are already filled. |
|
926
|
|
|
|
|
927
|
|
|
If the slot where the duplicate must be located is available, but the |
|
928
|
|
|
slot where the routine analysis should be found is empty, no duplicate |
|
929
|
|
|
will be generated for that given slot. |
|
930
|
|
|
|
|
931
|
|
|
:param wst: worksheet template used as the layout |
|
932
|
|
|
:returns: None |
|
933
|
|
|
""" |
|
934
|
|
|
wst_layout = wst.getLayout() |
|
935
|
|
|
|
|
936
|
|
|
for row in wst_layout: |
|
937
|
|
|
if row['type'] != 'd': |
|
938
|
|
|
continue |
|
939
|
|
|
|
|
940
|
|
|
src_pos = to_int(row['dup']) |
|
941
|
|
|
dest_pos = to_int(row['pos']) |
|
942
|
|
|
|
|
943
|
|
|
self.addDuplicateAnalyses(src_pos, dest_pos) |
|
944
|
|
|
|
|
945
|
|
|
def _resolve_reference_sample(self, reference_samples=None, |
|
946
|
|
|
service_uids=None): |
|
947
|
|
|
"""Returns the reference sample from reference_samples passed in that fits |
|
948
|
|
|
better with the service uid requirements. This is, the reference sample |
|
949
|
|
|
that covers most (or all) of the service uids passed in and has less |
|
950
|
|
|
number of remaining service_uids. |
|
951
|
|
|
|
|
952
|
|
|
If no reference_samples are set, returns None |
|
953
|
|
|
|
|
954
|
|
|
If no service_uids are set, returns the first reference_sample |
|
955
|
|
|
|
|
956
|
|
|
:param reference_samples: list of reference samples |
|
957
|
|
|
:param service_uids: list of service uids |
|
958
|
|
|
:return: the reference sample that fits better with the service uids |
|
959
|
|
|
""" |
|
960
|
|
|
if not reference_samples: |
|
961
|
|
|
return None, list() |
|
962
|
|
|
|
|
963
|
|
|
if not service_uids: |
|
964
|
|
|
# Since no service filtering has been defined, there is no need to |
|
965
|
|
|
# look for the best choice. Return the first one |
|
966
|
|
|
sample = reference_samples[0] |
|
967
|
|
|
spec_uids = sample.getSupportedServices(only_uids=True) |
|
968
|
|
|
return sample, spec_uids |
|
969
|
|
|
|
|
970
|
|
|
best_score = [0, 0] |
|
971
|
|
|
best_sample = None |
|
972
|
|
|
best_supported = None |
|
973
|
|
|
for sample in reference_samples: |
|
974
|
|
|
specs_uids = sample.getSupportedServices(only_uids=True) |
|
975
|
|
|
supported = [uid for uid in specs_uids if uid in service_uids] |
|
976
|
|
|
matches = len(supported) |
|
977
|
|
|
overlays = len(service_uids) - matches |
|
978
|
|
|
overlays = 0 if overlays < 0 else overlays |
|
979
|
|
|
|
|
980
|
|
|
if overlays == 0 and matches == len(service_uids): |
|
981
|
|
|
# Perfect match.. no need to go further |
|
982
|
|
|
return sample, supported |
|
983
|
|
|
|
|
984
|
|
|
if not best_sample \ |
|
985
|
|
|
or matches > best_score[0] \ |
|
986
|
|
|
or (matches == best_score[0] and overlays < best_score[1]): |
|
987
|
|
|
best_sample = sample |
|
988
|
|
|
best_score = [matches, overlays] |
|
989
|
|
|
best_supported = supported |
|
990
|
|
|
|
|
991
|
|
|
return best_sample, best_supported |
|
992
|
|
|
|
|
993
|
|
|
def _resolve_reference_samples(self, wst, type): |
|
994
|
|
|
""" |
|
995
|
|
|
Resolves the slots and reference samples in accordance with the |
|
996
|
|
|
Worksheet Template passed in and the type passed in. |
|
997
|
|
|
Returns a list of dictionaries |
|
998
|
|
|
:param wst: Worksheet Template that defines the layout |
|
999
|
|
|
:param type: type of analyses ('b' for blanks, 'c' for controls) |
|
1000
|
|
|
:return: list of dictionaries |
|
1001
|
|
|
""" |
|
1002
|
|
|
if not type or type not in ['b', 'c']: |
|
1003
|
|
|
return [] |
|
1004
|
|
|
|
|
1005
|
|
|
bc = api.get_tool("bika_catalog") |
|
1006
|
|
|
wst_type = type == 'b' and 'blank_ref' or 'control_ref' |
|
1007
|
|
|
|
|
1008
|
|
|
slots_sample = list() |
|
1009
|
|
|
available_slots = self.resolve_available_slots(wst, type) |
|
1010
|
|
|
wst_layout = wst.getLayout() |
|
1011
|
|
|
for row in wst_layout: |
|
1012
|
|
|
slot = int(row['pos']) |
|
1013
|
|
|
if slot not in available_slots: |
|
1014
|
|
|
continue |
|
1015
|
|
|
|
|
1016
|
|
|
ref_definition_uid = row.get(wst_type, None) |
|
1017
|
|
|
if not ref_definition_uid: |
|
1018
|
|
|
# Only reference analyses with reference definition can be used |
|
1019
|
|
|
# in worksheet templates |
|
1020
|
|
|
continue |
|
1021
|
|
|
|
|
1022
|
|
|
samples = bc(portal_type='ReferenceSample', |
|
1023
|
|
|
review_state='current', |
|
1024
|
|
|
is_active=True, |
|
1025
|
|
|
getReferenceDefinitionUID=ref_definition_uid) |
|
1026
|
|
|
|
|
1027
|
|
|
# We only want the reference samples that fit better with the type |
|
1028
|
|
|
# and with the analyses defined in the Template |
|
1029
|
|
|
services = wst.getService() |
|
1030
|
|
|
services = [s.UID() for s in services] |
|
1031
|
|
|
candidates = list() |
|
1032
|
|
|
for sample in samples: |
|
1033
|
|
|
obj = api.get_object(sample) |
|
1034
|
|
|
if (type == 'b' and obj.getBlank()) or \ |
|
1035
|
|
|
(type == 'c' and not obj.getBlank()): |
|
1036
|
|
|
candidates.append(obj) |
|
1037
|
|
|
|
|
1038
|
|
|
sample, uids = self._resolve_reference_sample(candidates, services) |
|
1039
|
|
|
if not sample: |
|
1040
|
|
|
continue |
|
1041
|
|
|
|
|
1042
|
|
|
slots_sample.append({'slot': slot, |
|
1043
|
|
|
'sample': sample, |
|
1044
|
|
|
'supported_services': uids}) |
|
1045
|
|
|
|
|
1046
|
|
|
return slots_sample |
|
1047
|
|
|
|
|
1048
|
|
|
def _apply_worksheet_template_reference_analyses(self, wst, type='all'): |
|
1049
|
|
|
""" |
|
1050
|
|
|
Add reference analyses to worksheet according to the worksheet template |
|
1051
|
|
|
layout passed in. Does not overwrite slots that are already filled. |
|
1052
|
|
|
:param wst: worksheet template used as the layout |
|
1053
|
|
|
""" |
|
1054
|
|
|
if type == 'all': |
|
1055
|
|
|
self._apply_worksheet_template_reference_analyses(wst, 'b') |
|
1056
|
|
|
self._apply_worksheet_template_reference_analyses(wst, 'c') |
|
1057
|
|
|
return |
|
1058
|
|
|
|
|
1059
|
|
|
if type not in ['b', 'c']: |
|
1060
|
|
|
return |
|
1061
|
|
|
|
|
1062
|
|
|
references = self._resolve_reference_samples(wst, type) |
|
1063
|
|
|
for reference in references: |
|
1064
|
|
|
slot = reference['slot'] |
|
1065
|
|
|
sample = reference['sample'] |
|
1066
|
|
|
services = reference['supported_services'] |
|
1067
|
|
|
self.addReferenceAnalyses(sample, services, slot) |
|
1068
|
|
|
|
|
1069
|
|
|
def applyWorksheetTemplate(self, wst): |
|
1070
|
|
|
""" Add analyses to worksheet according to wst's layout. |
|
1071
|
|
|
Will not overwrite slots which are filled already. |
|
1072
|
|
|
If the selected template has an instrument assigned, it will |
|
1073
|
|
|
only be applied to those analyses for which the instrument |
|
1074
|
|
|
is allowed, the same happens with methods. |
|
1075
|
|
|
""" |
|
1076
|
|
|
# Store the Worksheet Template field |
|
1077
|
|
|
self.getField('WorksheetTemplate').set(self, wst) |
|
1078
|
|
|
|
|
1079
|
|
|
if not wst: |
|
1080
|
|
|
return |
|
1081
|
|
|
|
|
1082
|
|
|
# Apply the template for routine analyses |
|
1083
|
|
|
self._apply_worksheet_template_routine_analyses(wst) |
|
1084
|
|
|
|
|
1085
|
|
|
# Apply the template for duplicate analyses |
|
1086
|
|
|
self._apply_worksheet_template_duplicate_analyses(wst) |
|
1087
|
|
|
|
|
1088
|
|
|
# Apply the template for reference analyses (blanks and controls) |
|
1089
|
|
|
self._apply_worksheet_template_reference_analyses(wst) |
|
1090
|
|
|
|
|
1091
|
|
|
# Assign the instrument |
|
1092
|
|
|
instrument = wst.getInstrument() |
|
1093
|
|
|
if instrument: |
|
1094
|
|
|
self.setInstrument(instrument, True) |
|
1095
|
|
|
|
|
1096
|
|
|
# Assign the method |
|
1097
|
|
|
method = wst.getRestrictToMethod() |
|
1098
|
|
|
if method: |
|
1099
|
|
|
self.setMethod(method, True) |
|
1100
|
|
|
|
|
1101
|
|
|
def getInstrumentTitle(self): |
|
1102
|
|
|
""" |
|
1103
|
|
|
Returns the instrument title |
|
1104
|
|
|
:returns: instrument's title |
|
1105
|
|
|
:rtype: string |
|
1106
|
|
|
""" |
|
1107
|
|
|
instrument = self.getInstrument() |
|
1108
|
|
|
if instrument: |
|
1109
|
|
|
return instrument.Title() |
|
1110
|
|
|
return '' |
|
1111
|
|
|
|
|
1112
|
|
|
def getWorksheetTemplateUID(self): |
|
1113
|
|
|
""" |
|
1114
|
|
|
Returns the template's UID assigned to this worksheet |
|
1115
|
|
|
:returns: worksheet's UID |
|
1116
|
|
|
:rtype: UID as string |
|
1117
|
|
|
""" |
|
1118
|
|
|
ws = self.getWorksheetTemplate() |
|
1119
|
|
|
if ws: |
|
1120
|
|
|
return ws.UID() |
|
1121
|
|
|
return '' |
|
1122
|
|
|
|
|
1123
|
|
|
def getWorksheetTemplateTitle(self): |
|
1124
|
|
|
""" |
|
1125
|
|
|
Returns the template's Title assigned to this worksheet |
|
1126
|
|
|
:returns: worksheet's Title |
|
1127
|
|
|
:rtype: string |
|
1128
|
|
|
""" |
|
1129
|
|
|
ws = self.getWorksheetTemplate() |
|
1130
|
|
|
if ws: |
|
1131
|
|
|
return ws.Title() |
|
1132
|
|
|
return '' |
|
1133
|
|
|
|
|
1134
|
|
|
def getWorksheetTemplateURL(self): |
|
1135
|
|
|
""" |
|
1136
|
|
|
Returns the template's URL assigned to this worksheet |
|
1137
|
|
|
:returns: worksheet's URL |
|
1138
|
|
|
:rtype: string |
|
1139
|
|
|
""" |
|
1140
|
|
|
ws = self.getWorksheetTemplate() |
|
1141
|
|
|
if ws: |
|
1142
|
|
|
return ws.absolute_url_path() |
|
1143
|
|
|
return '' |
|
1144
|
|
|
|
|
1145
|
|
|
def getWorksheetServices(self): |
|
1146
|
|
|
"""get list of analysis services present on this worksheet |
|
1147
|
|
|
""" |
|
1148
|
|
|
services = [] |
|
1149
|
|
|
for analysis in self.getAnalyses(): |
|
1150
|
|
|
service = analysis.getAnalysisService() |
|
1151
|
|
|
if service and service not in services: |
|
1152
|
|
|
services.append(service) |
|
1153
|
|
|
return services |
|
1154
|
|
|
|
|
1155
|
|
|
def getQCAnalyses(self): |
|
1156
|
|
|
""" |
|
1157
|
|
|
Return the Quality Control analyses. |
|
1158
|
|
|
:returns: a list of QC analyses |
|
1159
|
|
|
:rtype: List of ReferenceAnalysis/DuplicateAnalysis |
|
1160
|
|
|
""" |
|
1161
|
|
|
qc_types = ['ReferenceAnalysis', 'DuplicateAnalysis'] |
|
1162
|
|
|
analyses = self.getAnalyses() |
|
1163
|
|
|
return [a for a in analyses if a.portal_type in qc_types] |
|
1164
|
|
|
|
|
1165
|
|
|
def getDuplicateAnalyses(self): |
|
1166
|
|
|
"""Return the duplicate analyses assigned to the current worksheet |
|
1167
|
|
|
:return: List of DuplicateAnalysis |
|
1168
|
|
|
:rtype: List of IDuplicateAnalysis objects""" |
|
1169
|
|
|
ans = self.getAnalyses() |
|
1170
|
|
|
duplicates = [an for an in ans if IDuplicateAnalysis.providedBy(an)] |
|
1171
|
|
|
return duplicates |
|
1172
|
|
|
|
|
1173
|
|
|
def getReferenceAnalyses(self): |
|
1174
|
|
|
"""Return the reference analyses (controls) assigned to the current |
|
1175
|
|
|
worksheet |
|
1176
|
|
|
:return: List of reference analyses |
|
1177
|
|
|
:rtype: List of IReferenceAnalysis objects""" |
|
1178
|
|
|
ans = self.getAnalyses() |
|
1179
|
|
|
references = [an for an in ans if IReferenceAnalysis.providedBy(an)] |
|
1180
|
|
|
return references |
|
1181
|
|
|
|
|
1182
|
|
|
def getRegularAnalyses(self): |
|
1183
|
|
|
""" |
|
1184
|
|
|
Return the analyses assigned to the current worksheet that are directly |
|
1185
|
|
|
associated to an Analysis Request but are not QC analyses. This is all |
|
1186
|
|
|
analyses that implement IRoutineAnalysis |
|
1187
|
|
|
:return: List of regular analyses |
|
1188
|
|
|
:rtype: List of ReferenceAnalysis/DuplicateAnalysis |
|
1189
|
|
|
""" |
|
1190
|
|
|
qc_types = ['ReferenceAnalysis', 'DuplicateAnalysis'] |
|
1191
|
|
|
analyses = self.getAnalyses() |
|
1192
|
|
|
return [a for a in analyses if a.portal_type not in qc_types] |
|
1193
|
|
|
|
|
1194
|
|
|
def getNumberOfQCAnalyses(self): |
|
1195
|
|
|
""" |
|
1196
|
|
|
Returns the number of Quality Control analyses. |
|
1197
|
|
|
:returns: number of QC analyses |
|
1198
|
|
|
:rtype: integer |
|
1199
|
|
|
""" |
|
1200
|
|
|
return len(self.getQCAnalyses()) |
|
1201
|
|
|
|
|
1202
|
|
|
def getNumberOfRegularAnalyses(self): |
|
1203
|
|
|
""" |
|
1204
|
|
|
Returns the number of Regular analyses. |
|
1205
|
|
|
:returns: number of analyses |
|
1206
|
|
|
:rtype: integer |
|
1207
|
|
|
""" |
|
1208
|
|
|
return len(self.getRegularAnalyses()) |
|
1209
|
|
|
|
|
1210
|
|
|
def getNumberOfQCSamples(self): |
|
1211
|
|
|
""" |
|
1212
|
|
|
Returns the number of Quality Control samples. |
|
1213
|
|
|
:returns: number of QC samples |
|
1214
|
|
|
:rtype: integer |
|
1215
|
|
|
""" |
|
1216
|
|
|
qc_analyses = self.getQCAnalyses() |
|
1217
|
|
|
qc_samples = [a.getSample().UID() for a in qc_analyses] |
|
1218
|
|
|
# discarding any duplicate values |
|
1219
|
|
|
return len(set(qc_samples)) |
|
1220
|
|
|
|
|
1221
|
|
|
def getNumberOfRegularSamples(self): |
|
1222
|
|
|
""" |
|
1223
|
|
|
Returns the number of regular samples. |
|
1224
|
|
|
:returns: number of regular samples |
|
1225
|
|
|
:rtype: integer |
|
1226
|
|
|
""" |
|
1227
|
|
|
analyses = self.getRegularAnalyses() |
|
1228
|
|
|
samples = [a.getRequestUID() for a in analyses] |
|
1229
|
|
|
# discarding any duplicate values |
|
1230
|
|
|
return len(set(samples)) |
|
1231
|
|
|
|
|
1232
|
|
|
def setInstrument(self, instrument, override_analyses=False): |
|
1233
|
|
|
""" Sets the specified instrument to the Analysis from the |
|
1234
|
|
|
Worksheet. Only sets the instrument if the Analysis |
|
1235
|
|
|
allows it, according to its Analysis Service and Method. |
|
1236
|
|
|
If an analysis has already assigned an instrument, it won't |
|
1237
|
|
|
be overriden. |
|
1238
|
|
|
The Analyses that don't allow the instrument specified will |
|
1239
|
|
|
not be modified. |
|
1240
|
|
|
Returns the number of analyses affected |
|
1241
|
|
|
""" |
|
1242
|
|
|
analyses = [an for an in self.getAnalyses() |
|
1243
|
|
|
if (not an.getInstrument() or override_analyses) and |
|
1244
|
|
|
an.isInstrumentAllowed(instrument)] |
|
1245
|
|
|
total = 0 |
|
1246
|
|
|
for an in analyses: |
|
1247
|
|
|
# An analysis can be done using differents Methods. |
|
1248
|
|
|
# Un method can be supported by more than one Instrument, |
|
1249
|
|
|
# but not all instruments support one method. |
|
1250
|
|
|
# We must force to set the instrument's method too. Otherwise, |
|
1251
|
|
|
# the WS manage results view will display the an's default |
|
1252
|
|
|
# method and its instruments displaying, only the instruments |
|
1253
|
|
|
# for the default method in the picklist. |
|
1254
|
|
|
instr_methods = instrument.getMethods() |
|
1255
|
|
|
meth = instr_methods[0] if instr_methods else None |
|
1256
|
|
|
if meth and an.isMethodAllowed(meth): |
|
1257
|
|
|
if an.getMethod() not in instr_methods: |
|
1258
|
|
|
an.setMethod(meth) |
|
1259
|
|
|
|
|
1260
|
|
|
an.setInstrument(instrument) |
|
1261
|
|
|
total += 1 |
|
1262
|
|
|
|
|
1263
|
|
|
self.getField('Instrument').set(self, instrument) |
|
1264
|
|
|
return total |
|
1265
|
|
|
|
|
1266
|
|
|
def setMethod(self, method, override_analyses=False): |
|
1267
|
|
|
""" Sets the specified method to the Analyses from the |
|
1268
|
|
|
Worksheet. Only sets the method if the Analysis |
|
1269
|
|
|
allows to keep the integrity. |
|
1270
|
|
|
If an analysis has already been assigned to a method, it won't |
|
1271
|
|
|
be overriden. |
|
1272
|
|
|
Returns the number of analyses affected. |
|
1273
|
|
|
""" |
|
1274
|
|
|
analyses = [an for an in self.getAnalyses() |
|
1275
|
|
|
if (not an.getMethod() or |
|
1276
|
|
|
not an.getInstrument() or |
|
1277
|
|
|
override_analyses) and an.isMethodAllowed(method)] |
|
1278
|
|
|
total = 0 |
|
1279
|
|
|
for an in analyses: |
|
1280
|
|
|
success = False |
|
1281
|
|
|
if an.isMethodAllowed(method): |
|
1282
|
|
|
success = an.setMethod(method) |
|
1283
|
|
|
if success is True: |
|
1284
|
|
|
total += 1 |
|
1285
|
|
|
|
|
1286
|
|
|
self.getField('Method').set(self, method) |
|
1287
|
|
|
return total |
|
1288
|
|
|
|
|
1289
|
|
|
def getAnalystName(self): |
|
1290
|
|
|
""" Returns the name of the currently assigned analyst |
|
1291
|
|
|
""" |
|
1292
|
|
|
mtool = getToolByName(self, 'portal_membership') |
|
1293
|
|
|
analyst = self.getAnalyst().strip() |
|
1294
|
|
|
analyst_member = mtool.getMemberById(analyst) |
|
1295
|
|
|
if analyst_member is not None: |
|
1296
|
|
|
return analyst_member.getProperty('fullname') |
|
1297
|
|
|
return analyst |
|
1298
|
|
|
|
|
1299
|
|
View Code Duplication |
def getObjectWorkflowStates(self): |
|
|
|
|
|
|
1300
|
|
|
""" |
|
1301
|
|
|
This method is used as a metacolumn. |
|
1302
|
|
|
Returns a dictionary with the workflow id as key and workflow state as |
|
1303
|
|
|
value. |
|
1304
|
|
|
:returns: {'review_state':'active',...} |
|
1305
|
|
|
:rtype: dict |
|
1306
|
|
|
""" |
|
1307
|
|
|
workflow = getToolByName(self, 'portal_workflow') |
|
1308
|
|
|
states = {} |
|
1309
|
|
|
for w in workflow.getWorkflowsFor(self): |
|
1310
|
|
|
state = w._getWorkflowStateOf(self).id |
|
1311
|
|
|
states[w.state_var] = state |
|
1312
|
|
|
return states |
|
1313
|
|
|
|
|
1314
|
|
|
# TODO Workflow - Worksheet - Move to workflow.worksheet.events |
|
1315
|
|
|
def workflow_script_reject(self): |
|
1316
|
|
|
"""Copy real analyses to RejectAnalysis, with link to real |
|
1317
|
|
|
create a new worksheet, with the original analyses, and new |
|
1318
|
|
|
duplicates and references to match the rejected |
|
1319
|
|
|
worksheet. |
|
1320
|
|
|
""" |
|
1321
|
|
|
if skip(self, "reject"): |
|
1322
|
|
|
return |
|
1323
|
|
|
workflow = self.portal_workflow |
|
1324
|
|
|
|
|
1325
|
|
|
def copy_src_fields_to_dst(src, dst): |
|
1326
|
|
|
# These will be ignored when copying field values between analyses |
|
1327
|
|
|
ignore_fields = [ |
|
1328
|
|
|
'UID', |
|
1329
|
|
|
'id', |
|
1330
|
|
|
'title', |
|
1331
|
|
|
'allowDiscussion', |
|
1332
|
|
|
'subject', |
|
1333
|
|
|
'description', |
|
1334
|
|
|
'location', |
|
1335
|
|
|
'contributors', |
|
1336
|
|
|
'creators', |
|
1337
|
|
|
'effectiveDate', |
|
1338
|
|
|
'expirationDate', |
|
1339
|
|
|
'language', |
|
1340
|
|
|
'rights', |
|
1341
|
|
|
'creation_date', |
|
1342
|
|
|
'modification_date', |
|
1343
|
|
|
'Layout', # ws |
|
1344
|
|
|
'Analyses', # ws |
|
1345
|
|
|
] |
|
1346
|
|
|
fields = src.Schema().fields() |
|
1347
|
|
|
for field in fields: |
|
1348
|
|
|
fieldname = field.getName() |
|
1349
|
|
|
if fieldname in ignore_fields: |
|
1350
|
|
|
continue |
|
1351
|
|
|
getter = getattr(src, 'get' + fieldname, |
|
1352
|
|
|
src.Schema().getField(fieldname).getAccessor(src)) |
|
1353
|
|
|
setter = getattr(dst, 'set' + fieldname, |
|
1354
|
|
|
dst.Schema().getField(fieldname).getMutator(dst)) |
|
1355
|
|
|
if getter is None or setter is None: |
|
1356
|
|
|
# ComputedField |
|
1357
|
|
|
continue |
|
1358
|
|
|
setter(getter()) |
|
1359
|
|
|
|
|
1360
|
|
|
analysis_positions = {} |
|
1361
|
|
|
for item in self.getLayout(): |
|
1362
|
|
|
analysis_positions[item['analysis_uid']] = item['position'] |
|
1363
|
|
|
old_layout = [] |
|
1364
|
|
|
new_layout = [] |
|
1365
|
|
|
|
|
1366
|
|
|
# New worksheet |
|
1367
|
|
|
worksheets = self.aq_parent |
|
1368
|
|
|
new_ws = _createObjectByType('Worksheet', worksheets, tmpID()) |
|
1369
|
|
|
new_ws.unmarkCreationFlag() |
|
1370
|
|
|
new_ws_id = renameAfterCreation(new_ws) |
|
1371
|
|
|
copy_src_fields_to_dst(self, new_ws) |
|
1372
|
|
|
new_ws.edit( |
|
1373
|
|
|
Number=new_ws_id, |
|
1374
|
|
|
Remarks=self.getRemarks() |
|
1375
|
|
|
) |
|
1376
|
|
|
|
|
1377
|
|
|
# Objects are being created inside other contexts, but we want their |
|
1378
|
|
|
# workflow handlers to be aware of which worksheet this is occurring in. |
|
1379
|
|
|
# We save the worksheet in request['context_uid']. |
|
1380
|
|
|
# We reset it again below.... be very sure that this is set to the |
|
1381
|
|
|
# UID of the containing worksheet before invoking any transitions on |
|
1382
|
|
|
# analyses. |
|
1383
|
|
|
self.REQUEST['context_uid'] = new_ws.UID() |
|
1384
|
|
|
|
|
1385
|
|
|
# loop all analyses |
|
1386
|
|
|
analyses = self.getAnalyses() |
|
1387
|
|
|
new_ws_analyses = [] |
|
1388
|
|
|
old_ws_analyses = [] |
|
1389
|
|
|
for analysis in analyses: |
|
1390
|
|
|
# Skip published or verified analyses |
|
1391
|
|
|
review_state = workflow.getInfoFor(analysis, 'review_state', '') |
|
1392
|
|
|
if review_state in ['published', 'verified', 'retracted']: |
|
1393
|
|
|
old_ws_analyses.append(analysis.UID()) |
|
1394
|
|
|
|
|
1395
|
|
|
# XXX where does position come from? |
|
1396
|
|
|
old_layout.append({'position': position, |
|
|
|
|
|
|
1397
|
|
|
'type': 'a', |
|
1398
|
|
|
'analysis_uid': analysis.UID(), |
|
1399
|
|
|
'container_uid': analysis.aq_parent.UID()}) |
|
1400
|
|
|
continue |
|
1401
|
|
|
# Normal analyses: |
|
1402
|
|
|
# - Create matching RejectAnalysis inside old WS |
|
1403
|
|
|
# - Link analysis to new WS in same position |
|
1404
|
|
|
# - Copy all field values |
|
1405
|
|
|
# - Clear analysis result, and set Retested flag |
|
1406
|
|
|
if analysis.portal_type == 'Analysis': |
|
1407
|
|
|
reject = _createObjectByType('RejectAnalysis', self, tmpID()) |
|
1408
|
|
|
reject.unmarkCreationFlag() |
|
1409
|
|
|
copy_src_fields_to_dst(analysis, reject) |
|
1410
|
|
|
reject.setAnalysis(analysis) |
|
1411
|
|
|
reject.reindexObject() |
|
1412
|
|
|
analysis.edit( |
|
1413
|
|
|
Result=None, |
|
1414
|
|
|
Retested=True, |
|
1415
|
|
|
) |
|
1416
|
|
|
analysis.reindexObject() |
|
1417
|
|
|
position = analysis_positions[analysis.UID()] |
|
1418
|
|
|
old_ws_analyses.append(reject.UID()) |
|
1419
|
|
|
old_layout.append({'position': position, |
|
1420
|
|
|
'type': 'r', |
|
1421
|
|
|
'analysis_uid': reject.UID(), |
|
1422
|
|
|
'container_uid': self.UID()}) |
|
1423
|
|
|
new_ws_analyses.append(analysis.UID()) |
|
1424
|
|
|
new_layout.append({'position': position, |
|
1425
|
|
|
'type': 'a', |
|
1426
|
|
|
'analysis_uid': analysis.UID(), |
|
1427
|
|
|
'container_uid': analysis.aq_parent.UID()}) |
|
1428
|
|
|
# Reference analyses |
|
1429
|
|
|
# - Create a new reference analysis in the new worksheet |
|
1430
|
|
|
# - Transition the original analysis to 'rejected' state |
|
1431
|
|
|
if analysis.portal_type == 'ReferenceAnalysis': |
|
1432
|
|
|
service_uid = analysis.getServiceUID() |
|
1433
|
|
|
reference = analysis.aq_parent |
|
1434
|
|
|
new_reference = reference.addReferenceAnalysis(service_uid) |
|
1435
|
|
|
reference_type = new_reference.getReferenceType() |
|
1436
|
|
|
new_analysis_uid = api.get_uid(new_reference) |
|
1437
|
|
|
position = analysis_positions[analysis.UID()] |
|
1438
|
|
|
old_ws_analyses.append(analysis.UID()) |
|
1439
|
|
|
old_layout.append({'position': position, |
|
1440
|
|
|
'type': reference_type, |
|
1441
|
|
|
'analysis_uid': analysis.UID(), |
|
1442
|
|
|
'container_uid': reference.UID()}) |
|
1443
|
|
|
new_ws_analyses.append(new_analysis_uid) |
|
1444
|
|
|
new_layout.append({'position': position, |
|
1445
|
|
|
'type': reference_type, |
|
1446
|
|
|
'analysis_uid': new_analysis_uid, |
|
1447
|
|
|
'container_uid': reference.UID()}) |
|
1448
|
|
|
workflow.doActionFor(analysis, 'reject') |
|
1449
|
|
|
analysis.reindexObject() |
|
1450
|
|
|
# Duplicate analyses |
|
1451
|
|
|
# - Create a new duplicate inside the new worksheet |
|
1452
|
|
|
# - Transition the original analysis to 'rejected' state |
|
1453
|
|
|
if analysis.portal_type == 'DuplicateAnalysis': |
|
1454
|
|
|
duplicate_id = new_ws.generateUniqueId('DuplicateAnalysis') |
|
1455
|
|
|
new_duplicate = _createObjectByType('DuplicateAnalysis', |
|
1456
|
|
|
new_ws, duplicate_id) |
|
1457
|
|
|
new_duplicate.unmarkCreationFlag() |
|
1458
|
|
|
copy_src_fields_to_dst(analysis, new_duplicate) |
|
1459
|
|
|
new_duplicate.reindexObject() |
|
1460
|
|
|
position = analysis_positions[analysis.UID()] |
|
1461
|
|
|
old_ws_analyses.append(analysis.UID()) |
|
1462
|
|
|
old_layout.append({'position': position, |
|
1463
|
|
|
'type': 'd', |
|
1464
|
|
|
'analysis_uid': analysis.UID(), |
|
1465
|
|
|
'container_uid': self.UID()}) |
|
1466
|
|
|
new_ws_analyses.append(new_duplicate.UID()) |
|
1467
|
|
|
new_layout.append({'position': position, |
|
1468
|
|
|
'type': 'd', |
|
1469
|
|
|
'analysis_uid': new_duplicate.UID(), |
|
1470
|
|
|
'container_uid': new_ws.UID()}) |
|
1471
|
|
|
workflow.doActionFor(analysis, 'reject') |
|
1472
|
|
|
analysis.reindexObject() |
|
1473
|
|
|
|
|
1474
|
|
|
new_ws.setAnalyses(new_ws_analyses) |
|
1475
|
|
|
new_ws.setLayout(new_layout) |
|
1476
|
|
|
new_ws.replaces_rejected_worksheet = self.UID() |
|
1477
|
|
|
for analysis in new_ws.getAnalyses(): |
|
1478
|
|
|
review_state = workflow.getInfoFor(analysis, 'review_state', '') |
|
1479
|
|
|
if review_state == 'to_be_verified': |
|
1480
|
|
|
# TODO Workflow - Analysis Retest transition within a Worksheet |
|
1481
|
|
|
changeWorkflowState(analysis, "bika_analysis_workflow", "assigned") |
|
1482
|
|
|
self.REQUEST['context_uid'] = self.UID() |
|
1483
|
|
|
self.setLayout(old_layout) |
|
1484
|
|
|
self.setAnalyses(old_ws_analyses) |
|
1485
|
|
|
self.replaced_by = new_ws.UID() |
|
1486
|
|
|
|
|
1487
|
|
|
# TODO Workflow - Worksheet - Remove this function |
|
1488
|
|
|
def checkUserManage(self): |
|
1489
|
|
|
""" Checks if the current user has granted access to this worksheet |
|
1490
|
|
|
and if has also privileges for managing it. |
|
1491
|
|
|
""" |
|
1492
|
|
|
granted = False |
|
1493
|
|
|
can_access = self.checkUserAccess() |
|
1494
|
|
|
|
|
1495
|
|
|
if can_access is True: |
|
1496
|
|
|
pm = getToolByName(self, 'portal_membership') |
|
1497
|
|
|
edit_allowed = pm.checkPermission(EditWorksheet, self) |
|
1498
|
|
|
if edit_allowed: |
|
1499
|
|
|
# Check if the current user is the WS's current analyst |
|
1500
|
|
|
member = pm.getAuthenticatedMember() |
|
1501
|
|
|
analyst = self.getAnalyst().strip() |
|
1502
|
|
|
if analyst != _c(member.getId()): |
|
1503
|
|
|
# Has management privileges? |
|
1504
|
|
|
if pm.checkPermission(ManageWorksheets, self): |
|
1505
|
|
|
granted = True |
|
1506
|
|
|
else: |
|
1507
|
|
|
granted = True |
|
1508
|
|
|
|
|
1509
|
|
|
return granted |
|
1510
|
|
|
|
|
1511
|
|
|
# TODO Workflow - Worksheet - Remove this function |
|
1512
|
|
|
def checkUserAccess(self): |
|
1513
|
|
|
""" Checks if the current user has granted access to this worksheet. |
|
1514
|
|
|
Returns False if the user has no access, otherwise returns True |
|
1515
|
|
|
""" |
|
1516
|
|
|
# Deny access to foreign analysts |
|
1517
|
|
|
allowed = True |
|
1518
|
|
|
pm = getToolByName(self, "portal_membership") |
|
1519
|
|
|
member = pm.getAuthenticatedMember() |
|
1520
|
|
|
|
|
1521
|
|
|
analyst = self.getAnalyst().strip() |
|
1522
|
|
|
if analyst != _c(member.getId()): |
|
1523
|
|
|
roles = member.getRoles() |
|
1524
|
|
|
restrict = 'Manager' not in roles \ |
|
1525
|
|
|
and 'LabManager' not in roles \ |
|
1526
|
|
|
and 'LabClerk' not in roles \ |
|
1527
|
|
|
and 'RegulatoryInspector' not in roles \ |
|
1528
|
|
|
and self.bika_setup.getRestrictWorksheetUsersAccess() |
|
1529
|
|
|
allowed = not restrict |
|
1530
|
|
|
|
|
1531
|
|
|
return allowed |
|
1532
|
|
|
|
|
1533
|
|
|
def setAnalyst(self, analyst): |
|
1534
|
|
|
for analysis in self.getAnalyses(): |
|
1535
|
|
|
analysis.setAnalyst(analyst) |
|
1536
|
|
|
self.Schema().getField('Analyst').set(self, analyst) |
|
1537
|
|
|
self.reindexObject() |
|
1538
|
|
|
|
|
1539
|
|
|
def getAnalysesUIDs(self): |
|
1540
|
|
|
""" |
|
1541
|
|
|
Returns the analyses UIDs from the analyses assigned to this worksheet |
|
1542
|
|
|
:returns: a list of UIDs |
|
1543
|
|
|
:rtype: a list of strings |
|
1544
|
|
|
""" |
|
1545
|
|
|
analyses = self.getAnalyses() |
|
1546
|
|
|
if isinstance(analyses, list): |
|
1547
|
|
|
return [an.UID() for an in analyses] |
|
1548
|
|
|
return [] |
|
1549
|
|
|
|
|
1550
|
|
|
def getProgressPercentage(self): |
|
1551
|
|
|
"""Returns the progress percentage of this worksheet |
|
1552
|
|
|
""" |
|
1553
|
|
|
state = api.get_workflow_status_of(self) |
|
1554
|
|
|
if state == "verified": |
|
1555
|
|
|
return 100 |
|
1556
|
|
|
|
|
1557
|
|
|
steps = 0 |
|
1558
|
|
|
query = dict(getWorksheetUID=api.get_uid(self)) |
|
1559
|
|
|
analyses = api.search(query, CATALOG_ANALYSIS_LISTING) |
|
1560
|
|
|
max_steps = len(analyses) * 2 |
|
1561
|
|
|
for analysis in analyses: |
|
1562
|
|
|
an_state = analysis.review_state |
|
1563
|
|
|
if an_state in ["rejected", "retracted", "cancelled"]: |
|
1564
|
|
|
steps += 2 |
|
1565
|
|
|
elif an_state in ["verified", "published"]: |
|
1566
|
|
|
steps += 2 |
|
1567
|
|
|
elif an_state == "to_be_verified": |
|
1568
|
|
|
steps += 1 |
|
1569
|
|
|
if steps == 0: |
|
1570
|
|
|
return 0 |
|
1571
|
|
|
if steps > max_steps: |
|
1572
|
|
|
return 100 |
|
1573
|
|
|
return (steps * 100)/max_steps |
|
1574
|
|
|
|
|
1575
|
|
|
registerType(Worksheet, PROJECTNAME) |
|
1576
|
|
|
|