Passed
Push — master ( 8785a0...af2087 )
by Jordi
05:32
created

bika.lims.browser.worksheet.views.analyses   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 552
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 40
eloc 334
dl 0
loc 552
rs 9.2
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A AnalysesView.isItemAllowed() 0 14 2
A AnalysesView.is_analysis_remarks_enabled() 0 5 1
A AnalysesView.get_uids_strpositions() 0 35 4
B AnalysesView._get_slots() 0 16 6
A AnalysesView.get_slot_header() 0 20 1
A AnalysesView.get_item_position() 0 15 2
A AnalysesView.folderitem() 0 52 2
B AnalysesView.__init__() 0 87 2
C AnalysesView.get_slot_header_data() 0 115 5
A AnalysesView.get_item_slot() 0 12 2
A AnalysesView.skip_item_key() 0 8 2
A AnalysesView.get_empty_slots() 0 9 1
A AnalysesView.folderitems() 0 26 1
A AnalysesView.fill_slots_headers() 0 24 4
A AnalysesView.get_occupied_slots() 0 8 1
A AnalysesView.fill_empty_slots() 0 36 2
A AnalysesView.render_remarks_tag() 0 22 2

How to fix   Complexity   

Complexity

Complex classes like bika.lims.browser.worksheet.views.analyses 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 collections
9
from operator import itemgetter
10
11
from bika.lims import api
12
from bika.lims import bikaMessageFactory as _
13
from bika.lims import logger
14
from bika.lims.browser.analyses import AnalysesView as BaseView
15
from bika.lims.utils import get_image
16
from bika.lims.utils import t
17
from bika.lims.utils import to_int
18
from plone.memoize import view
19
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
20
from bika.lims.interfaces import IRoutineAnalysis
21
from bika.lims.interfaces import IReferenceAnalysis
22
from bika.lims.interfaces import IDuplicateAnalysis
23
24
25
class AnalysesView(BaseView):
26
    """Manage Results View for Worksheet Analyses
27
    """
28
29
    def __init__(self, context, request):
30
        super(AnalysesView, self).__init__(context, request)
31
32
        self.context = context
33
        self.request = request
34
35
        self.analyst = None
36
        self.instrument = None
37
38
        self.contentFilter = {
39
            "getWorksheetUID": api.get_uid(context),
40
            "sort_on": "sortable_title",
41
        }
42
43
        self.icon = "{}/{}".format(
44
            self.portal_url,
45
            "++resource++bika.lims.images/worksheet_big.png"
46
        )
47
48
        self.allow_edit = True
49
        self.show_categories = False
50
        self.expand_all_categories = False
51
        self.show_search = False
52
53
        self.bika_setup = api.get_bika_setup()
54
        self.uids_strpositions = self.get_uids_strpositions()
55
        self.items_rowspans = dict()
56
57
        self.columns = collections.OrderedDict((
58
            ("Pos", {
59
                "sortable": False,
60
                "title": _("Position")}),
61
            ("Service", {
62
                "sortable": False,
63
                "title": _("Analysis")}),
64
            ("Method", {
65
                "sortable": False,
66
                "title": _("Method")}),
67
            ("Instrument", {
68
                "sortable": False,
69
                "title": _("Instrument")}),
70
            ("DetectionLimit", {
71
                "title": _("DL"),
72
                "sortable": False,
73
                "toggle": False}),
74
            ("Result", {
75
                "title": _("Result"),
76
                "ajax": True,
77
                "sortable": False}),
78
            ("retested", {
79
                "title": get_image("retested.png", title=t(_("Retested"))),
80
                "toggle": False,
81
                "type": "boolean"}),
82
            ("Uncertainty", {
83
                "sortable": False,
84
                "title": _("+-")}),
85
            ("DueDate", {
86
                "sortable": False,
87
                "title": _("Due Date")}),
88
            ("state_title", {
89
                "sortable": False,
90
                "title": _("State")}),
91
            ("Attachments", {
92
                "sortable": False,
93
                "title": _("Attachments")}),
94
        ))
95
96
        # Inject Remarks column for listing
97
        if self.is_analysis_remarks_enabled():
