Passed
Push — master ( 0606d7...2b79be )
by Ramon
03:55
created

AnalysesView.folderitem()   A

Complexity

Conditions 2

Size

Total Lines 72
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 34
dl 0
loc 72
rs 9.064
c 0
b 0
f 0
cc 2
nop 4

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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-2019 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
import json
22
from collections import OrderedDict
23
from copy import copy
24
25
from DateTime import DateTime
26
from Products.Archetypes.config import REFERENCE_CATALOG
27
from Products.CMFPlone.utils import safe_unicode
28
from bika.lims import api
29
from bika.lims import bikaMessageFactory as _
30
from bika.lims import logger
31
from bika.lims.api.analysis import get_formatted_interval
32
from bika.lims.api.analysis import is_out_of_range
33
from bika.lims.browser.bika_listing import BikaListingView
34
from bika.lims.catalog import CATALOG_ANALYSIS_LISTING
35
from bika.lims.config import LDL
36
from bika.lims.config import UDL
37
from bika.lims.interfaces import IAnalysisRequest
38
from bika.lims.interfaces import IFieldIcons
39
from bika.lims.permissions import EditFieldResults
40
from bika.lims.permissions import EditResults
41
from bika.lims.permissions import FieldEditAnalysisHidden
42
from bika.lims.permissions import FieldEditAnalysisResult
43
from bika.lims.permissions import TransitionVerify
44
from bika.lims.permissions import ViewResults
45
from bika.lims.permissions import ViewRetractedAnalyses
46
from bika.lims.utils import check_permission
47
from bika.lims.utils import formatDecimalMark
48
from bika.lims.utils import format_supsub
49
from bika.lims.utils import getUsers
50
from bika.lims.utils import get_image
51
from bika.lims.utils import get_link
52
from bika.lims.utils import t
53
from bika.lims.utils.analysis import format_uncertainty
54
from plone.memoize import view as viewcache
55
from zope.component import getAdapters
56
57
58
class AnalysesView(BikaListingView):
59
    """Displays a list of Analyses in a table.
60
61
    Visible InterimFields from all analyses are added to self.columns[].
62
    Keyword arguments are passed directly to bika_analysis_catalog.
63
    """
64
65
    def __init__(self, context, request, **kwargs):
66
        super(AnalysesView, self).__init__(context, request, **kwargs)
67
68
        # prepare the content filter of this listing
69
        self.contentFilter = dict(kwargs)
70
        self.contentFilter.update({
71
            "portal_type": "Analysis",
72
            "sort_on": "sortable_title",
73
            "sort_order": "ascending",
74
        })
75
76
        # set the listing view config
77
        self.catalog = CATALOG_ANALYSIS_LISTING
78
        self.sort_order = "ascending"
79
        self.context_actions = {}
80
81
        self.show_select_row = False
82
        self.show_select_column = False
83
        self.show_column_toggles = False
84
        self.pagesize = 9999999
85
        self.form_id = "analyses_form"
86
        self.context_active = api.is_active(context)
87
        self.interim_fields = {}
88
        self.interim_columns = OrderedDict()
89
        self.specs = {}
90
        self.bsc = api.get_tool("bika_setup_catalog")
91
        self.portal = api.get_portal()
92
        self.portal_url = api.get_url(self.portal)
93
        self.rc = api.get_tool(REFERENCE_CATALOG)
94
        self.dmk = context.bika_setup.getResultsDecimalMark()
95
        self.scinot = context.bika_setup.getScientificNotationResults()
96
        self.categories = []
97
98
        # each editable item needs it's own allow_edit
99
        # which is a list of field names.
100
        self.allow_edit = False
101
102
        self.columns = OrderedDict((
103
            # Although 'created' column is not displayed in the list (see
104
            # review_states to check the columns that will be rendered), this
105
            # column is needed to sort the list by create date
106
            ("created", {
107
                "title": _("Date Created"),
108
                "toggle": False}),
109
            ("Service", {
110
                "title": _("Analysis"),
111
                "attr": "Title",
112
                "index": "sortable_title",
113
                "sortable": False}),
114
            ("Method", {
115
                "title": _("Method"),
116
                "sortable": False,
117
                "ajax": True,
118
                "toggle": True}),
119
            ("Instrument", {
120
                "title": _("Instrument"),
121
                "ajax": True,
122
                "sortable": False,
123
                "toggle": True}),
124
            ("Analyst", {
125
                "title": _("Analyst"),
126
                "sortable": False,
127
                "ajax": True,
128
                "toggle": True}),
129
            ("state_title", {
130
                "title": _("Status"),
131
                "sortable": False}),
132
            ("DetectionLimitOperand", {
133
                "title": _("DL"),
134
                "sortable": False,
135
                "ajax": True,
136
                "autosave": True,
137
                "toggle": False}),
138
            ("Result", {
139
                "title": _("Result"),
140
                "input_width": "6",
141
                "input_class": "ajax_calculate numeric",
142
                "ajax": True,
143
                "sortable": False}),
144
            ("Specification", {
145
                "title": _("Specification"),
146
                "sortable": False}),
147
            ("Uncertainty", {
148
                "title": _("+-"),
149
                "ajax": True,
150
                "sortable": False}),
151
            ("retested", {
152
                "title": _("Retested"),
153
                "type": "boolean",
154
                "sortable": False}),
155
            ("Attachments", {
156
                "title": _("Attachments"),
157
                "sortable": False}),
158
            ("CaptureDate", {
159
                "title": _("Captured"),
160
                "index": "getResultCaptureDate",
161
                "sortable": False}),
162
            ("SubmittedBy", {
163
                "title": _("Submitter"),
164
                "sortable": False}),
165
            ("DueDate", {
166
                "title": _("Due Date"),
167
                "index": "getDueDate",
168
                "sortable": False}),
169
            ("Hidden", {
170
                "title": _("Hidden"),
171
                "toggle": True,
172
                "sortable": False,
173
                "ajax": True,
174
                "type": "boolean"}),
175
        ))
176
177
        # Inject Remarks column for listing
