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