98
            self.columns["Remarks"] = {
99
                "title": "Remarks",
100
                "toggle": False,
101
                "sortable": False,
102
                "type": "remarks"
103
            }
104
105
        self.review_states = [
106
            {
107
                "id": "default",
108
                "title": _("All"),
109
                "contentFilter": {},
110
                "transitions": [
111
                    {"id": "submit"},
112
                    {"id": "verify"},
113
                    {"id": "retract"},
114
                    {"id": "unassign"}],
115
                "columns": self.columns.keys(),
116
            },
117
        ]
118
119
    @view.memoize
120
    def is_analysis_remarks_enabled(self):
121
        """Check if analysis remarks are enabled
122
        """
123
        return self.context.bika_setup.getEnableAnalysisRemarks()
124
125
    def isItemAllowed(self, obj):
126
        """Returns true if the current analysis to be rendered has a slot
127
        assigned for the current layout.
128
129
        :param obj: analysis to be rendered as a row in the list
130
        :type obj: ATContentType/DexterityContentType
131
        :return: True if the obj has an slot assigned. Otherwise, False.
132
        :rtype: bool
133
        """
134
        uid = api.get_uid(obj)
135
        if not self.get_item_slot(uid):
136
            logger.warning("Slot not assigned to item %s" % uid)
137
            return False
138
        return BaseView.isItemAllowed(self, obj)
139
140
    def folderitem(self, obj, item, index):
141
        """Applies new properties to the item (analysis) that is currently
142
        being rendered as a row in the list.
143
144
        :param obj: analysis to be rendered as a row in the list
145
        :param item: dict representation of the analysis, suitable for the list
146
        :param index: current position of the item within the list
147
        :type obj: ATContentType/DexterityContentType
148
        :type item: dict
149
        :type index: int
150
        :return: the dict representation of the item
151
        :rtype: dict
152
        """
153
        item = super(AnalysesView, self).folderitem(obj, item, index)
154
        item_obj = api.get_object(obj)
155
        uid = item["uid"]
156
157
        # Slot is the row position where all analyses sharing the same parent
158
        # (eg. AnalysisRequest, SampleReference), will be displayed as a group
159
        slot = self.get_item_slot(uid)
160
        item["Pos"] = slot
161
162
        # The position string contains both the slot + the position of the
163
        # analysis within the slot: "position_sortkey" will be used to sort all
164
        # the analyses to be displayed in the list
165
        str_position = self.uids_strpositions[uid]
166
        item["pos_sortkey"] = str_position
167
168
        item["colspan"] = {"Pos": 1}
169
        item["Service"] = item_obj.Title()
170
        item["Category"] = item_obj.getCategoryTitle()
171
        item["DueDate"] = self.ulocalized_time(item_obj, long_format=0)
172
        item["class"]["Service"] = "service_title"
173
174
        # To prevent extra loops, we compute here the number of analyses to be
175
        # rendered within each slot. This information will be useful later for
176
        # applying rowspan to the first cell of each slot, that contains info
177
        # about the parent of all the analyses contained in that slot (e.g
178
        # Analysis Request ID, Sample Type, etc.)
179
        rowspans = self.items_rowspans.get(slot, 0) + 1
180
181
        remarks_enabled = self.is_analysis_remarks_enabled()
182
        if remarks_enabled:
183
            # Increase in one unit the rowspan, cause the comment field for
184
            # this analysis will be rendered in a new row, below the row that
185
            # displays the current item
186
            rowspans = rowspans + 1
187
        # We map this rowspan information in items_rowspan, that will be used
188
        # later during the rendereing of slot headers (first cell of each row)
189
        self.items_rowspans[slot] = rowspans
190
191
        return item
192
193
    def folderitems(self):
194
        """Returns an array of dictionaries, each dictionary represents an
195
        analysis row to be rendered in the list. The array returned is sorted
196
        in accordance with the layout positions set for the analyses this
197
        worksheet contains when the analyses were added in the worksheet.
198
199
        :returns: list of dicts with the items to be rendered in the list
200
        """
201
        items = BaseView.folderitems(self)
202
203
        # Fill empty positions from the layout with fake rows. The worksheet
204
        # can be generated by making use of a WorksheetTemplate, so there is
205
        # the chance that some slots of this worksheet being empty. We need to