178
        if self.analysis_remarks_enabled():
179
            self.columns["Remarks"] = {
180
                "title": "Remarks",
181
                "toggle": False,
182
                "sortable": False,
183
                "type": "remarks",
184
                "ajax": True,
185
            }
186
187
        self.review_states = [
188
            {
189
                "id": "default",
190
                "title": _("Valid"),
191
                "contentFilter": {
192
                    "review_state": [
193
                        "registered",
194
                        "unassigned",
195
                        "assigned",
196
                        "to_be_verified",
197
                        "verified",
198
                        "published",
199
                    ]
200
                },
201
                "columns": self.columns.keys()
202
            },
203
            {
204
                "id": "invalid",
205
                "contentFilter": {
206
                    "review_state": [
207
                        "cancelled",
208
                        "retracted",
209
                        "rejected",
210
                    ]
211
                },
212
                "title": _("Invalid"),
213
                "columns": self.columns.keys(),
214
            },
215
            {
216
                "id": "all",
217
                "title": _("All"),
218
                "contentFilter": {},
219
                "columns": self.columns.keys()
220
             },
221
        ]
222
223
        # This is used to display method and instrument columns if there is at
224
        # least one analysis to be rendered that allows the assignment of
225
        # method and/or instrument
226
        self.show_methodinstr_columns = False
227
228
    def update(self):
229
        """Update hook
230
        """
231
        super(AnalysesView, self).update()
232
        self.load_analysis_categories()
233
234
    def before_render(self):
235
        """Before render hook
236
        """
237
        super(AnalysesView, self).before_render()
238
        self.request.set("disable_plone.rightcolumn", 1)
239
240
    @viewcache.memoize
241
    def analysis_remarks_enabled(self):
242
        """Check if analysis remarks are enabled
243
        """
244
        return self.context.bika_setup.getEnableAnalysisRemarks()
245
246
    @viewcache.memoize
247
    def has_permission(self, permission, obj=None):
248
        """Returns if the current user has rights for the permission passed in
249
250
        :param permission: permission identifier
251
        :param obj: object to check the permission against
252
        :return: True if the user has rights for the permission passed in
253
        """
254
        if not permission:
255
            logger.warn("None permission is not allowed")
256
            return False
257
        if obj is None:
258
            return check_permission(permission, self.context)
259
        return check_permission(permission, api.get_object(obj))
260
261
    @viewcache.memoize
262
    def is_analysis_edition_allowed(self, analysis_brain):
263
        """Returns if the analysis passed in can be edited by the current user
264
265
        :param analysis_brain: Brain that represents an analysis
266
        :return: True if the user can edit the analysis, otherwise False
267
        """
268
        if not self.context_active:
269
            # The current context must be active. We cannot edit analyses from
270
            # inside a deactivated Analysis Request, for instance
271
            return False
272
273
        analysis_obj = api.get_object(analysis_brain)
274
        if analysis_obj.getPointOfCapture() == 'field':
275
            # This analysis must be captured on field, during sampling.
276
            if not self.has_permission(EditFieldResults, analysis_obj):
277
                # Current user cannot edit field analyses.
278
                return False
279
280
        elif not self.has_permission(EditResults, analysis_obj):
281
            # The Point of Capture is 'lab' and the current user cannot edit
282
            # lab analyses.
283
            return False
284
285
        # Check if the user is allowed to enter a value to to Result field
286
        if not self.has_permission(FieldEditAnalysisResult, analysis_obj):
287
            return False
288
289
        # Is the instrument out of date?
290
        # The user can assign a result to the analysis if it does not have any
291
        # instrument assigned or the instrument assigned is valid.
292
        if not self.is_analysis_instrument_valid(analysis_brain):
293
            # return if it is allowed to enter a manual result
294
            return analysis_obj.getManualEntryOfResults()
295
296
        return True
297
298
    @viewcache.memoize
299
    def is_result_edition_allowed(self, analysis_brain):
300
        """Checks if the edition of the result field is allowed
301
302
        :param analysis_brain: Brain that represents an analysis
303
        :return: True if the user can edit the result field, otherwise False
304
        """
305
306
        # Always check general edition first
307
        if not self.is_analysis_edition_allowed(analysis_brain):
308
            return False
309
310
        # Get the ananylsis object
311
        obj = api.get_object(analysis_brain)
312
313
        if not obj.getDetectionLimitOperand():
314
            # This is a regular result (not a detection limit)
315
            return True
316
317
        # Detection limit selector is enabled in the Analysis Service
318
        if obj.getDetectionLimitSelector():
319
            # Manual detection limit entry is *not* allowed
320
            if not obj.getAllowManualDetectionLimit():
321
                return False
322
323
        return True
324
325
    @viewcache.memoize
326
    def is_uncertainty_edition_allowed(self, analysis_brain):
327
        """Checks if the edition of the uncertainty field is allowed
328
329
        :param analysis_brain: Brain that represents an analysis
330
        :return: True if the user can edit the result field, otherwise False
331
        """
332
333
        # Only allow to edit the uncertainty if result edition is allowed
334
        if not self.is_result_edition_allowed(analysis_brain):
335
            return False
336
337
        # Get the ananylsis object
338
        obj = api.get_object(analysis_brain)
339
340
        # Manual setting of uncertainty is not allowed
341
        if not obj.getAllowManualUncertainty():
342
            return False
343
344
        # Result is a detection limit -> uncertainty setting makes no sense!
345
        if obj.getDetectionLimitOperand() in [LDL, UDL]:
346
            return False
347
348
        return True
349
350
    @viewcache.memoize
351
    def is_analysis_instrument_valid(self, analysis_brain):
352
        """Return if the analysis has a valid instrument.
353
354
        If the analysis passed in is from ReferenceAnalysis type or does not
355
        have an instrument assigned, returns True
356
357
        :param analysis_brain: Brain that represents an analysis
358
        :return: True if the instrument assigned is valid or is None"""
359
        if analysis_brain.meta_type == 'ReferenceAnalysis':
360
            # If this is a ReferenceAnalysis, there is no need to check the
