Passed
Push — master ( caa188...9d0ad3 )
by Ramon
11:25 queued 07:12
created

bika.lims.browser.fields.aranalysesfield   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 383
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 53
eloc 193
dl 0
loc 383
rs 6.96
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
F ARAnalysesField.get() 0 57 15
A ARAnalysesField._update_price() 0 10 1
A ARAnalysesField._is_frozen() 0 16 3
A ARAnalysesField._to_service() 0 31 5
A ARAnalysesField._is_assigned_to_worksheet() 0 10 1
F ARAnalysesField.set() 0 125 16
A ARAnalysesField._get_assigned_worksheets() 0 8 1
A ARAnalysesField.Services() 0 5 1
A ARAnalysesField._update_specs() 0 26 5
A ARAnalysesField.Vocabulary() 0 8 2
A ARAnalysesField._update_interims() 0 8 1
A ARAnalysesField._get_services() 0 8 2

How to fix   Complexity   

Complexity

Complex classes like bika.lims.browser.fields.aranalysesfield 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
import itertools
9
10
from AccessControl import ClassSecurityInfo
11
from bika.lims import api, deprecated, logger
12
from bika.lims.catalog import CATALOG_ANALYSIS_LISTING
13
from bika.lims.interfaces import IAnalysis, IAnalysisService, IARAnalysesField
14
from bika.lims.permissions import ViewRetractedAnalyses
15
from bika.lims.utils.analysis import create_analysis
16
from bika.lims.workflow import getReviewHistoryActionsList
17
from Products.Archetypes.public import Field, ObjectField
18
from Products.Archetypes.Registry import registerField
19
from Products.Archetypes.utils import shasattr
20
from Products.CMFCore.utils import getToolByName
21
from zope.interface import implements
22
23
"""Field to manage Analyses on ARs
24
25
Please see the assigned doctest at tests/doctests/ARAnalysesField.rst
26
27
Run this test from the buildout directory:
28
29
    bin/test test_textual_doctests -t ARAnalysesField
30
"""
31
32
33
class ARAnalysesField(ObjectField):
34
    """A field that stores Analyses instances
35
    """
36
    implements(IARAnalysesField)
37
38
    security = ClassSecurityInfo()
39
    _properties = Field._properties.copy()
40
    _properties.update({
41
        'type': 'analyses',
42
        'default': None,
43
    })
44
45
    security.declarePrivate('get')
46
47
    def get(self, instance, **kwargs):
48
        """Returns a list of Analyses assigned to this AR
49
50
        Return a list of catalog brains unless `full_objects=True` is passed.
51
        Overrides "ViewRetractedAnalyses" when `retracted=True` is passed.
52
        Other keyword arguments are passed to bika_analysis_catalog
53
54
        :param instance: Analysis Request object
55
        :param kwargs: Keyword arguments to be passed to control the output
56
        :returns: A list of Analysis Objects/Catalog Brains
57
        """
58
59
        full_objects = False
60
        # If reflexed is false don't return the analyses that have been
61
        # reflexed, only the final ones
62
        reflexed = True
63
64
        if 'full_objects' in kwargs:
65
            full_objects = kwargs['full_objects']
66
            del kwargs['full_objects']
67
68
        if 'reflexed' in kwargs:
69
            reflexed = kwargs['reflexed']
70
            del kwargs['reflexed']
71
72
        if 'retracted' in kwargs:
73
            retracted = kwargs['retracted']
74
            del kwargs['retracted']
75
        else:
76
            mtool = getToolByName(instance, 'portal_membership')
77
            retracted = mtool.checkPermission(
78
                ViewRetractedAnalyses, instance)
79
80
        bac = getToolByName(instance, CATALOG_ANALYSIS_LISTING)
81
        contentFilter = dict([(k, v) for k, v in kwargs.items()
82
                              if k in bac.indexes()])
83
        contentFilter['portal_type'] = "Analysis"
84
        contentFilter['sort_on'] = "getKeyword"
85
        contentFilter['path'] = {'query': api.get_path(instance),
86
                                 'level': 0}
87
        analyses = bac(contentFilter)
88
        if not retracted or full_objects or not reflexed:
89
            analyses_filtered = []
90
            for a in analyses:
91
                if not retracted and a.review_state == 'retracted':
92
                    continue
93
                if full_objects or not reflexed:
94
                    a_obj = a.getObject()
95
                    # Check if analysis has been reflexed
96
                    if not reflexed and \
97
                            a_obj.getReflexRuleActionsTriggered() != '':
98
                        continue
99
                    if full_objects:
100
                        a = a_obj
101
                analyses_filtered.append(a)
102
            analyses = analyses_filtered
103
        return analyses
104
105
    security.declarePrivate('set')
106
107
    def set(self, instance, items, prices=None, specs=None, **kwargs):