206
        # render a row still, at lest to display the slot number (Pos)
207
        self.fill_empty_slots(items)
208
209
        # Sort the items in accordance with the layout
210
        items = sorted(items, key=itemgetter("pos_sortkey"))
211
212
        # Fill the slot header cells (first cell of each row). Each slot
213
        # contains the analyses that belong to the same parent
214
        # (AnalysisRequest, ReferenceSample), so the information about the
215
        # parent must be displayed in the first cell of each slot.
216
        self.fill_slots_headers(items)
217
218
        return items
219
220
    def get_uids_strpositions(self):
221
        """Returns a dict with the positions of each analysis within the
222
        current worksheet in accordance with the current layout. The key of the
223
        dict is the uid of the analysis and the value is an string
224
        representation of the full position of the analysis within the list:
225
226
            {<analysis_uid>: '<slot_number>:<position_within_slot>',}
227
228
        :returns: a dictionary with the full position within the worksheet of
229
                  all analyses defined in the current layout.
230
        """
231
        uids_positions = dict()
232
        layout = self.context.getLayout()
233
        layout = layout and layout or []
234
        # Map the analysis uids with their positions.
235
        occupied = []
236
        next_positions = {}
237
        for item in layout:
238
            uid = item.get("analysis_uid", "")
239
            slot = int(item["position"])
240
            occupied.append(slot)
241
            position = next_positions.get(slot, 1)
242
            str_position = "{:010}:{:010}".format(slot, position)
243
            next_positions[slot] = position + 1
244
            uids_positions[uid] = str_position
245
246
        # Fill empties
247
        last_slot = max(occupied) if occupied else 1
248
        empties = [num for num in range(1, last_slot) if num not in occupied]
249
        for empty_slot in empties:
250
            str_position = "{:010}:{:010}".format(empty_slot, 1)
251
            uid = "empty-{}".format(empty_slot)
252
            uids_positions[uid] = str_position
253
254
        return uids_positions
255
256
    def get_item_position(self, analysis_uid):
257
        """Returns a list with the position for the analysis_uid passed in
258
        within the current worksheet in accordance with the current layout,
259
        where the first item from the list returned is the slot and the second
260
        is the position of the analysis within the slot.
261
262
        :param analysis_uid: uid of the analysis the position is requested
263
        :return: the position (slot + position within slot) of the analysis
264
        :rtype: list
265
        """
266
        str_position = self.uids_strpositions.get(analysis_uid, "")
267
        tokens = str_position.split(":")
268
        if len(tokens) != 2:
269
            return None
270
        return [to_int(tokens[0]), to_int(tokens[1])]
271
272
    def get_item_slot(self, analysis_uid):
273
        """Returns the slot number where the analysis must be rendered. An slot
274
        contains all analyses that belong to the same parent (AnalysisRequest,
275
        ReferenceSample).
276
277
        :param analysis_uid: the uid of the analysis the slot is requested
278
        :return: the slot number where the analysis must be rendered
279
        """
280
        position = self.get_item_position(analysis_uid)
281
        if not position:
282
            return None
283
        return position[0]
284
285
    def _get_slots(self, empty_uid=False):
286
        """Returns a list with the position number of the slots that are
287
        occupied (if empty_uid=False) or are empty (if empty_uid=True)
288
289
        :param empty_uid: True exclude occupied slots. False excludes empties
290
        :return: sorted list with slot numbers
291
        """
292
        slots = list()
293
        for uid, position in self.uids_strpositions.items():
294
            if empty_uid and not uid.startswith("empty-"):
295
                continue
296
            elif not empty_uid and uid.startswith("empty-"):
297
                continue
298
            tokens = position.split(":")
299
            slots.append(to_int(tokens[0]))
300
        return sorted(list(set(slots)))
301
302
    def get_occupied_slots(self):
303
        """Returns the list of occupied slots, those that have at least one
304
        analysis assigned according to the current layout. Delegates to
305
        self._get_slots(empty_uid=False)
306
307
        :return: list of slot numbers that at least have one analysis assigned
308
        """
309
        return self._get_slots(empty_uid=False)
310
311
    def get_empty_slots(self):
