Passed
Push — master ( 7eb218...6d6a8c )
by Ramon
04:59
created

bika.lims.browser.partition_magic   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 359
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 53
eloc 232
dl 0
loc 359
rs 6.96
c 0
b 0
f 0

20 Methods

Rating   Name   Duplication   Size   Complexity  
A PartitionMagicView.push_primary_analyses_for_removal() 0 6 1
D PartitionMagicView.__call__() 0 63 12
A PartitionMagicView.get_sampletypes() 0 11 1
A PartitionMagicView.remove_primary_analyses() 0 7 2
A PartitionMagicView.add_status_message() 0 4 1
A PartitionMagicView.to_super_model() 0 6 1
A PartitionMagicView.__init__() 0 6 1
A PartitionMagicView.redirect() 0 8 3
A PartitionMagicView.get_ar_data() 0 12 2
A PartitionMagicView.get_sampletype_data() 0 6 2
B PartitionMagicView.create_partition() 0 47 5
A PartitionMagicView.get_object_by_uid() 0 8 2
A PartitionMagicView.is_proxy_field() 0 4 1
A PartitionMagicView.get_number_of_partitions_for() 0 19 4
A PartitionMagicView.is_computed_field() 0 4 1
A PartitionMagicView.get_objects() 0 12 3
A PartitionMagicView.get_specifications_for() 0 7 2
A PartitionMagicView.get_analysis_data_for() 0 13 2
B PartitionMagicView.get_template_data_for() 0 38 6
A PartitionMagicView.get_base_info() 0 15 1

How to fix   Complexity   

Complexity

Complex classes like bika.lims.browser.partition_magic 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
from collections import OrderedDict
4
from collections import defaultdict
5
6
from bika.lims import bikaMessageFactory as _
7
from bika.lims import logger
8
from bika.lims.decorators import returns_super_model
9
from bika.lims.workflow import doActionFor
10
from bika.lims import api
11
from bika.lims.interfaces import IProxyField
12
from bika.lims.utils.analysisrequest import create_analysisrequest as crar
13
from Products.Five.browser import BrowserView
14
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
15
16
DEFAULT_NUMBER_OF_PARTITIONS = 0
17
18
PARTITION_SKIP_FIELDS = [
19
    "Analyses",
20
    "Attachment",
21
    "Client",
22
    "Profile",
23
    "Profiles",
24
    "RejectionReasons",
25
    "Remarks",
26
    "ResultsInterpretation",
27
    "Sample",
28
    "SampleType",
29
    "Template",
30
    "creation_date",
31
    "id",
32
    "modification_date",
33
    "ParentAnalysisRequest",
34
]
35
36
37
class PartitionMagicView(BrowserView):
38
    """Manage Partitions of primary ARs
39
    """
40
    template = ViewPageTemplateFile("templates/partition_magic.pt")
41
42
    def __init__(self, context, request):
43
        super(PartitionMagicView, self).__init__(context, request)
44
        self.context = context
45
        self.request = request
46
        self.back_url = self.context.absolute_url()
47
        self.analyses_to_remove = dict()
48
49
50
    def __call__(self):
51
        form = self.request.form
52
53
        # Form submit toggle
54
        form_submitted = form.get("submitted", False)
55
56
        # Buttons
57
        form_preview = form.get("button_preview", False)
58
        form_create = form.get("button_create", False)
59
        form_cancel = form.get("button_cancel", False)
60
61
        objs = self.get_objects()
62
63
        # No ARs selected
64
        if not objs:
65
            return self.redirect(message=_("No items selected"),
66
                                 level="warning")
67
68
        # Handle preview
69
        if form_submitted and form_preview:
70
            logger.info("*** PREVIEW ***")
71
72
        # Handle create
73
        if form_submitted and form_create:
74
            logger.info("*** CREATE PARTITIONS ***")
75
76
            partitions = []
77
78
            # create the partitions
79
            for partition in form.get("partitions", []):
80
                primary_uid = partition.get("primary_uid")
81
                sampletype_uid = partition.get("sampletype_uid")
82
                analyses_uids = partition.get("analyses")
83
                if not analyses_uids or not primary_uid:
84
                    # Cannot create a partition w/o analyses!
85
                    continue
86
87
                partition = self.create_partition(
88
                    primary_uid, sampletype_uid, analyses_uids)
89
                partitions.append(partition)
90
                logger.info("Successfully created partition: {}".format(
91
                    api.get_path(partition)))
92
93
                # Force the reception of the partition
94
                doActionFor(partition, "receive")
95
96
            if not partitions:
97
                # If no partitions were created, show a warning message