361
            # validity of the instrument, because this is a QC analysis and by
362
            # definition, it has the ability to promote an instrument to a
363
            # valid state if the result is correct.
364
            return True
365
        instrument = self.get_instrument(analysis_brain)
366
        return not instrument or instrument.isValid()
367
368
    def get_instrument(self, analysis_brain):
369
        """Returns the instrument assigned to the analysis passed in, if any
370
371
        :param analysis_brain: Brain that represents an analysis
372
        :return: Instrument object or None"""
373
        instrument_uid = analysis_brain.getInstrumentUID
374
        # Note we look for the instrument by using its UID, case we want the
375
        # instrument to be cached by UID so if same instrument is assigned to
376
        # several analyses, a single search for instrument will be required
377
        return self.get_object(instrument_uid)
378
379
    @viewcache.memoize
380
    def get_object(self, brain_or_object_or_uid):
381
        """Get the full content object. Returns None if the param passed in is
382
        not a valid, not a valid object or not found
383
384
        :param brain_or_object_or_uid: UID/Catalog brain/content object
385
        :returns: content object
386
        """
387
        if api.is_uid(brain_or_object_or_uid):
388
            return api.get_object_by_uid(brain_or_object_or_uid, default=None)
389
        if api.is_object(brain_or_object_or_uid):
390
            return api.get_object(brain_or_object_or_uid)
391
        return None
392
393
    @viewcache.memoize
394
    def get_methods_vocabulary(self, analysis_brain):
395
        """Returns a vocabulary with all the methods available for the passed in
396
        analysis, either those assigned to an instrument that are capable to
397
        perform the test (option "Allow Entry of Results") and those assigned
398
        manually in the associated Analysis Service.
399
400
        The vocabulary is a list of dictionaries. Each dictionary has the
401
        following structure:
402
403
            {'ResultValue': <method_UID>,
404
             'ResultText': <method_Title>}
405
406
        :param analysis_brain: A single Analysis brain
407
        :type analysis_brain: CatalogBrain
408
        :returns: A list of dicts
409
        """
410
        uids = analysis_brain.getAllowedMethodUIDs
411
        query = {'portal_type': 'Method',
412
                 'is_active': True,
413
                 'UID': uids}
414
        brains = api.search(query, 'bika_setup_catalog')
415
        if not brains:
416
            return [{'ResultValue': '', 'ResultText': _('None')}]
417
        return map(lambda brain: {'ResultValue': brain.UID,
418
                                  'ResultText': brain.Title}, brains)
419
420
    @viewcache.memoize
421
    def get_instruments_vocabulary(self, analysis_brain):
422
        """Returns a vocabulary with the valid and active instruments available
423
        for the analysis passed in.
424
425
        If the option "Allow instrument entry of results" for the Analysis
426
        is disabled, the function returns an empty vocabulary.
427
428
        If the analysis passed in is a Reference Analysis (Blank or Control),
429
        the vocabulary, the invalid instruments will be included in the
430
        vocabulary too.
431
432
        The vocabulary is a list of dictionaries. Each dictionary has the
433
        following structure:
434
435
            {'ResultValue': <instrument_UID>,
436
             'ResultText': <instrument_Title>}
437
438
        :param analysis_brain: A single Analysis or ReferenceAnalysis
439
        :type analysis_brain: Analysis or.ReferenceAnalysis
440
        :return: A vocabulary with the instruments for the analysis
441
        :rtype: A list of dicts: [{'ResultValue':UID, 'ResultText':Title}]
442
        """
443
        if not analysis_brain.getInstrumentEntryOfResults:
444
            # Instrument entry of results for this analysis is not allowed
445
            return list()
446
447
        # If the analysis is a QC analysis, display all instruments, including
448
        # those uncalibrated or for which the last QC test failed.
449
        meta_type = analysis_brain.meta_type
450
        uncalibrated = meta_type == 'ReferenceAnalysis'
451
        if meta_type == 'DuplicateAnalysis':
452
            base_analysis_type = analysis_brain.getAnalysisPortalType
453
            uncalibrated = base_analysis_type == 'ReferenceAnalysis'
454
455
        uids = analysis_brain.getAllowedInstrumentUIDs
456
        query = {'portal_type': 'Instrument',
457
                 'is_active': True,
458
                 'UID': uids}
459
        brains = api.search(query, 'bika_setup_catalog')
460
        vocab = [{'ResultValue': '', 'ResultText': _('None')}]
461
        for brain in brains:
462
            instrument = self.get_object(brain)
463
            if uncalibrated and not instrument.isOutOfDate():
464
                # Is a QC analysis, include instrument also if is not valid
465
                vocab.append({'ResultValue': instrument.UID(),
466
                              'ResultText': instrument.Title()})
467
            if instrument.isValid():
468
                # Only add the 'valid' instruments: certificate
469
                # on-date and valid internal calibration tests
470
                vocab.append({'ResultValue': instrument.UID(),
471
                              'ResultText': instrument.Title()})
472
        return vocab
473
474
    @viewcache.memoize
475
    def get_analysts(self):
476
        analysts = getUsers(self.context, ['Manager', 'LabManager', 'Analyst'])
477
        analysts = analysts.sortedByKey()
478
        results = list()
479
        for analyst_id, analyst_name in analysts.items():
480
            results.append({'ResultValue': analyst_id,
481
                            'ResultText': analyst_name})
482
        return results
483
484
    def load_analysis_categories(self):
485
        # Getting analysis categories
486
        bsc = api.get_tool('bika_setup_catalog')
487
        analysis_categories = bsc(portal_type="AnalysisCategory",
488
                                  sort_on="sortable_title")
489
        # Sorting analysis categories
490
        self.analysis_categories_order = dict([
491
            (b.Title, "{:04}".format(a)) for a, b in
492
            enumerate(analysis_categories)])
493
494
    def isItemAllowed(self, obj):
495
        """Checks if the passed in Analysis must be displayed in the list.
496
        :param obj: A single Analysis brain or content object
497
        :type obj: ATContentType/CatalogBrain
498
        :returns: True if the item can be added to the list.
499
        :rtype: bool
500
        """
