Completed
Branch master (9edffc)
by Jordi
04:36
created

bika.lims.content.abstractroutineanalysis   F

Complexity

Total Complexity 72

Size/Duplication

Total Lines 518
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 72
eloc 277
dl 0
loc 518
rs 2.64
c 0
b 0
f 0

32 Methods

Rating   Name   Duplication   Size   Complexity  
A AbstractRoutineAnalysis.getRequestID() 0 8 2
A AbstractRoutineAnalysis.getClientUID() 0 10 3
A AbstractRoutineAnalysis.getClientOrderNumber() 0 8 2
A AbstractRoutineAnalysis.isSampleReceived() 0 6 1
A AbstractRoutineAnalysis.getDateSampled() 0 8 2
A AbstractRoutineAnalysis.getClientTitle() 0 10 3
A AbstractRoutineAnalysis.getRequestUID() 0 7 2
A AbstractRoutineAnalysis.isSampleSampled() 0 6 1
A AbstractRoutineAnalysis.getDatePublished() 0 7 1
A AbstractRoutineAnalysis.getDateReceived() 0 14 4
A AbstractRoutineAnalysis.getStartProcessDate() 0 10 1
A AbstractRoutineAnalysis.getClientURL() 0 10 3
A AbstractRoutineAnalysis.getRequest() 0 7 1
A AbstractRoutineAnalysis.getRequestURL() 0 10 2
A AbstractRoutineAnalysis.getSiblings() 0 11 1
A AbstractRoutineAnalysis.getPrioritySortkey() 0 16 3
A AbstractRoutineAnalysis.getDependents() 0 19 4
A AbstractRoutineAnalysis.getAnalysisRequestPrintStatus() 0 7 2
A AbstractRoutineAnalysis.getHidden() 0 23 3
A AbstractRoutineAnalysis.getSamplePointUID() 0 7 2
A AbstractRoutineAnalysis.getOriginalReflexedAnalysisUID() 0 9 2
A AbstractRoutineAnalysis.getDependencies() 0 22 4
A AbstractRoutineAnalysis.setHidden() 0 14 1
A AbstractRoutineAnalysis.addReflexRuleActionsTriggered() 0 11 1
A AbstractRoutineAnalysis.getDueDate() 0 14 3
A AbstractRoutineAnalysis.setReflexAnalysisOf() 0 10 3
A AbstractRoutineAnalysis._reflex_rule_process() 0 29 5
A AbstractRoutineAnalysis.getSampleType() 0 6 2
A AbstractRoutineAnalysis.getResultsRange() 0 24 2
A AbstractRoutineAnalysis.getSampleTypeUID() 0 7 2
A AbstractRoutineAnalysis.getSamplePoint() 0 6 2
A AbstractRoutineAnalysis.getBatchUID() 0 7 2

How to fix   Complexity   

Complexity

Complex classes like bika.lims.content.abstractroutineanalysis often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
# -*- coding: utf-8 -*-
2
#
3
# This file is part of SENAITE.CORE
4
#
5
# Copyright 2018 by it's authors.
6
# Some rights reserved. See LICENSE.rst, CONTRIBUTORS.rst.
7
8
from AccessControl import ClassSecurityInfo
9
from datetime import timedelta
10
from Products.Archetypes.Field import BooleanField, FixedPointField, \
11
    StringField
