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