501
        if not obj:
502
            return False
503
504
        # Does the user has enough privileges to see retracted analyses?
505
        if obj.review_state == 'retracted' and \
506
                not self.has_permission(ViewRetractedAnalyses):
507
            return False
508
        return True
509
510
    def folderitem(self, obj, item, index):
511
        """Prepare a data item for the listing.
512
513
        :param obj: The catalog brain or content object
514
        :param item: Listing item (dictionary)
515
        :param index: Index of the listing item
516
        :returns: Augmented listing data item
517
        """
518
519
        item['Service'] = obj.Title
520
        item['class']['service'] = 'service_title'
521
        item['service_uid'] = obj.getServiceUID
522
        item['Keyword'] = obj.getKeyword
523
        item['Unit'] = format_supsub(obj.getUnit) if obj.getUnit else ''
524
        item['retested'] = obj.getRetestOfUID and True or False
525
        item['class']['retested'] = 'center'
526
527
        # Append info link before the service
528
        # see: bika.lims.site.coffee for the attached event handler
529
        item["before"]["Service"] = get_link(
530
            "analysisservice_info?service_uid={}&analysis_uid={}"
531
            .format(obj.getServiceUID, obj.UID),
532
            value="<span class='glyphicon glyphicon-info-sign'></span>",
533
            css_class="service_info")
534
535
        # Note that getSampleTypeUID returns the type of the Sample, no matter
536
        # if the sample associated to the analysis is a regular Sample (routine
537
        # analysis) or if is a Reference Sample (Reference Analysis). If the
538
        # analysis is a duplicate, it returns the Sample Type of the sample
539
        # associated to the source analysis.
540
        item['st_uid'] = obj.getSampleTypeUID
541
542
        # Fill item's category
543
        self._folder_item_category(obj, item)
544
        # Fill item's row class
545
        self._folder_item_css_class(obj, item)
546
        # Fill result and/or result options
547
        self._folder_item_result(obj, item)
548
        # Fill calculation and interim fields
549
        self._folder_item_calculation(obj, item)
550
        # Fill method
551
        self._folder_item_method(obj, item)
552
        # Fill instrument
553
        self._folder_item_instrument(obj, item)
554
        # Fill analyst
555
        self._folder_item_analyst(obj, item)
556
        # Fill submitted by
557
        self._folder_item_submitted_by(obj, item)
558
        # Fill attachments
559
        self._folder_item_attachments(obj, item)
560
        # Fill uncertainty
561
        self._folder_item_uncertainty(obj, item)
562
        # Fill Detection Limits
563
        self._folder_item_detection_limits(obj, item)
564
        # Fill Specifications
565
        self._folder_item_specifications(obj, item)
566
        # Fill Due Date and icon if late/overdue
567
        self._folder_item_duedate(obj, item)
568
        # Fill verification criteria
569
        self._folder_item_verify_icons(obj, item)
570
        # Fill worksheet anchor/icon
571
        self._folder_item_assigned_worksheet(obj, item)
572
        # Fill reflex analysis icons
573
        self._folder_item_reflex_icons(obj, item)
574
        # Fill hidden field (report visibility)
575
        self._folder_item_report_visibility(obj, item)
576
        # Renders additional icons to be displayed
577
        self._folder_item_fieldicons(obj)
578
        # Renders remarks toggle button
579
        self._folder_item_remarks(obj, item)
580
581
        return item
582
583
    def folderitems(self):
584
        # This shouldn't be required here, but there are some views that calls
585
        # directly contents_table() instead of __call__, so before_render is
586
        # never called. :(
587
        self.before_render()
588
589
        # Gettin all the items
590
        items = super(AnalysesView, self).folderitems(classic=False)
591
592
        # the TAL requires values for all interim fields on all
593
        # items, so we set blank values in unused cells
594
        for item in items:
595
            for field in self.interim_columns:
596
                if field not in item:
597
                    item[field] = ""
598
599
        # XXX order the list of interim columns
600
        interim_keys = self.interim_columns.keys()
601
        interim_keys.reverse()
602
603
        # add InterimFields keys to columns
604
        for col_id in interim_keys:
605
            if col_id not in self.columns:
606
                self.columns[col_id] = {
607
                    "title": self.interim_columns[col_id],
608
                    "input_width": "6",
609
                    "input_class": "ajax_calculate numeric",
610
                    "sortable": False,
611
                    "toggle": True,
612
                    "ajax": True,
613
                }
614
615
        if self.allow_edit:
616
            new_states = []
617
            for state in self.review_states:
618
                # InterimFields are displayed in review_state
619
                # They are anyway available through View.columns though.
620
                # In case of hidden fields, the calcs.py should check
621
                # calcs/services
622
                # for additional InterimFields!!
623
                pos = "Result" in state["columns"] and \
624
                      state["columns"].index("Result") or len(state["columns"])
625
                for col_id in interim_keys:
626
                    if col_id not in state["columns"]:
627
                        state["columns"].insert(pos, col_id)
628
                # retested column is added after Result.
629
                pos = "Result" in state["columns"] and \
630
                      state["columns"].index("Uncertainty") + 1 or len(
631
                    state["columns"])
632
                if "retested" in state["columns"]:
633
                    state["columns"].remove("retested")
634
                state["columns"].insert(pos, "retested")
635
                new_states.append(state)
636
            self.review_states = new_states
637
            # Allow selecting individual analyses
638
            self.show_select_column = True
639
640
        if self.show_categories:
641
            self.categories = map(lambda x: x[0],
642
                                  sorted(self.categories, key=lambda x: x[1]))
643
        else:
644
            self.categories.sort()
645
646
        # self.json_specs = json.dumps(self.specs)
647
        self.json_interim_fields = json.dumps(self.interim_fields)
648
        self.items = items
649
650
        # Method and Instrument columns must be shown or hidden at the
651
        # same time, because the value assigned to one causes
652
        # a value reassignment to the other (one method can be performed
653
        # by different instruments)