12
from Products.Archetypes.Schema import Schema
13
from Products.ATContentTypes.utils import DT2dt, dt2DT
14
from bika.lims import api
15
from bika.lims import bikaMessageFactory as _
16
from bika.lims.browser.fields import UIDReferenceField
17
from bika.lims.browser.widgets import DecimalWidget
18
from bika.lims.catalog.indexers.baseanalysis import sortable_title
19
from bika.lims.content.abstractanalysis import AbstractAnalysis
20
from bika.lims.content.abstractanalysis import schema
21
from bika.lims.content.analysisspec import ResultsRangeDict
22
from bika.lims.content.reflexrule import doReflexRuleAction
23
from bika.lims.interfaces import IAnalysis, IRoutineAnalysis, ICancellable
24
from bika.lims.interfaces.analysis import IRequestAnalysis
25
from bika.lims.workflow import doActionFor
26
from bika.lims.workflow import getTransitionDate
27
from bika.lims.workflow import skip
28
from bika.lims.workflow import wasTransitionPerformed
29
from zope.interface import implements
30
31
32
# TODO Remove in >v1.3.0 - This is kept for backwards-compatibility
33
# The physical sample partition linked to the Analysis.
34
SamplePartition = UIDReferenceField(
35
    'SamplePartition',
36
    required=0,
37
    allowed_types=('SamplePartition',)
38
)
39
40
# True if the analysis is created by a reflex rule
41
IsReflexAnalysis = BooleanField(
42
    'IsReflexAnalysis',
43
    default=False,
44
    required=0
45
)
46
47
# This field contains the original analysis which was reflected
48
OriginalReflexedAnalysis = UIDReferenceField(
49
    'OriginalReflexedAnalysis',
50
    required=0,
51
    allowed_types=('Analysis',)
52
)
53
54
# This field contains the analysis which has been reflected following
55
# a reflex rule
56
ReflexAnalysisOf = UIDReferenceField(
57
    'ReflexAnalysisOf',
58
    required=0,
59
    allowed_types=('Analysis',)
60
)
61
62
# Which is the Reflex Rule action that has created this analysis
63
ReflexRuleAction = StringField(
64
    'ReflexRuleAction',
65
    required=0,
66
    default=0
67
)
68
69
# Which is the 'local_id' inside the reflex rule
70
ReflexRuleLocalID = StringField(
71
    'ReflexRuleLocalID',
72
    required=0,
73
    default=0
74
)
75
76
# Reflex rule triggered actions which the current analysis is responsible for.
77
# Separated by '|'
78
ReflexRuleActionsTriggered = StringField(
79
    'ReflexRuleActionsTriggered',
80
    required=0,
81
    default=''
82
)
83
84
# The actual uncertainty for this analysis' result, populated when the result
85
# is submitted.
86
Uncertainty = FixedPointField(
87
    'Uncertainty',
88
    precision=10,
89
    widget=DecimalWidget(
90
        label=_("Uncertainty")
91
    )
92
)
93
# This field keep track if the field hidden has been set manually or not. If
94
# this value is false, the system will assume the visibility of this analysis
95
# in results report will depend on the value set at AR, Profile or Template
96
# levels (see AnalysisServiceSettings fields in AR). If the value for this
97
# field is set to true, the system will assume the visibility of the analysis
98
# will only depend on the value set for the field Hidden (bool).
99
HiddenManually = BooleanField(
100
    'HiddenManually',
101
    default=False,
102
)
103
104
105
schema = schema.copy() + Schema((
106
    IsReflexAnalysis,
107
    OriginalReflexedAnalysis,
108
    ReflexAnalysisOf,
109
    ReflexRuleAction,
110
    ReflexRuleActionsTriggered,
111
    ReflexRuleLocalID,
112
    SamplePartition,
113
    Uncertainty,
114
    HiddenManually,
115
))
116
117
118
class AbstractRoutineAnalysis(AbstractAnalysis):
119
    implements(IAnalysis, IRequestAnalysis, IRoutineAnalysis, ICancellable)
120
    security = ClassSecurityInfo()
121
    displayContentsTab = False
122
    schema = schema
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable schema does not seem to be defined.
Loading history...
123
124
    @security.public
125
    def getRequest(self):
126
        """Returns the Analysis Request this analysis belongs to.
127
        Delegates to self.aq_parent
128
        """
129
        ar = self.aq_parent
130
        return ar
131
132
    @security.public
133
    def getRequestID(self):
134
        """Used to populate catalog values.
135
        Returns the ID of the parent analysis request.
136
        """
137
        ar = self.getRequest()
138
        if ar:
139
            return ar.getId()
140
141
    @security.public
142
    def getRequestUID(self):
143
        """Returns the UID of the parent analysis request.
144
        """
145
        ar = self.getRequest()
146
        if ar:
147
            return ar.UID()
148
149
    @security.public
150
    def getRequestURL(self):
151
        """Returns the url path of the Analysis Request object this analysis
152
        belongs to. Returns None if there is no Request assigned.
153
        :return: the Analysis Request URL path this analysis belongs to
154
        :rtype: str
155
        """
