Total Complexity | 251 |
Total Lines | 1587 |
Duplicated Lines | 0.88 % |
Changes | 0 |
Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like bika.lims.content.worksheet often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
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 |