108
        """Set/Assign Analyses to this AR
109
110
        :param items: List of Analysis objects/brains, AnalysisService
111
                      objects/brains and/or Analysis Service uids
112
        :type items: list
113
        :param prices: Mapping of AnalysisService UID -> price
114
        :type prices: dict
115
        :param specs: List of AnalysisService UID -> Result Range Record mappings
116
        :type specs: list
117
        :returns: list of new assigned Analyses
118
        """
119
120
        # This setter returns a list of new set Analyses
121
        new_analyses = []
122
123
        # Prevent removing all Analyses
124
        if not items:
125
            logger.warn("Not allowed to remove all Analyses from AR.")
126
            return new_analyses
127
128
        # Bail out if the items is not a list type
129
        if not isinstance(items, (list, tuple)):
130
            raise TypeError(
131
                "Items parameter must be a tuple or list, got '{}'".format(
132
                    type(items)))
133
134
        # Bail out if the AR in frozen state
135
        if self._is_frozen(instance):
136
            raise ValueError(
137
                "Analyses can not be modified for inactive/verified ARs")
138
139
        # Convert the items to a valid list of AnalysisServices
140
        services = filter(None, map(self._to_service, items))
141
142
        # Calculate dependencies
143
        # FIXME Infinite recursion error possible here, if the formula includes
144
        #       the Keyword of the Service that includes the Calculation
145
        dependencies = map(lambda s: s.getServiceDependencies(), services)
146
        dependencies = list(itertools.chain.from_iterable(dependencies))
147
148
        # Merge dependencies and services
149
        services = set(services + dependencies)
150
151
        # Service UIDs
152
        service_uids = map(api.get_uid, services)
153
154
        # Modify existing AR specs with new form values of selected analyses.
155
        self._update_specs(instance, specs)
156
157
        for service in services:
158
            keyword = service.getKeyword()
159
160
            # Create the Analysis if it doesn't exist
161
            if shasattr(instance, keyword):
162
                analysis = instance._getOb(keyword)
163
            else:
164
                # TODO Entry point for interims assignment and Calculation
165
                #      decoupling from Analysis. See comments PR#593
166
                analysis = create_analysis(instance, service)
167
                # TODO Remove when the `create_analysis` function supports this
168
                # Set the interim fields only for new created Analysis
169
                self._update_interims(analysis, service)
170
                new_analyses.append(analysis)
171
172
            # Set the price of the Analysis
173
            self._update_price(analysis, service, prices)
174
175
        # delete analyses
176
        delete_ids = []
177
        assigned_attachments = []
178
179
        for analysis in instance.objectValues('Analysis'):
180
            service_uid = analysis.getServiceUID()
181
182
            # Skip assigned Analyses
183
            if service_uid in service_uids:
184
                continue
185
186
            # Skip Analyses in frozen states
187
            if self._is_frozen(analysis, "retract"):
188
                logger.warn("Inactive/verified/retracted Analyses can not be "
189
                            "removed.")
190
                continue
191
192
            # Remember assigned attachments
193
            # https://github.com/senaite/senaite.core/issues/1025
194
            assigned_attachments.extend(analysis.getAttachment())
195
            analysis.setAttachment([])
196
197
            # If it is assigned to a worksheet, unassign it before deletion.
198
            if self._is_assigned_to_worksheet(analysis):
199
                backrefs = self._get_assigned_worksheets(analysis)
200
                ws = backrefs[0]
201
                ws.removeAnalysis(analysis)
202
203
            # Unset the partition reference
204
            part = analysis.getSamplePartition()
205
            if part:
206
                # From this partition, remove the reference to the current
207
                # analysis that is going to be removed to prevent inconsistent
208
                # states (Sample Partitions referencing to Analyses that do not
209
                # exist anymore
210
                an_uid = api.get_uid(analysis)
211
                part_ans = part.getAnalyses() or []
212
                part_ans = filter(lambda an: api.get_uid(an) != an_uid, part_ans)
0 ignored issues
show
introduced by
The variable an_uid does not seem to be defined for all execution paths.
Loading history...
213
                part.setAnalyses(part_ans)
214
            # Unset the Analysis-to-Partition reference
215
            analysis.setSamplePartition(None)
216
            delete_ids.append(analysis.getId())
217
218
        if delete_ids:
219
            # Note: subscriber might promote the AR
220
            instance.manage_delObjects(ids=delete_ids)
221
222
        # Remove orphaned attachments
223
        for attachment in assigned_attachments:
224
            # only delete attachments which are no further linked
225
            if not attachment.getLinkedAnalyses():
226
                logger.info(
227
                    "Deleting attachment: {}".format(attachment.getId()))
228
                attachment_id = api.get_id(attachment)
229
                api.get_parent(attachment).manage_delObjects(attachment_id)
230
231
        return new_analyses
232
233
    def _get_services(self, full_objects=False):
234
        """Fetch and return analysis service objects
235
        """
236
        bsc = api.get_tool('bika_setup_catalog')
237
        brains = bsc(portal_type='AnalysisService')
238
        if full_objects:
239
            return map(api.get_object, brains)
240
        return brains