156
        request = self.getRequest()
157
        if request:
158
            return request.absolute_url_path()
159
160
    @security.public
161
    def getClientTitle(self):
162
        """Used to populate catalog values.
163
        Returns the Title of the client for this analysis' AR.
164
        """
165
        request = self.getRequest()
166
        if request:
167
            client = request.getClient()
168
            if client:
169
                return client.Title()
170
171
    @security.public
172
    def getClientUID(self):
173
        """Used to populate catalog values.
174
        Returns the UID of the client for this analysis' AR.
175
        """
176
        request = self.getRequest()
177
        if request:
178
            client = request.getClient()
179
            if client:
180
                return client.UID()
181
182
    @security.public
183
    def getClientURL(self):
184
        """This method is used to populate catalog values
185
        Returns the URL of the client for this analysis' AR.
186
        """
187
        request = self.getRequest()
188
        if request:
189
            client = request.getClient()
190
            if client:
191
                return client.absolute_url_path()
192
193
    @security.public
194
    def getClientOrderNumber(self):
195
        """Used to populate catalog values.
196
        Returns the ClientOrderNumber of the associated AR
197
        """
198
        request = self.getRequest()
199
        if request:
200
            return request.getClientOrderNumber()
201
202
    @security.public
203
    def getDateReceived(self):
204
        """Used to populate catalog values.
205
        Returns the date the Analysis Request this analysis belongs to was
206
        received. If the analysis was created after, then returns the date
207
        the analysis was created.
208
        """
209
        request = self.getRequest()
210
        if request:
211
            ar_date = request.getDateReceived()
212
            if ar_date and self.created() > ar_date:
213
                return self.created()
214
            return ar_date
215
        return None
216
217
    @security.public
218
    def isSampleReceived(instance):
219
        """Returns whether if the Analysis Request this analysis comes from has
220
        been received or not
221
        """
222
        return instance.getDateReceived() and True or False
223
224
    @security.public
225
    def getDatePublished(self):
226
        """Used to populate catalog values.
227
        Returns the date on which the "publish" transition was invoked on this
228
        analysis.
229
        """
230
        return getTransitionDate(self, 'publish', return_as_datetime=True)
231
232
    @security.public
233
    def getDateSampled(self):
234
        """Returns the date when the Sample was Sampled
235
        """
236
        request = self.getRequest()
237
        if request:
238
            return request.getDateSampled()
239
        return None
240
241
    @security.public
242
    def isSampleSampled(instance):
243
        """Returns whether if the Analysis Request this analysis comes from has
244
        been received or not
245
        """
246
        return instance.getDateSampled() and True or False
247
248
    @security.public
249
    def getStartProcessDate(self):
250
        """Returns the date time when the analysis request the analysis belongs
251
        to was received. If the analysis request hasn't yet been received,
252
        returns None
253
        Overrides getStartProcessDateTime from the base class
254
        :return: Date time when the analysis is ready to be processed.
255
        :rtype: DateTime
256
        """
257
        return self.getDateReceived()
258
259
    @security.public
260
    def getSamplePoint(self):
261
        request = self.getRequest()
262
        if request:
263
            return request.getSamplePoint()
264
        return None
265
266
    @security.public
267
    def getSamplePointUID(self):
268
        """Used to populate catalog values.
269
        """
270
        sample_point = self.getSamplePoint()
271
        if sample_point:
272
            return api.get_uid(sample_point)
273
274
    @security.public
275
    def getDueDate(self):
276
        """Used to populate getDueDate index and metadata.
277
        This calculates the difference between the time the analysis processing
278
        started and the maximum turnaround time. If the analysis has no
279
        turnaround time set or is not yet ready for proces, returns None
280
        """
281
        tat = self.getMaxTimeAllowed()
282
        if not tat:
283
            return None
284
        start = self.getStartProcessDate()
285
        if not start:
286
            return None
287
        return dt2DT(DT2dt(start) + timedelta(minutes=api.to_minutes(**tat)))
288
289
    @security.public
290
    def getSampleType(self):
291
        request = self.getRequest()
292
        if request:
