Passed
Pull Request — 2.x (#1854)
by Jordi
05:32
created

bika.lims.content.abstractroutineanalysis   F

Complexity

Total Complexity 62

Size/Duplication

Total Lines 456
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 62
eloc 237
dl 0
loc 456
rs 3.44
c 0
b 0
f 0

26 Methods

Rating   Name   Duplication   Size   Complexity  
A AbstractRoutineAnalysis.getSiblings() 0 11 1
A AbstractRoutineAnalysis.getPrioritySortkey() 0 16 3
B AbstractRoutineAnalysis.getDependents() 0 35 6
A AbstractRoutineAnalysis.getCalculation() 0 9 2
A AbstractRoutineAnalysis.getRequestID() 0 8 2
A AbstractRoutineAnalysis.getClient() 0 5 1
A AbstractRoutineAnalysis.isSampleReceived() 0 6 1
A AbstractRoutineAnalysis.getClientOrderNumber() 0 8 2
A AbstractRoutineAnalysis.getHidden() 0 23 3
A AbstractRoutineAnalysis.getDateSampled() 0 8 2
A AbstractRoutineAnalysis.setInternalUse() 0 9 2
A AbstractRoutineAnalysis.getRequestUID() 0 7 2
A AbstractRoutineAnalysis.isSampleSampled() 0 6 1
A AbstractRoutineAnalysis.getDatePublished() 0 7 1
A AbstractRoutineAnalysis.setHidden() 0 14 1
B AbstractRoutineAnalysis.getDependencies() 0 34 6
B AbstractRoutineAnalysis.getDueDate() 0 46 7
A AbstractRoutineAnalysis.getSampleType() 0 6 2
A AbstractRoutineAnalysis.getResultsRange() 0 14 1
A AbstractRoutineAnalysis.getSampleTypeUID() 0 7 2
A AbstractRoutineAnalysis.getStartProcessDate() 0 10 1
A AbstractRoutineAnalysis.getDateReceived() 0 14 4
A AbstractRoutineAnalysis.getRequest() 0 7 1
A AbstractRoutineAnalysis.getSamplePoint() 0 6 2
A AbstractRoutineAnalysis.getConditions() 0 18 4
A AbstractRoutineAnalysis.getRequestURL() 0 10 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
# 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-2021 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
import copy
22
23
from datetime import timedelta
24
25
from AccessControl import ClassSecurityInfo
26
from bika.lims import api
27
from bika.lims import bikaMessageFactory as _
28
from bika.lims.browser.fields import UIDReferenceField
29
from bika.lims.browser.widgets import DecimalWidget
30
from bika.lims.catalog.indexers.baseanalysis import sortable_title
31
from bika.lims.content.abstractanalysis import AbstractAnalysis
32
from bika.lims.content.abstractanalysis import schema
33
from bika.lims.content.clientawaremixin import ClientAwareMixin
34
from bika.lims.interfaces import IAnalysis
35
from bika.lims.interfaces import ICancellable
36
from bika.lims.interfaces import IInternalUse
37
from bika.lims.interfaces import IRoutineAnalysis
38
from bika.lims.interfaces.analysis import IRequestAnalysis
39
from bika.lims.workflow import getTransitionDate
40
from Products.Archetypes.Field import BooleanField
41
from Products.Archetypes.Field import FixedPointField
42
from Products.Archetypes.Field import StringField
43
from Products.Archetypes.Schema import Schema
44
from Products.ATContentTypes.utils import DT2dt
45
from Products.ATContentTypes.utils import dt2DT
46
from Products.CMFCore.permissions import View
47
from zope.interface import alsoProvides
48
from zope.interface import implements
49
from zope.interface import noLongerProvides
50
51
52
# The actual uncertainty for this analysis' result, populated when the result
53
# is submitted.
54
Uncertainty = FixedPointField(
55
    'Uncertainty',
56
    read_permission=View,
57
    write_permission="Field: Edit Result",
58
    precision=10,
59
    widget=DecimalWidget(
60
        label=_("Uncertainty")
61
    )
62
)
63
# This field keep track if the field hidden has been set manually or not. If
64
# this value is false, the system will assume the visibility of this analysis
65
# in results report will depend on the value set at AR, Profile or Template
66
# levels (see AnalysisServiceSettings fields in AR). If the value for this
67
# field is set to true, the system will assume the visibility of the analysis
68
# will only depend on the value set for the field Hidden (bool).
69
HiddenManually = BooleanField(
70
    'HiddenManually',
71
    default=False,
72
)
73
74
75
schema = schema.copy() + Schema((
76
    Uncertainty,
77
    HiddenManually,
78
))
79
80
81
class AbstractRoutineAnalysis(AbstractAnalysis, ClientAwareMixin):
82
    implements(IAnalysis, IRequestAnalysis, IRoutineAnalysis, ICancellable)
83
    security = ClassSecurityInfo()
84
    displayContentsTab = False
85
    schema = schema
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable schema does not seem to be defined.
Loading history...
86
87
    @security.public
88
    def getRequest(self):
89
        """Returns the Analysis Request this analysis belongs to.
90
        Delegates to self.aq_parent
91
        """
92
        ar = self.aq_parent
93
        return ar
94
95
    @security.public
96
    def getRequestID(self):
97
        """Used to populate catalog values.
98
        Returns the ID of the parent analysis request.
99
        """
100
        ar = self.getRequest()
101
        if ar:
102
            return ar.getId()
103
104
    @security.public
105
    def getRequestUID(self):
106
        """Returns the UID of the parent analysis request.
107
        """
108
        ar = self.getRequest()
109
        if ar:
110
            return ar.UID()
111
112
    @security.public
113
    def getRequestURL(self):
114
        """Returns the url path of the Analysis Request object this analysis
115
        belongs to. Returns None if there is no Request assigned.
116
        :return: the Analysis Request URL path this analysis belongs to
117
        :rtype: str
118
        """
119
        request = self.getRequest()
120
        if request:
121
            return request.absolute_url_path()
122
123
    def getClient(self):
124
        """Returns the Client this analysis is bound to, if any
125
        """
126
        request = self.getRequest()
127
        return request and request.getClient() or None
128
129
    @security.public
130
    def getClientOrderNumber(self):
131
        """Used to populate catalog values.
132
        Returns the ClientOrderNumber of the associated AR
133
        """
134
        request = self.getRequest()
135
        if request:
136
            return request.getClientOrderNumber()
137
138
    @security.public
139
    def getDateReceived(self):
140
        """Used to populate catalog values.
141
        Returns the date the Analysis Request this analysis belongs to was
142
        received. If the analysis was created after, then returns the date
143
        the analysis was created.
144
        """
145
        request = self.getRequest()
146
        if request:
147
            ar_date = request.getDateReceived()
148
            if ar_date and self.created() > ar_date:
149
                return self.created()
150
            return ar_date
151
        return None
152
153
    @security.public
154
    def isSampleReceived(instance):
155
        """Returns whether if the Analysis Request this analysis comes from has
156
        been received or not
157
        """
158
        return instance.getDateReceived() and True or False
159
160
    @security.public
161
    def getDatePublished(self):
162
        """Used to populate catalog values.
163
        Returns the date on which the "publish" transition was invoked on this
164
        analysis.
165
        """
166
        return getTransitionDate(self, 'publish', return_as_datetime=True)
167
168
    @security.public
169
    def getDateSampled(self):
170
        """Returns the date when the Sample was Sampled
171
        """
172
        request = self.getRequest()
173
        if request:
174
            return request.getDateSampled()
175
        return None
176
177
    @security.public
178
    def isSampleSampled(self):
179
        """Returns whether if the Analysis Request this analysis comes from has
180
        been received or not
181
        """
182
        return self.getDateSampled() and True or False
183
184
    @security.public
185
    def getStartProcessDate(self):
186
        """Returns the date time when the analysis request the analysis belongs
187
        to was received. If the analysis request hasn't yet been received,
188
        returns None
189
        Overrides getStartProcessDateTime from the base class
190
        :return: Date time when the analysis is ready to be processed.
191
        :rtype: DateTime
192
        """
193
        return self.getDateReceived()
194
195
    @security.public
196
    def getSamplePoint(self):
197
        request = self.getRequest()
198
        if request:
199
            return request.getSamplePoint()
200
        return None
201
202
    @security.public
203
    def getDueDate(self):
204
        """Used to populate getDueDate index and metadata.
205
        This calculates the difference between the time the analysis processing
206
        started and the maximum turnaround time. If the analysis has no
207
        turnaround time set or is not yet ready for proces, returns None
208
        """
209
        tat = self.getMaxTimeAllowed()
210
        if not tat:
211
            return None
212
        start = self.getStartProcessDate()
213
        if not start:
214
            return None
215
216
        # delta time when the first analysis is considered as late
217
        delta = timedelta(minutes=api.to_minutes(**tat))
218
219
        # calculated due date
220
        end = dt2DT(DT2dt(start) + delta)
221
222
        # delta is within one day, return immediately
223
        if delta.days == 0:
224
            return end
225
226
        # get the laboratory workdays
227
        setup = api.get_setup()
228
        workdays = setup.getWorkdays()
229
230
        # every day is a workday, no need for calculation
231
        if workdays == tuple(map(str, range(7))):
232
            return end
233
234
        # reset the due date to the received date, and add only for configured
235
        # workdays another day
236
        due_date = end - delta.days
237
238
        days = 0
239
        while days < delta.days:
240
            # add one day to the new due date
241
            due_date += 1
242
            # skip if the weekday is a non working day
243
            if str(due_date.asdatetime().weekday()) not in workdays:
244
                continue
245
            days += 1
246
247
        return due_date
248
249
    @security.public
250
    def getSampleType(self):
251
        request = self.getRequest()
252
        if request:
253
            return request.getSampleType()
254
        return None
255
256
    @security.public
257
    def getSampleTypeUID(self):
258
        """Used to populate catalog values.
259
        """
260
        sample_type = self.getSampleType()
261
        if sample_type:
262
            return api.get_uid(sample_type)
263
264
    @security.public
265
    def getResultsRange(self):
266
        """Returns the valid result range for this routine analysis
267
268
        A routine analysis will be considered out of range if it result falls
269
        out of the range defined in "min" and "max". If there are values set
270
        for "warn_min" and "warn_max", these are used to compute the shoulders
271
        in both ends of the range. Thus, an analysis can be out of range, but
272
        be within shoulders still.
273
274
        :return: A dictionary with keys "min", "max", "warn_min" and "warn_max"
275
        :rtype: dict
276
        """
277
        return self.getField("ResultsRange").get(self)
278
279
    @security.public
280
    def getSiblings(self, with_retests=False):
281
        """
282
        Return the siblings analyses, using the parent to which the current
283
        analysis belongs to as the source
284
        :param with_retests: If false, siblings with retests are dismissed
285
        :type with_retests: bool
286
        :return: list of siblings for this analysis
287
        :rtype: list of IAnalysis
288
        """
289
        raise NotImplementedError("getSiblings is not implemented.")
290
291
    @security.public
292
    def getCalculation(self):
293
        """Return current assigned calculation
294
        """
295
        field = self.getField("Calculation")
296
        calculation = field.get(self)
297
        if not calculation:
298
            return None
299
        return calculation
300
301
    @security.public
302
    def getDependents(self, with_retests=False, recursive=False):
303
        """
304
        Returns a list of siblings who depend on us to calculate their result.
305
        :param with_retests: If false, dependents with retests are dismissed
306
        :param recursive: If true, returns all dependents recursively down
307
        :type with_retests: bool
308
        :return: Analyses the current analysis depends on
309
        :rtype: list of IAnalysis
310
        """
311
        def is_dependent(analysis):
312
            calculation = analysis.getCalculation()
313
            if not calculation:
314
                return False
315
316
            services = calculation.getRawDependentServices()
317
            if not services:
318
                return False
319
320
            query = dict(UID=services, getKeyword=self.getKeyword())
321
            services = api.search(query, "bika_setup_catalog")
322
            return len(services) > 0
323
324
        siblings = self.getSiblings(with_retests=with_retests)
325
        dependents = filter(lambda sib: is_dependent(sib), siblings)
326
        if not recursive:
327
            return dependents
328
329
        # Return all dependents recursively
330
        deps = dependents
331
        for dep in dependents:
332
            down_dependencies = dep.getDependents(with_retests=with_retests,
333
                                                  recursive=True)
334
            deps.extend(down_dependencies)
335
        return deps
336
337
    @security.public
338
    def getDependencies(self, with_retests=False, recursive=False):
339
        """
340
        Return a list of siblings who we depend on to calculate our result.
341
        :param with_retests: If false, siblings with retests are dismissed
342
        :param recursive: If true, looks for dependencies recursively up
343
        :type with_retests: bool
344
        :return: Analyses the current analysis depends on
345
        :rtype: list of IAnalysis
346
        """
347
        calc = self.getCalculation()
348
        if not calc:
349
            return []
350
351
        # If the calculation this analysis is bound does not have analysis
352
        # keywords (only interims), no need to go further
353
        service_uids = calc.getRawDependentServices()
354
        if len(service_uids) == 0:
355
            return []
356
357
        dependencies = []
358
        for sibling in self.getSiblings(with_retests=with_retests):
359
            # We get all analyses that depend on me, also if retracted (maybe
360
            # I am one of those that are retracted!)
361
            deps = map(api.get_uid, sibling.getDependents(with_retests=True))
362
            if self.UID() in deps:
363
                dependencies.append(sibling)
364
                if recursive:
365
                    # Append the dependencies of this dependency
366
                    up_deps = sibling.getDependencies(with_retests=with_retests,
367
                                                      recursive=True)
368
                    dependencies.extend(up_deps)
369
370
        return dependencies
371
372
    @security.public
373
    def getPrioritySortkey(self):
374
        """
375
        Returns the key that will be used to sort the current Analysis, from
376
        most prioritary to less prioritary.
377
        :return: string used for sorting
378
        """
379
        analysis_request = self.getRequest()
380
        if analysis_request is None:
381
            return None
382
        ar_sort_key = analysis_request.getPrioritySortkey()
383
        ar_id = analysis_request.getId().lower()
384
        title = sortable_title(self)
385
        if callable(title):
386
            title = title()
387
        return '{}.{}.{}'.format(ar_sort_key, ar_id, title)
388
389
    @security.public
390
    def getHidden(self):
391
        """ Returns whether if the analysis must be displayed in results
392
        reports or not, as well as in analyses view when the user logged in
393
        is a Client Contact.
394
395
        If the value for the field HiddenManually is set to False, this function
396
        will delegate the action to the method getAnalysisServiceSettings() from
397
        the Analysis Request.
398
399
        If the value for the field HiddenManually is set to True, this function
400
        will return the value of the field Hidden.
401
        :return: true or false
402
        :rtype: bool
403
        """
404
        if self.getHiddenManually():
405
            return self.getField('Hidden').get(self)
406
        request = self.getRequest()
407
        if request:
408
            service_uid = self.getServiceUID()
409
            ar_settings = request.getAnalysisServiceSettings(service_uid)
410
            return ar_settings.get('hidden', False)
411
        return False
412
413
    @security.public
414
    def setHidden(self, hidden):
415
        """ Sets if this analysis must be displayed or not in results report and
416
        in manage analyses view if the user is a lab contact as well.
417
418
        The value set by using this field will have priority over the visibility
419
        criteria set at Analysis Request, Template or Profile levels (see
420
        field AnalysisServiceSettings from Analysis Request. To achieve this
421
        behavior, this setter also sets the value to HiddenManually to true.
422
        :param hidden: true if the analysis must be hidden in report
423
        :type hidden: bool
424
        """
425
        self.setHiddenManually(True)
426
        self.getField('Hidden').set(self, hidden)
427
428
    @security.public
429
    def setInternalUse(self, internal_use):
430
        """Applies the internal use of this Analysis. Analyses set for internal
431
        use are not accessible to clients and are not visible in reports
432
        """
433
        if internal_use:
434
            alsoProvides(self, IInternalUse)
435
        else:
436
            noLongerProvides(self, IInternalUse)
437
438
    def getConditions(self):
439
        """Returns the conditions of this analysis. These conditions are usually
440
        set on sample registration and are stored at sample level
441
        """
442
        sample = self.getRequest()
443
        service_uid = self.getRawAnalysisService()
444
445
        def is_valid(condition):
446
            uid = condition.get("uid")
447
            if api.is_uid(uid) and uid == service_uid:
448
                value = condition.get("value", None)
449
                if value is not None:
450
                    return "title" in condition
451
            return False
452
453
        conditions = sample.getServiceConditions()
454
        conditions = filter(is_valid, conditions)
455
        return copy.deepcopy(conditions)
456