Passed
Push — master ( b60410...062e8c )
by Jordi
05:05
created

Worksheet.applyWorksheetTemplate()   A

Complexity

Conditions 4

Size

Total Lines 31
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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