Passed
Push — master ( d8e2ec...90ae0b )
by Jordi
10:07 queued 04:19
created

Worksheet.getInstrumentTitle()   A

Complexity

Conditions 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 10
rs 10
c 0
b 0
f 0
cc 2
nop 1
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
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable schema does not seem to be defined.
Loading history...
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,
0 ignored issues
show
introduced by
The variable uid does not seem to be defined for all execution paths.
Loading history...
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):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
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,
0 ignored issues
show
introduced by
The variable position does not seem to be defined for all execution paths.
Loading history...
Bug introduced by
The variable position was used before it was assigned.
Loading history...
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