bika.lims.content.worksheet   F
last analyzed

Complexity

Total Complexity 244

Size/Duplication

Total Lines 1552
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 244
eloc 869
dl 0
loc 1552
rs 1.731
c 0
b 0
f 0

53 Methods

Rating   Name   Duplication   Size   Complexity  
D Worksheet._resolve_reference_samples() 0 54 12
A Worksheet._apply_worksheet_template_reference_analyses() 0 20 4
C Worksheet._resolve_reference_sample() 0 47 11
A Worksheet._apply_worksheet_template_duplicate_analyses() 0 21 3
A Worksheet.nextRefAnalysesGroupID() 0 22 4
F Worksheet.addAnalysis() 0 64 14
B Worksheet.addToLayout() 0 23 6
A Worksheet._getInstrumentsVoc() 0 18 5
B Worksheet.add_duplicate_analysis() 0 53 6
A Worksheet.get_container_for() 0 6 2
B Worksheet.resolve_available_slots() 0 36 6
B Worksheet.get_slot_positions() 0 27 6
A Worksheet.get_duplicates_for() 0 12 3
C Worksheet.addDuplicateAnalyses() 0 44 11
B Worksheet.get_slot_position() 0 19 6
C Worksheet.addReferenceAnalyses() 0 52 11
A Worksheet.Title() 0 2 1
A Worksheet.removeAnalysis() 0 8 2
C Worksheet.get_suitable_slot_for_duplicate() 0 52 9
A Worksheet.get_containers_slots() 0 5 2
A Worksheet.get_analysis_type() 0 11 4
A Worksheet._getMethodsVoc() 0 12 2
B Worksheet.add_reference_analysis() 0 49 6
A Worksheet.addAnalyses() 0 5 2
A Worksheet.get_container_at() 0 24 5
B Worksheet.get_suitable_slot_for_reference() 0 45 6
A Worksheet.purgeLayout() 0 7 2
A Worksheet.setLayout() 0 7 2
A Worksheet.get_slot_position_for() 0 9 3
A Worksheet._renameAfterCreation() 0 3 1
A Worksheet.get_analyses_at() 0 26 5
A Worksheet.setMethod() 0 22 4
A Worksheet.getWorksheetTemplateURL() 0 10 2
A Worksheet.getQCAnalyses() 0 9 1
A Worksheet.getAnalystName() 0 9 2
A Worksheet.checkUserAccess() 0 20 2
F Worksheet.workflow_script_reject() 0 168 14
A Worksheet.getReferenceAnalyses() 0 8 1
A Worksheet.applyWorksheetTemplate() 0 36 4
A Worksheet.checkUserManage() 0 21 5
A Worksheet.getAnalysesUIDs() 0 10 2
F Worksheet._apply_worksheet_template_routine_analyses() 0 104 17
A Worksheet.getDuplicateAnalyses() 0 7 1
A Worksheet.getRegularAnalyses() 0 11 1
A Worksheet.getWorksheetTemplateUID() 0 7 1
A Worksheet.getNumberOfRegularAnalyses() 0 7 1
A Worksheet.setAnalyst() 0 5 2
A Worksheet.getNumberOfRegularSamples() 0 10 1
A Worksheet.getWorksheetTemplateTitle() 0 10 2
A Worksheet.getNumberOfQCSamples() 0 10 1
C Worksheet.setInstrument() 0 44 9
A Worksheet.getNumberOfQCAnalyses() 0 7 1
B Worksheet.getProgressPercentage() 0 24 8

How to fix   Complexity   

Complexity

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