654
        self.columns["Method"]["toggle"] = self.show_methodinstr_columns
655
        self.columns["Instrument"]["toggle"] = self.show_methodinstr_columns
656
657
        return items
658
659
    def _folder_item_category(self, analysis_brain, item):
660
        """Sets the category to the item passed in
661
662
        :param analysis_brain: Brain that represents an analysis
663
        :param item: analysis' dictionary counterpart that represents a row
664
        """
665
        if not self.show_categories:
666
            return
667
668
        cat = analysis_brain.getCategoryTitle
669
        item["category"] = cat
670
        cat_order = self.analysis_categories_order.get(cat)
671
        if (cat, cat_order) not in self.categories:
672
            self.categories.append((cat, cat_order))
673
674
    def _folder_item_css_class(self, analysis_brain, item):
675
        """Sets the suitable css class name(s) to `table_row_class` from the
676
        item passed in, depending on the properties of the analysis object
677
678
        :param analysis_brain: Brain that represents an analysis
679
        :param item: analysis' dictionary counterpart that represents a row
680
        """
681
        meta_type = analysis_brain.meta_type
682
683
        # Default css names for table_row_class
684
        css_names = item.get('table_row_class', '').split()
685
        css_names.extend(['state-{}'.format(analysis_brain.review_state),
686
                          'type-{}'.format(meta_type.lower())])
687
688
        if meta_type == 'ReferenceAnalysis':
689
            css_names.append('qc-analysis')
690
691
        elif meta_type == 'DuplicateAnalysis':
692
            if analysis_brain.getAnalysisPortalType == 'ReferenceAnalysis':
693
                css_names.append('qc-analysis')
694
695
        item['table_row_class'] = ' '.join(css_names)
696
697
    def _folder_item_duedate(self, analysis_brain, item):
698
        """Set the analysis' due date to the item passed in.
699
700
        :param analysis_brain: Brain that represents an analysis
701
        :param item: analysis' dictionary counterpart that represents a row
702
        """
703
704
        # Note that if the analysis is a Reference Analysis, `getDueDate`
705
        # returns the date when the ReferenceSample expires. If the analysis is
706
        # a duplicate, `getDueDate` returns the due date of the source analysis
707
        due_date = analysis_brain.getDueDate
708
        if not due_date:
709
            return None
710
        due_date_str = self.ulocalized_time(due_date, long_format=0)
711
        item['DueDate'] = due_date_str
712
713
        # If the Analysis is late/overdue, display an icon
714
        capture_date = analysis_brain.getResultCaptureDate
715
        capture_date = capture_date or DateTime()
716
        if capture_date > due_date:
717
            # The analysis is late or overdue
718
            img = get_image('late.png', title=t(_("Late Analysis")),
719
                            width='16px', height='16px')
720
            item['replace']['DueDate'] = '{} {}'.format(due_date_str, img)
721
722
    def _folder_item_result(self, analysis_brain, item):
723
        """Set the analysis' result to the item passed in.
724
725
        :param analysis_brain: Brain that represents an analysis
726
        :param item: analysis' dictionary counterpart that represents a row
727
        """
728
729
        item["Result"] = ""
730
731
        if not self.has_permission(ViewResults, analysis_brain):
732
            # If user has no permissions, don"t display the result but an icon
733
            img = get_image("to_follow.png", width="16px", height="16px")
734
            item["before"]["Result"] = img
735
            return
736
737
        result = analysis_brain.getResult
738
        capture_date = analysis_brain.getResultCaptureDate
739
        capture_date_str = self.ulocalized_time(capture_date, long_format=0)
740
741
        item["Result"] = result
742
        item["CaptureDate"] = capture_date_str
743
        item["result_captured"] = capture_date_str
744
        item["string_result"] = False
745
746
        # Edit mode enabled of this Analysis
747
        if self.is_analysis_edition_allowed(analysis_brain):
748
            # Allow to set Remarks
749
            item["allow_edit"].append("Remarks")
750
751
            # Set the results field editable
752
            if self.is_result_edition_allowed(analysis_brain):
753
                item["allow_edit"].append("Result")
754
755
            # Prepare result options
756
            choices = analysis_brain.getResultOptions
757
            if choices:
758
                # N.B.we copy here the list to avoid persistent changes
759
                choices = copy(choices)
760
                # By default set empty as the default selected choice
761
                choices.insert(0, dict(ResultValue="", ResultText=""))
762
                item["choices"]["Result"] = choices
763
            else:
764
                # If not choices, set whether the result must be floatable
765
                obj = self.get_object(analysis_brain)
766
                item["string_result"] = obj.getStringResult()
767
768
        if not result:
769
            return
770
771
        obj = self.get_object(analysis_brain)
772
        formatted_result = obj.getFormattedResult(
773
            sciformat=int(self.scinot), decimalmark=self.dmk)
774
        item["formatted_result"] = formatted_result
775
776
    def _folder_item_calculation(self, analysis_brain, item):
777
        """Set the analysis' calculation and interims to the item passed in.
778
779
        :param analysis_brain: Brain that represents an analysis
780
        :param item: analysis' dictionary counterpart that represents a row
781
        """
782
783
        if not self.has_permission(ViewResults, analysis_brain):
784
            # Hide interims and calculation if user cannot view results
785
            return
786
787
        is_editable = self.is_analysis_edition_allowed(analysis_brain)
788
789
        # Set interim fields. Note we add the key 'formatted_value' to the list
790
        # of interims the analysis has already assigned.
791
        interim_fields = analysis_brain.getInterimFields or list()
792
793
        for interim_field in interim_fields:
794
            interim_keyword = interim_field.get('keyword', '')
795
            if not interim_keyword:
796
                continue
797
798
            interim_value = interim_field.get("value", "")
799
            interim_formatted = formatDecimalMark(interim_value, self.dmk)
800
            interim_field['formatted_value'] = interim_formatted
801
            item[interim_keyword] = interim_field
802
            item['class'][interim_keyword] = 'interim'
803
804
            # Note: As soon as we have a separate content type for field