312
        """Returns the list of empty slots, those that don't have any analysis
313
        assigned according to the current layout.
314
315
        Delegates to self._get_slots(empty_uid=True)
316
317
        :return: list of slot numbers that don't have any analysis assigned
318
        """
319
        return self._get_slots(empty_uid=True)
320
321
    def fill_empty_slots(self, items):
322
        """Append dicts to the items passed in for those slots that don't have
323
        any analysis assigned but the row needs to be rendered still.
324
325
        :param items: dictionary with the items to be rendered in the list
326
        """
327
        for pos in self.get_empty_slots():
328
            item = {
329
                "obj": self.context,
330
                "id": self.context.id,
331
                "uid": self.context.UID(),
332
                "title": self.context.Title(),
333
                "type_class": "blank-worksheet-row",
334
                "url": self.context.absolute_url(),
335
                "relative_url": self.context.absolute_url(),
336
                "view_url": self.context.absolute_url(),
337
                "path": "/".join(self.context.getPhysicalPath()),
338
                "before": {},
339
                "after": {},
340
                "replace": {
341
                    "Pos": "<span class='badge'>{}</span> {}".format(
342
                        pos, _("Reassignable Slot"))
343
                },
344
                "choices": {},
345
                "class": {},
346
                "state_class": "state-empty",
347
                "allow_edit": [],
348
                "Pos": pos,
349
                "pos_sortkey": "{:010}:{:010}".format(pos, 1),
350
                "Service": "",
351
                "Attachments": "",
352
                "state_title": "",
353
                "disabled": True,
354
            }
355
356
            items.append(item)
357
358
    def fill_slots_headers(self, items):
359
        """Generates the header cell for each slot. For each slot, the first
360
        cell displays information about the parent all analyses within that
361
        given slot have in common, such as the AR Id, SampleType, etc.
362
363
        :param items: dictionary with items to be rendered in the list
364
        """
365
        prev_position = 0
366
        for item in items:
367
            item_position = item["Pos"]
368
            if item_position == prev_position:
369
                item = self.skip_item_key(item, "Pos")
370
                # head slot already filled
371
                continue
372
            if item.get("disabled", False):
373
                # empty slot
374
                continue
375
376
            # This is the first analysis found for the given position, add the
377
            # slot info in there and apply a rowspan accordingly.
378
            rowspan = self.items_rowspans.get(item_position, 1)
379
            prev_position = item_position
380
            item["rowspan"] = {"Pos": rowspan}
381
            item["replace"]["Pos"] = self.get_slot_header(item)
382
383
    def skip_item_key(self, item, key):
384
        """Add the key to the item's "skip" list
385
        """
386
        if "skip" in item:
387
            item["skip"].append(key)
388
        else:
389
            item["skip"] = [key]
390
        return item
391
392
    def get_slot_header(self, item):
393
        """Generates a slot header (the first cell of the row) for the item
394
395
        :param item: the item for which the slot header is requested
396
        :return: the html contents to be displayed in the first cell of a slot
397
        """
398
        obj = item["obj"]
399
        obj = api.get_object(obj)
400
401
        # Prepare the template data
402
        data = {
403
            "obj": obj,
404
            "item": item,
405
            "position": item["Pos"],
406
        }
407
        # update the data
408
        data.update(self.get_slot_header_data(obj))
409
410
        template = ViewPageTemplateFile("../templates/slot_header.pt")
411
        return template(self, data=data)
412
413
    def get_slot_header_data(self, obj):
414
        """Prepare the data for the slot header template
415
        """
416
417
        item_obj = None
418
        item_title = ""
419
        item_url = ""
420
        item_img = ""
421
        item_img_url = ""
422
        item_img_text = ""
423
        additional_item_icons = []
424
425
        parent_obj = None
426
        parent_title = ""
427
        parent_url = ""
428
        parent_img = ""
429
        parent_img_text = ""
430
        additional_parent_icons = []
431
432
        sample_type_obj = None
433
        sample_type_title = ""
434
        sample_type_url = ""
435
        sample_type_img = ""
436
        sample_type_img_text = ""
437
438
        if IDuplicateAnalysis.providedBy(obj):
439
            # item
440
            request = obj.getRequest()
441
            item_obj = request