241
242
    def _to_service(self, thing):
243
        """Convert to Analysis Service
244
245
        :param thing: UID/Catalog Brain/Object/Something
246
        :returns: Analysis Service object or None
247
        """
248
249
        # Convert UIDs to objects
250
        if api.is_uid(thing):
251
            thing = api.get_object_by_uid(thing, None)
252
253
        # Bail out if the thing is not a valid object
254
        if not api.is_object(thing):
255
            logger.warn("'{}' is not a valid object!".format(repr(thing)))
256
            return None
257
258
        # Ensure we have an object here and not a brain
259
        obj = api.get_object(thing)
260
261
        if IAnalysisService.providedBy(obj):
262
            return obj
263
264
        if IAnalysis.providedBy(obj):
265
            return obj.getAnalysisService()
266
267
        # An object, but neither an Analysis nor AnalysisService?
268
        # This should never happen.
269
        msg = "ARAnalysesField doesn't accept objects from {} type. " \
270
            "The object will be dismissed.".format(api.get_portal_type(obj))
271
        logger.warn(msg)
272
        return None
273
274
    def _is_frozen(self, brain_or_object, *frozen_transitions):
275
        """Check if the passed in object is frozen: the object is cancelled,
276
        inactive or has been verified at some point
277
        :param brain_or_object: Analysis or AR Brain/Object
278
        :param frozen_transitions: additional transitions that freeze the object
279
        :returns: True if the object is frozen
280
        """
281
        if not api.is_active(brain_or_object):
282
            return True
283
        object = api.get_object(brain_or_object)
284
        frozen_trans = set(frozen_transitions)
285
        frozen_trans.add('verify')
286
        performed_transitions = set(getReviewHistoryActionsList(object))
287
        if frozen_trans.intersection(performed_transitions):
288
            return True
289
        return False
290
291
    def _get_assigned_worksheets(self, analysis):
292
        """Return the assigned worksheets of this Analysis
293
294
        :param analysis: Analysis Brain/Object
295
        :returns: Worksheet Backreferences
296
        """
297
        analysis = api.get_object(analysis)
298
        return analysis.getBackReferences("WorksheetAnalysis")
299
300
    def _is_assigned_to_worksheet(self, analysis):
301
        """Check if the Analysis is assigned to a worksheet
302
303
        :param analysis: Analysis Brain/Object
304
        :returns: True if the Analysis is assigned to a WS
305
        """
306
        analysis = api.get_object(analysis)
307
        state = api.get_workflow_status_of(
308
            analysis, state_var='worksheetanalysis_review_state')
309
        return state == "assigned"
310
311
    def _update_interims(self, analysis, service):
312
        """Update Interim Fields of the Analysis
313
314
        :param analysis: Analysis Object
315
        :param service: Analysis Service Object
316
        """
317
        service_interims = service.getInterimFields()
318
        analysis.setInterimFields(service_interims)
319
320
    def _update_price(self, analysis, service, prices):
321
        """Update the Price of the Analysis
322
323
        :param analysis: Analysis Object
324
        :param service: Analysis Service Object
325
        :param prices: Price mapping
326
        """
327
        prices = prices or {}
328
        price = prices.get(service.UID(), service.getPrice())
329
        analysis.setPrice(price)
330
331
    def _update_specs(self, instance, specs):
332
        """Update AR specifications
333
334
        :param instance: Analysis Request
335
        :param specs: List of Specification Records
336
        """
337
338
        if specs is None:
339
            return
340
341
        # N.B. we copy the records here, otherwise the spec will be written to
342
        #      the attached specification of this AR
343
        rr = {item["keyword"]: item.copy()
344
              for item in instance.getResultsRange()}
345
        for spec in specs:
346
            keyword = spec.get("keyword")
347
            if keyword in rr:
348
                # overwrite the instance specification only, if the specific
349
                # analysis spec has min/max values set
350
                if all([spec.get("min"), spec.get("max")]):
351
                    rr[keyword].update(spec)
352
                else:
353
                    rr[keyword] = spec
354
            else:
355
                rr[keyword] = spec
356
        return instance.setResultsRange(rr.values())
357
358
    # DEPRECATED: The following code should not be in the field's domain
359
360
    security.declarePublic('Vocabulary')
361
362
    @deprecated("Please refactor, this method will be removed in senaite.core 1.5")
363
    def Vocabulary(self, content_instance=None):
364
        """Create a vocabulary from analysis services
365
        """
366
        vocab = []
367
        for service in self._get_services():
368
            vocab.append((api.get_uid(service), api.get_title(service)))
369
        return vocab
370
371
    security.declarePublic('Services')
372
373
    @deprecated("Please refactor, this method will be removed in senaite.core 1.5")
374
    def Services(self):
375
        """Fetch and return analysis service objects
376
        """
377
        return self._get_services(full_objects=True)
378
379
380
registerField(ARAnalysesField,
381
              title="Analyses",
382
              description="Manages Analyses of ARs")
383