805
            #       analysis, we can solely rely on the field permission
806
            #       "senaite.core: Field: Edit Analysis Result"
807
            if is_editable:
808
                if self.has_permission(FieldEditAnalysisResult, analysis_brain):
809
                    item['allow_edit'].append(interim_keyword)
810
811
            # Add this analysis' interim fields to the interim_columns list
812
            interim_hidden = interim_field.get('hidden', False)
813
            if not interim_hidden:
814
                interim_title = interim_field.get('title')
815
                self.interim_columns[interim_keyword] = interim_title
816
817
        item['interimfields'] = interim_fields
818
        self.interim_fields[analysis_brain.UID] = interim_fields
819
820
        # Set calculation
821
        calculation_uid = analysis_brain.getCalculationUID
822
        has_calculation = calculation_uid and True or False
823
        item['calculation'] = has_calculation
824
825
    def _folder_item_method(self, analysis_brain, item):
826
        """Fills the analysis' method to the item passed in.
827
828
        :param analysis_brain: Brain that represents an analysis
829
        :param item: analysis' dictionary counterpart that represents a row
830
        """
831
832
        is_editable = self.is_analysis_edition_allowed(analysis_brain)
833
        method_title = analysis_brain.getMethodTitle
834
        item['Method'] = method_title or ''
835
        if is_editable:
836
            method_vocabulary = self.get_methods_vocabulary(analysis_brain)
837
            if method_vocabulary:
838
                item['Method'] = analysis_brain.getMethodUID
839
                item['choices']['Method'] = method_vocabulary
840
                item['allow_edit'].append('Method')
841
                self.show_methodinstr_columns = True
842
        elif method_title:
843
            item['replace']['Method'] = get_link(analysis_brain.getMethodURL,
844
                                                 method_title)
845
            self.show_methodinstr_columns = True
846
847
    def _folder_item_instrument(self, analysis_brain, item):
848
        """Fills the analysis' instrument to the item passed in.
849
850
        :param analysis_brain: Brain that represents an analysis
851
        :param item: analysis' dictionary counterpart that represents a row
852
        """
853
        item['Instrument'] = ''
854
        if not analysis_brain.getInstrumentEntryOfResults:
855
            # Manual entry of results, instrument is not allowed
856
            item['Instrument'] = _('Manual')
857
            item['replace']['Instrument'] = \
858
                '<a href="#">{}</a>'.format(t(_('Manual')))
859
            return
860
861
        # Instrument can be assigned to this analysis
862
        is_editable = self.is_analysis_edition_allowed(analysis_brain)
863
        self.show_methodinstr_columns = True
864
        instrument = self.get_instrument(analysis_brain)
865
        if is_editable:
866
            # Edition allowed
867
            voc = self.get_instruments_vocabulary(analysis_brain)
868
            if voc:
869
                # The service has at least one instrument available
870
                item['Instrument'] = instrument.UID() if instrument else ''
871
                item['choices']['Instrument'] = voc
872
                item['allow_edit'].append('Instrument')
873
                return
874
875
        if instrument:
876
            # Edition not allowed
877
            instrument_title = instrument and instrument.Title() or ''
878
            instrument_link = get_link(instrument.absolute_url(),
879
                                       instrument_title)
880
            item['Instrument'] = instrument_title
881
            item['replace']['Instrument'] = instrument_link
882
            return
883
884
    def _folder_item_analyst(self, obj, item):
885
        is_editable = self.is_analysis_edition_allowed(obj)
886
        if not is_editable:
887
            item['Analyst'] = obj.getAnalystName
888
            return
889
890
        # Analyst is editable
891
        item['Analyst'] = obj.getAnalyst or api.get_current_user().id
892
        item['choices']['Analyst'] = self.get_analysts()
893
894
    def _folder_item_submitted_by(self, obj, item):
895
        submitted_by = obj.getSubmittedBy
896
        if submitted_by:
897
            user = self.get_user_by_id(submitted_by)
898
            user_name = user and user.getProperty("fullname") or submitted_by
899
            item['SubmittedBy'] = user_name
900
901
    @viewcache.memoize
902
    def get_user_by_id(self, user_id):
903
        return api.get_user(user_id)
904
905
    def _folder_item_attachments(self, obj, item):
906
        item['Attachments'] = ''
907
        attachment_uids = obj.getAttachmentUIDs
908
        if not attachment_uids:
909
            return
910
911
        if not self.has_permission(ViewResults, obj):
912
            return
913
914
        attachments_html = []
915
        attachments = api.search({'UID': attachment_uids}, 'uid_catalog')
916
        for attachment in attachments:
917
            attachment = api.get_object(attachment)
918
            uid = api.get_uid(attachment)
919
            html = '<span class="attachment" attachment_uid="{}">'.format(uid)
920
            attachments_html.append(html)
921
922
            at_file = attachment.getAttachmentFile()
923
            icon = at_file.icon
924
            if callable(icon):
925
                icon = icon()
926
            if icon:
927
                html = '<img src="{}/{}">'.format(self.portal_url, icon)
928
                attachments_html.append(html)
929
930
            url = '{}/at_download/AttachmentFile'
931
            url = url.format(attachment.absolute_url())
932
            link = get_link(url, at_file.filename)
933
            attachments_html.append(link)
934
935
            if not self.is_analysis_edition_allowed(obj):
936
                attachments_html.append('<br/></span>')
937
                continue
938
939
            img = '<img class="deleteAttachmentButton"' \
940
                  ' attachment_uid="{}" src="{}"/>'
941
            img = img.format(uid, '++resource++bika.lims.images/delete.png')
942
            attachments_html.append(img)
943
            attachments_html.append('<br/></span>')
944
945
        if attachments_html:
946
            # Remove the last <br/></span> and add only </span>
947
            attachments_html = attachments_html[:-1]
948
            attachments_html.append('</span>')
949
            item['replace']['Attachments'] = ''.join(attachments_html)
950
951
    def _folder_item_uncertainty(self, analysis_brain, item):