98
                return self.redirect(message=_("No partitions were created"))
99
100
            # Remove analyses from primary Analysis Requests
101
            self.remove_primary_analyses()
102
103
            message = _("Created {} partitions: {}".format(
104
                len(partitions), ", ".join(map(api.get_title, partitions))))
105
            return self.redirect(message=message)
106
107
        # Handle cancel
108
        if form_submitted and form_cancel:
109
            logger.info("*** CANCEL ***")
110
            return self.redirect(message=_("Partitioning canceled"))
111
112
        return self.template()
113
114
    def create_partition(self, primary_uid, sampletype_uid, analyses_uids):
115
        """Create a new partition (AR)
116
        """
117
        logger.info("*** CREATE PARTITION ***")
118
119
        ar = self.get_object_by_uid(primary_uid)
120
        record = {
121
            "InternalUse": True,
122
            "ParentAnalysisRequest": primary_uid,
123
            "SampleType": sampletype_uid,
124
        }
125
126
        for fieldname, field in api.get_fields(ar).items():
127
            # if self.is_proxy_field(field):
128
            #     logger.info("Skipping proxy field {}".format(fieldname))
129
            #     continue
130
            if self.is_computed_field(field):
131
                logger.info("Skipping computed field {}".format(fieldname))
132
                continue
133
            if fieldname in PARTITION_SKIP_FIELDS:
134
                logger.info("Skipping field {}".format(fieldname))
135
                continue
136
            fieldvalue = field.get(ar)
137
            record[fieldname] = fieldvalue
138
            logger.info("Update record '{}': {}".format(
139
                fieldname, repr(fieldvalue)))
140
141
        client = ar.getClient()
142
        analyses = map(self.get_object_by_uid, analyses_uids)
143
        services = map(lambda an: an.getAnalysisService(), analyses)
144
145
        partition = crar(
146
            client,
147
            self.request,
148
            record,
149
            analyses=services,
150
            specifications=self.get_specifications_for(ar)
151
        )
152
153
        # Remove selected analyses from the parent Analysis Request
154
        self.push_primary_analyses_for_removal(ar, analyses)
155
156
        # Reindex Parent Analysis Request
157
        # TODO Workflow - AnalysisRequest - Partitions creation
158
        ar.reindexObject(idxs=["isRootAncestor"])
159
160
        return partition
161
162
    def push_primary_analyses_for_removal(self, analysis_request, analyses):
163
        """Stores the analyses to be removed after partitions creation
164
        """
165
        to_remove = self.analyses_to_remove.get(analysis_request, [])
166
        to_remove.extend(analyses)
167
        self.analyses_to_remove[analysis_request] = to_remove
168
169
    def remove_primary_analyses(self):
170
        """Remove analyses relocated to partitions
171
        """
172
        for ar, analyses in self.analyses_to_remove.items():
173
            analyses_ids = list(set(map(api.get_id, analyses)))
174
            ar.manage_delObjects(analyses_ids)
175
        self.analyses_to_remove = dict()
176
177
    def get_specifications_for(self, ar):
178
        """Returns a mapping of service uid -> specification
179
        """
180
        spec = ar.getSpecification()
181
        if not spec:
182
            return []
183
        return spec.getResultsRange()
184
185
    def is_proxy_field(self, field):
186
        """Checks if the field is a proxy field
187
        """
188
        return IProxyField.providedBy(field)
189
190
    def is_computed_field(self, field):
191
        """Checks if the field is a coumputed field
192
        """
193
        return field.type == "computed"
194
195
    def get_ar_data(self):
196
        """Returns a list of AR data
197
        """
198
        for obj in self.get_objects():
199
            info = self.get_base_info(obj)
200
            info.update({
201
                "analyses": self.get_analysis_data_for(obj),
202
                "sampletype": self.get_base_info(obj.getSampleType()),
203
                "number_of_partitions": self.get_number_of_partitions_for(obj),
204
                "template": self.get_template_data_for(obj),
205
            })
206
            yield info
207
208
    def get_sampletype_data(self):
209
        """Returns a list of SampleType data
210
        """
211
        for obj in self.get_sampletypes():
212
            info = self.get_base_info(obj)
213
            yield info
214
215
    def get_objects(self):
216
        """Returns a list of objects coming from the "uids" request parameter
217
        """
218
        # Create a mapping of source ARs for copy
219
        uids = self.request.form.get("uids", "")
220
        if not uids:
221
            # check for the `items` parammeter
222
            uids = self.request.form.get("items", "")
223
        if isinstance(uids, basestring):
224
            uids = uids.split(",")