293
            return request.getSampleType()
294
        return None
295
296
    @security.public
297
    def getSampleTypeUID(self):
298
        """Used to populate catalog values.
299
        """
300
        sample_type = self.getSampleType()
301
        if sample_type:
302
            return api.get_uid(sample_type)
303
304
    @security.public
305
    def getBatchUID(self):
306
        """This method is used to populate catalog values
307
        """
308
        request = self.getRequest()
309
        if request:
310
            return request.getBatchUID()
311
312
    @security.public
313
    def getAnalysisRequestPrintStatus(self):
314
        """This method is used to populate catalog values
315
        """
316
        request = self.getRequest()
317
        if request:
318
            return request.getPrinted()
319
320
    @security.public
321
    def getResultsRange(self):
322
        """Returns the valid result range for this routine analysis based on the
323
        results ranges defined in the Analysis Request this routine analysis is
324
        assigned to.
325
326
        A routine analysis will be considered out of range if it result falls
327
        out of the range defined in "min" and "max". If there are values set for
328
        "warn_min" and "warn_max", these are used to compute the shoulders in
329
        both ends of the range. Thus, an analysis can be out of range, but be
330
        within shoulders still.
331
        :return: A dictionary with keys "min", "max", "warn_min" and "warn_max"
332
        :rtype: dict
333
        """
334
        specs = ResultsRangeDict()
335
        analysis_request = self.getRequest()
336
        if not analysis_request:
337
            return specs
338
339
        keyword = self.getKeyword()
340
        ar_ranges = analysis_request.getResultsRange()
341
        # Get the result range that corresponds to this specific analysis
342
        an_range = [rr for rr in ar_ranges if rr.get('keyword', '') == keyword]
343
        return an_range and an_range[0] or specs
344
345
    @security.public
346
    def getSiblings(self, retracted=False):
347
        """
348
        Return the siblings analyses, using the parent to which the current
349
        analysis belongs to as the source
350
        :param retracted: If false, retracted/rejected siblings are dismissed
351
        :type retracted: bool
352
        :return: list of siblings for this analysis
353
        :rtype: list of IAnalysis
354
        """
355
        raise NotImplementedError("getSiblings is not implemented.")
356
357
    @security.public
358
    def getDependents(self, retracted=False):
359
        """
360
        Returns a list of siblings who depend on us to calculate their result.
361
        :param retracted: If false, retracted/rejected dependents are dismissed
362
        :type retracted: bool
363
        :return: Analyses the current analysis depends on
364
        :rtype: list of IAnalysis
365
        """
366
        dependents = []
367
        for sibling in self.getSiblings(retracted=retracted):
368
            calculation = sibling.getCalculation()
369
            if not calculation:
370
                continue
371
            depservices = calculation.getDependentServices()
372
            dep_keywords = [dep.getKeyword() for dep in depservices]
373
            if self.getKeyword() in dep_keywords:
374
                dependents.append(sibling)
375
        return dependents
376
377
    @security.public
378
    def getDependencies(self, retracted=False):
379
        """
380
        Return a list of siblings who we depend on to calculate our result.
381
        :param retracted: If false retracted/rejected dependencies are dismissed
382
        :type retracted: bool
383
        :return: Analyses the current analysis depends on
384
        :rtype: list of IAnalysis
385
        """
386
        calc = self.getCalculation()
387
        if not calc:
388
            return []
389
390
        dependencies = []
391
        for sibling in self.getSiblings(retracted=retracted):
392
            # We get all analyses that depend on me, also if retracted (maybe
393
            # I am one of those that are retracted!)
394
            deps = sibling.getDependents(retracted=True)
395
            deps = [dep.UID() for dep in deps]
396
            if self.UID() in deps:
397
                dependencies.append(sibling)
398
        return dependencies
399
400
    @security.public
401
    def getPrioritySortkey(self):
402
        """
403
        Returns the key that will be used to sort the current Analysis, from
404
        most prioritary to less prioritary.
405
        :return: string used for sorting
406
        """
407
        analysis_request = self.getRequest()
408
        if analysis_request is None:
409
            return None
410
        ar_sort_key = analysis_request.getPrioritySortkey()