952
        """Fills the analysis' uncertainty to the item passed in.
953
954
        :param analysis_brain: Brain that represents an analysis
955
        :param item: analysis' dictionary counterpart that represents a row
956
        """
957
958
        item["Uncertainty"] = ""
959
960
        if not self.has_permission(ViewResults, analysis_brain):
961
            return
962
963
        result = analysis_brain.getResult
964
965
        obj = self.get_object(analysis_brain)
966
        formatted = format_uncertainty(obj, result, decimalmark=self.dmk,
967
                                       sciformat=int(self.scinot))
968
        if formatted:
969
            item["Uncertainty"] = formatted
970
        else:
971
            item["Uncertainty"] = obj.getUncertainty(result)
972
973
        if self.is_uncertainty_edition_allowed(analysis_brain):
974
            item["allow_edit"].append("Uncertainty")
975
976
    def _folder_item_detection_limits(self, analysis_brain, item):
977
        """Fills the analysis' detection limits to the item passed in.
978
979
        :param analysis_brain: Brain that represents an analysis
980
        :param item: analysis' dictionary counterpart that represents a row
981
        """
982
        item["DetectionLimitOperand"] = ""
983
984
        if not self.is_analysis_edition_allowed(analysis_brain):
985
            # Return immediately if the we are not in edit mode
986
            return
987
988
        # TODO: Performance, we wake-up the full object here
989
        obj = self.get_object(analysis_brain)
990
991
        # No Detection Limit Selection
992
        if not obj.getDetectionLimitSelector():
993
            return None
994
995
        # Show Detection Limit Operand Selector
996
        item["DetectionLimitOperand"] = obj.getDetectionLimitOperand()
997
        item["allow_edit"].append("DetectionLimitOperand")
998
        self.columns["DetectionLimitOperand"]["toggle"] = True
999
1000
        # Prepare selection list for LDL/UDL
1001
        choices = [
1002
            {"ResultValue": "", "ResultText": ""},
1003
            {"ResultValue": LDL, "ResultText": LDL},
1004
            {"ResultValue": UDL, "ResultText": UDL}
1005
        ]
1006
        # Set the choices to the item
1007
        item["choices"]["DetectionLimitOperand"] = choices
1008
1009
    def _folder_item_specifications(self, analysis_brain, item):
1010
        """Set the results range to the item passed in"""
1011
        # Everyone can see valid-ranges
1012
        item['Specification'] = ''
1013
        results_range = analysis_brain.getResultsRange
1014
        if not results_range:
1015
            return
1016
1017
        # Display the specification interval
1018
        item["Specification"] = get_formatted_interval(results_range, "")
1019
1020
        # Show an icon if out of range
1021
        out_range, out_shoulders = is_out_of_range(analysis_brain)
1022
        if not out_range:
1023
            return
1024
        # At least is out of range
1025
        img = get_image("exclamation.png", title=_("Result out of range"))
1026
        if not out_shoulders:
1027
            img = get_image("warning.png", title=_("Result in shoulder range"))
1028
        self._append_html_element(item, "Result", img)
1029
1030
    def _folder_item_verify_icons(self, analysis_brain, item):
1031
        """Set the analysis' verification icons to the item passed in.
1032
1033
        :param analysis_brain: Brain that represents an analysis
1034
        :param item: analysis' dictionary counterpart that represents a row
1035
        """
1036
        submitter = analysis_brain.getSubmittedBy
1037
        if not submitter:
1038
            # This analysis hasn't yet been submitted, no verification yet
1039
            return
1040
1041
        if analysis_brain.review_state == 'retracted':
1042
            # Don't display icons and additional info about verification
1043
            return
1044
1045
        verifiers = analysis_brain.getVerificators
1046
        in_verifiers = submitter in verifiers
1047
        if in_verifiers:
1048
            # If analysis has been submitted and verified by the same person,
1049
            # display a warning icon
1050
            msg = t(_("Submitted and verified by the same user: {}"))
1051
            icon = get_image('warning.png', title=msg.format(submitter))
1052
            self._append_html_element(item, 'state_title', icon)
1053
1054
        num_verifications = analysis_brain.getNumberOfRequiredVerifications
1055
        if num_verifications > 1:
1056
            # More than one verification required, place an icon and display
1057
            # the number of verifications done vs. total required
1058
            done = analysis_brain.getNumberOfVerifications
1059
            pending = num_verifications - done
1060
            ratio = float(done) / float(num_verifications) if done > 0 else 0
1061
            ratio = int(ratio * 100)
1062
            scale = ratio == 0 and 0 or (ratio / 25) * 25
1063
            anchor = "<a href='#' title='{} &#13;{} {}' " \
1064
                     "class='multi-verification scale-{}'>{}/{}</a>"
1065
            anchor = anchor.format(t(_("Multi-verification required")),
1066
                                   str(pending),
1067
                                   t(_("verification(s) pending")),
1068
                                   str(scale),
1069
                                   str(done),
1070
                                   str(num_verifications))
1071
            self._append_html_element(item, 'state_title', anchor)
1072
1073
        if analysis_brain.review_state != 'to_be_verified':
1074
            # The verification of analysis has already been done or first
1075
            # verification has not been done yet. Nothing to do
1076
            return
1077
1078
        # Check if the user has "Bika: Verify" privileges
1079
        if not self.has_permission(TransitionVerify):
1080
            # User cannot verify, do nothing
1081
            return
1082
1083
        username = api.get_current_user().id
1084
        if username not in verifiers:
1085
            # Current user has not verified this analysis
1086
            if submitter != username:
1087
                # Current user is neither a submitter nor a verifier
1088
                return
1089
1090
            # Current user is the same who submitted the result
1091
            if analysis_brain.isSelfVerificationEnabled:
1092
                # Same user who submitted can verify
1093
                title = t(_("Can verify, but submitted by current user"))
1094
                html = get_image('warning.png', title=title)
1095
                self._append_html_element(item, 'state_title', html)
1096
                return
1097
1098
            # User who submitted cannot verify
1099
            title = t(_("Cannot verify, submitted by current user"))