442
            item_title = api.get_id(request)
443
            item_url = api.get_url(request)
444
            item_img = "duplicate.png"
445
            item_img_url = api.get_url(request)
446
            item_img_text = t(_("Duplicate"))
447
            # additional item icons
448
            additional_item_icons.append(
449
                self.render_remarks_tag(request))
450
            # parent
451
            client = request.getClient()
452
            parent_obj = client
453
            parent_title = api.get_title(client)
454
            parent_url = api.get_url(client)
455
            parent_img = "client.png"
456
            parent_img_text = t(_("Client"))
457
            # sample type
458
            sample_type = request.getSampleType()
459
            sample_type_title = request.getSampleTypeTitle()
460
            sample_type_url = api.get_url(sample_type)
461
            sample_type_img = "sampletype.png"
462
            sample_type_img_text = t(_("Sample Type"))
463
        elif IReferenceAnalysis.providedBy(obj):
464
            # item
465
            sample = obj.getSample()
466
            item_obj = sample
467
            item_title = api.get_id(sample)
468
            item_url = api.get_url(sample)
469
            item_img_url = api.get_url(sample)
470
            item_img = "control.png"
471
            item_img_text = t(_("Control"))
472
            if obj.getReferenceType() == "b":
473
                item_img = "blank.png"
474
                item_img_text = t(_("Duplicate"))
475
            # parent
476
            supplier = obj.getSupplier()
477
            parent_obj = supplier
478
            parent_title = api.get_title(supplier)
479
            parent_url = api.get_url(supplier)
480
            parent_img = "supplier.png"
481
            parent_img_text = t(_("Supplier"))
482
        elif IRoutineAnalysis.providedBy(obj):
483
            # item
484
            request = obj.getRequest()
485
            item_obj = request
486
            item_title = api.get_id(request)
487
            item_url = api.get_url(request)
488
            item_img = "analysisrequest.png"
489
            item_img_url = api.get_url(request)
490
            item_img_text = t(_("Analysis Request"))
491
            # additional item icons
492
            additional_item_icons.append(
493
                self.render_remarks_tag(request))
494
            # parent
495
            client = obj.getClient()
496
            parent_obj = client
497
            parent_title = api.get_title(client)
498
            parent_url = api.get_url(client)
499
            parent_img = "client.png"
500
            parent_img_text = t(_("Client"))
501
            # sample type
502
            sample_type = obj.getSampleType()
503
            sample_type_title = obj.getSampleTypeTitle()
504
            sample_type_url = api.get_url(sample_type)
505
            sample_type_img = "sampletype.png"
506
            sample_type_img_text = t(_("Sample Type"))
507
508
        return {
509
            # item
510
            "item_obj": item_obj,
511
            "item_title": item_title,
512
            "item_url": item_url,
513
            "item_img": get_image(item_img, title=item_img_text),
514
            "item_img_url": item_img_url,
515
            "additional_item_icons": additional_item_icons,
516
            # parent
517
            "parent_obj": parent_obj,
518
            "parent_title": parent_title,
519
            "parent_url": parent_url,
520
            "parent_img": get_image(parent_img, title=parent_img_text),
521
            "additional_parent_icons": additional_parent_icons,
522
            # sample type
523
            "sample_type_obj": sample_type_obj,
524
            "sample_type_title": sample_type_title,
525
            "sample_type_url": sample_type_url,
526
            "sample_type_img": get_image(
527
                sample_type_img, title=sample_type_img_text),
528
        }
529
530
    def render_remarks_tag(self, ar):
531
        """Renders a remarks image icon
532
        """
533
        if not ar.getRemarks():
534
            return ""
535
536
        uid = api.get_uid(ar)
537
        url = ar.absolute_url()
538
        title = ar.Title()
539
        tooltip = _("Remarks of {}").format(title)
540
541
        # Note: The 'href' is picked up by the overlay handler, see
542
        #       bika.lims.worksheet.coffee
543
        attrs = {
544
            "css_class": "slot-remarks",
545
            "style": "cursor: pointer;",
546
            "title": tooltip,
547
            "uid": uid,
548
            "href": "{}/base_view".format(url),
549
        }
550
551
        return get_image("remarks_ico.png", **attrs)
552