411
        ar_id = analysis_request.getId().lower()
412
        title = sortable_title(self)
413
        if callable(title):
414
            title = title()
415
        return '{}.{}.{}'.format(ar_sort_key, ar_id, title)
416
417
    @security.public
418
    def getHidden(self):
419
        """ Returns whether if the analysis must be displayed in results
420
        reports or not, as well as in analyses view when the user logged in
421
        is a Client Contact.
422
423
        If the value for the field HiddenManually is set to False, this function
424
        will delegate the action to the method getAnalysisServiceSettings() from
425
        the Analysis Request.
426
427
        If the value for the field HiddenManually is set to True, this function
428
        will return the value of the field Hidden.
429
        :return: true or false
430
        :rtype: bool
431
        """
432
        if self.getHiddenManually():
433
            return self.getField('Hidden').get(self)
434
        request = self.getRequest()
435
        if request:
436
            service_uid = self.getServiceUID()
437
            ar_settings = request.getAnalysisServiceSettings(service_uid)
438
            return ar_settings.get('hidden', False)
439
        return False
440
441
    @security.public
442
    def setHidden(self, hidden):
443
        """ Sets if this analysis must be displayed or not in results report and
444
        in manage analyses view if the user is a lab contact as well.
445
446
        The value set by using this field will have priority over the visibility
447
        criteria set at Analysis Request, Template or Profile levels (see
448
        field AnalysisServiceSettings from Analysis Request. To achieve this
449
        behavior, this setter also sets the value to HiddenManually to true.
450
        :param hidden: true if the analysis must be hidden in report
451
        :type hidden: bool
452
        """
453
        self.setHiddenManually(True)
454
        self.getField('Hidden').set(self, hidden)
455
456
    @security.public
457
    def setReflexAnalysisOf(self, analysis):
458
        """Sets the analysis that has been reflexed in order to create this
459
        one, but if the analysis is the same as self, do nothing.
460
        :param analysis: an analysis object or UID
461
        """
462
        if not analysis or analysis.UID() == self.UID():
463
            pass
464
        else:
465
            self.getField('ReflexAnalysisOf').set(self, analysis)
466
467
    @security.public
468
    def addReflexRuleActionsTriggered(self, text):
469
        """This function adds a new item to the string field
470
        ReflexRuleActionsTriggered. From the field: Reflex rule triggered
471
        actions from which the current analysis is responsible of. Separated
472
        by '|'
473
        :param text: is a str object with the format '<UID>.<rulename>' ->
474
        '123354.1'
475
        """
476
        old = self.getReflexRuleActionsTriggered()
477
        self.setReflexRuleActionsTriggered(old + text + '|')
478
479
    @security.public
480
    def getOriginalReflexedAnalysisUID(self):
481
        """
482
        Returns the uid of the original reflexed analysis.
483
        """
484
        original = self.getOriginalReflexedAnalysis()
485
        if original:
486
            return original.UID()
487
        return ''
488
489
    @security.private
490
    def _reflex_rule_process(self, wf_action):
491
        """This function does all the reflex rule process.
492
        :param wf_action: is a string containing the workflow action triggered
493
        """
494
        # Check out if the analysis has any reflex rule bound to it.
495
        # First we have get the analysis' method because the Reflex Rule
496
        # objects are related to a method.
497
        a_method = self.getMethod()
498
        if not a_method:
499
            return
500
        # After getting the analysis' method we have to get all Reflex Rules
501
        # related to that method.
502
        all_rrs = a_method.getBackReferences('ReflexRuleMethod')
503
        if not all_rrs:
504
            return
505
        # Once we have all the Reflex Rules with the same method as the
506
        # analysis has, it is time to get the rules that are bound to the
507
        # same analysis service that is using the analysis.
508
        for rule in all_rrs:
509
            if not api.is_active(rule):
510
                continue
511
            # Getting the rules to be done from the reflex rule taking
512
            # in consideration the analysis service, the result and
513
            # the state change
514
            action_row = rule.getActionReflexRules(self, wf_action)
515
            # Once we have the rules, the system has to execute its
516
            # instructions if the result has the expected result.
517
            doReflexRuleAction(self, action_row)
518