Passed
Push — master ( 6d6a8c...6374c8 )
by Ramon
05:17
created

  A

Complexity

Conditions 1

Size

Total Lines 9
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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