1100
            html = get_image('submitted-by-current-user.png', title=title)
1101
            self._append_html_element(item, 'state_title', html)
1102
            return
1103
1104
        # This user verified this analysis before
1105
        multi_verif = self.context.bika_setup.getTypeOfmultiVerification()
1106
        if multi_verif != 'self_multi_not_cons':
1107
            # Multi verification by same user is not allowed
1108
            title = t(_("Cannot verify, was verified by current user"))
1109
            html = get_image('submitted-by-current-user.png', title=title)
1110
            self._append_html_element(item, 'state_title', html)
1111
            return
1112
1113
        # Multi-verification by same user, but non-consecutively, is allowed
1114
        if analysis_brain.getLastVerificator != username:
1115
            # Current user was not the last user to verify
1116
            title = t(
1117
                _("Can verify, but was already verified by current user"))
1118
            html = get_image('warning.png', title=title)
1119
            self._append_html_element(item, 'state_title', html)
1120
            return
1121
1122
        # Last user who verified is the same as current user
1123
        title = t(_("Cannot verify, last verified by current user"))
1124
        html = get_image('submitted-by-current-user.png', title=title)
1125
        self._append_html_element(item, 'state_title', html)
1126
        return
1127
1128
    def _folder_item_assigned_worksheet(self, analysis_brain, item):
1129
        """Adds an icon to the item dict if the analysis is assigned to a
1130
        worksheet and if the icon is suitable for the current context
1131
1132
        :param analysis_brain: Brain that represents an analysis
1133
        :param item: analysis' dictionary counterpart that represents a row
1134
        """
1135
        if not IAnalysisRequest.providedBy(self.context):
1136
            # We want this icon to only appear if the context is an AR
1137
            return
1138
1139
        analysis_obj = self.get_object(analysis_brain)
1140
        worksheet = analysis_obj.getWorksheet()
1141
        if not worksheet:
1142
            # No worksheet assigned. Do nothing
1143
            return
1144
1145
        title = t(_("Assigned to: ${worksheet_id}",
1146
                    mapping={'worksheet_id': safe_unicode(worksheet.id)}))
1147
        img = get_image('worksheet.png', title=title)
1148
        anchor = get_link(worksheet.absolute_url(), img)
1149
        self._append_html_element(item, 'state_title', anchor)
1150
1151
    def _folder_item_reflex_icons(self, analysis_brain, item):
1152
        """Adds an icon to the item dictionary if the analysis has been
1153
        automatically generated due to a reflex rule
1154
1155
        :param analysis_brain: Brain that represents an analysis
1156
        :param item: analysis' dictionary counterpart that represents a row
1157
        """
1158
        if not analysis_brain.getIsReflexAnalysis:
1159
            # Do nothing
1160
            return
1161
        img = get_image('reflexrule.png',
1162
                        title=t(_('It comes form a reflex rule')))
1163
        self._append_html_element(item, 'Service', img)
1164
1165
    def _folder_item_report_visibility(self, analysis_brain, item):
1166
        """Set if the hidden field can be edited (enabled/disabled)
1167
1168
        :analysis_brain: Brain that represents an analysis
1169
        :item: analysis' dictionary counterpart to be represented as a row"""
1170
        # Users that can Add Analyses to an Analysis Request must be able to
1171
        # set the visibility of the analysis in results report, also if the
1172
        # current state of the Analysis Request (e.g. verified) does not allow
1173
        # the edition of other fields. Note that an analyst has no privileges
1174
        # by default to edit this value, cause this "visibility" field is
1175
        # related with results reporting and/or visibility from the client
1176
        # side. This behavior only applies to routine analyses, the visibility
1177
        # of QC analyses is managed in publish and are not visible to clients.
1178
        if 'Hidden' not in self.columns:
1179
            return
1180
1181
        full_obj = self.get_object(analysis_brain)
1182
        item['Hidden'] = full_obj.getHidden()
1183
        if self.has_permission(FieldEditAnalysisHidden, obj=full_obj):
1184
            item['allow_edit'].append('Hidden')
1185
1186
    def _folder_item_fieldicons(self, analysis_brain):
1187
        """Resolves if field-specific icons must be displayed for the object
1188
        passed in.
1189
1190
        :param analysis_brain: Brain that represents an analysis
1191
        """
1192
        full_obj = self.get_object(analysis_brain)
1193
        uid = api.get_uid(full_obj)
1194
        for name, adapter in getAdapters((full_obj,), IFieldIcons):
1195
            alerts = adapter()
1196
            if not alerts or uid not in alerts:
1197
                continue
1198
            alerts = alerts[uid]
1199
            if uid not in self.field_icons:
1200
                self.field_icons[uid] = alerts
1201
                continue
1202
            self.field_icons[uid].extend(alerts)
1203
1204
    def _folder_item_remarks(self, analysis_brain, item):
1205
        """Renders the Remarks field for the passed in analysis
1206
1207
        If the edition of the analysis is permitted, adds the field into the
1208
        list of editable fields.
1209
1210
        :param analysis_brain: Brain that represents an analysis
1211
        :param item: analysis' dictionary counterpart that represents a row
1212
        """
1213
1214
        if self.analysis_remarks_enabled():
1215
            item["Remarks"] = analysis_brain.getRemarks
1216
1217
        if self.is_analysis_edition_allowed(analysis_brain):
1218
            item["allow_edit"].extend(["Remarks"])
1219
1220
    def _append_html_element(self, item, element, html, glue="&nbsp;",
1221
                             after=True):
1222
        """Appends an html value after or before the element in the item dict
1223
1224
        :param item: dictionary that represents an analysis row
1225
        :param element: id of the element the html must be added thereafter
1226
        :param html: element to append
1227
        :param glue: glue to use for appending
1228
        :param after: if the html content must be added after or before"""
1229
        position = after and 'after' or 'before'
1230
        item[position] = item.get(position, {})
1231
        original = item[position].get(element, '')
1232
        if not original:
1233
            item[position][element] = html
1234
            return
1235
        item[position][element] = glue.join([original, html])
1236