Passed
Push — 2.x ( 3f40bd...d670c9 )
by Ramon
05:54 queued 01:27
created

AnalysesView._folder_item_uncertainty()   A

Complexity

Conditions 4

Size

Total Lines 24
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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