225
        unique_uids = OrderedDict().fromkeys(uids).keys()
226
        return filter(None, map(self.get_object_by_uid, unique_uids))
227
228
    def get_sampletypes(self):
229
        """Returns the available SampleTypes of the system
230
        """
231
        query = {
232
            "portal_type": "SampleType",
233
            "sort_on": "sortable_title",
234
            "sort_order": "ascending",
235
            "inactive_state": "active",
236
        }
237
        results = api.search(query, "bika_setup_catalog")
238
        return map(api.get_object, results)
239
240
    @returns_super_model
241
    def to_super_model(self, obj_or_objs):
242
        """Returns a SuperModel for a given object or a list of Supermodels if
243
        a list of objects was passed in
244
        """
245
        return obj_or_objs
246
247
    def get_analysis_data_for(self, ar):
248
        """Return the Analysis data for this AR
249
        """
250
        # Exclude analyses from children (partitions)
251
        analyses = ar.objectValues("Analysis")
252
        out = []
253
        for an in analyses:
254
            info = self.get_base_info(an)
255
            info.update({
256
                "service_uid": an.getServiceUID(),
257
            })
258
            out.append(info)
259
        return out
260
261
    def get_template_data_for(self, ar):
262
        """Return the Template data for this AR
263
        """
264
        info = None
265
        template = ar.getTemplate()
266
        ar_sampletype_uid = api.get_uid(ar.getSampleType())
267
268
        if template:
269
            info = self.get_base_info(template)
270
271
            analyses = template.getAnalyses()
272
            partition_analyses = map(
273
                lambda x: (x.get("partition"), x.get("service_uid")), analyses)
274
275
            analyses_by_partition = defaultdict(list)
276
            for partition, service_uid in partition_analyses:
277
                analyses_by_partition[partition].append(service_uid)
278
279
            sampletypes_by_partition = defaultdict(list)
280
            for part in template.getPartitions():
281
                part_id = part.get("part_id")
282
                sampletype_uid = part.get('sampletype_uid', ar_sampletype_uid)
283
                sampletypes_by_partition[part_id] = sampletype_uid
284
285
            partitions = map(lambda p: p.get("part_id"),
286
                             template.getPartitions())
287
            info.update({
288
                "analyses": analyses_by_partition,
289
                "partitions": partitions,
290
                "sample_types": sampletypes_by_partition,
291
            })
292
        else:
293
            info = {
294
                "analyses": {},
295
                "partitions": [],
296
                "sample_types": {},
297
            }
298
        return info
299
300
    def get_number_of_partitions_for(self, ar):
301
        """Return the number of selected partitions
302
        """
303
        # fetch the number of partitions from the request
304
        uid = api.get_uid(ar)
305
        num = self.request.get("primary", {}).get(uid)
306
307
        if num is None:
308
            # get the number of partitions from the template
309
            template = ar.getTemplate()
310
            if template:
311
                num = len(template.getPartitions())
312
            else:
313
                num = DEFAULT_NUMBER_OF_PARTITIONS
314
        try:
315
            num = int(num)
316
        except (TypeError, ValueError):
317
            num = DEFAULT_NUMBER_OF_PARTITIONS
318
        return num
319
320
    def get_base_info(self, obj):
321
        """Extract the base info from the given object
322
        """
323
        obj = api.get_object(obj)
324
        review_state = api.get_workflow_status_of(obj)
325
        state_title = review_state.capitalize().replace("_", " ")
326
        return {
327
            "obj": obj,
328
            "id": api.get_id(obj),
329
            "uid": api.get_uid(obj),
330
            "title": api.get_title(obj),
331
            "path": api.get_path(obj),
332
            "url": api.get_url(obj),
333
            "review_state": review_state,
334
            "state_title": state_title,
335
        }
336
337
    def redirect(self, redirect_url=None, message=None, level="info"):
338
        """Redirect with a message
339
        """
340
        if redirect_url is None:
341
            redirect_url = self.back_url
342
        if message is not None:
343
            self.add_status_message(message, level)
344
        return self.request.response.redirect(redirect_url)
345
346
    def get_object_by_uid(self, uid):
347
        """Get the object by UID
348
        """
349
        logger.debug("get_object_by_uid::UID={}".format(uid))
350
        obj = api.get_object_by_uid(uid, None)
351
        if obj is None:
352
            logger.warn("!! No object found for UID #{} !!")
353
        return obj
354
355
    def add_status_message(self, message, level="info"):
356
        """Set a portal status message
357
        """
358
        return self.context.plone_utils.addPortalMessage(message, level)
359