Passed
Pull Request — 2.x (#1854)
by Jordi
05:32
created

AnalysesView._folder_item_conditions()   A

Complexity

Conditions 3

Size

Total Lines 11
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 9
dl 0
loc 11
rs 9.95
c 0
b 0
f 0
cc 3
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
609
        # Append info link before the service
610
        # see: bika.lims.site.coffee for the attached event handler
611
        item["before"]["Service"] = get_link(
612
            "analysisservice_info?service_uid={}&analysis_uid={}"
613
            .format(obj.getServiceUID, obj.UID),
614
            value="<i class='fas fa-info-circle'></i>",
615
            css_class="service_info", tabindex="-1")
616
617
        # Note that getSampleTypeUID returns the type of the Sample, no matter
618
        # if the sample associated to the analysis is a regular Sample (routine
619
        # analysis) or if is a Reference Sample (Reference Analysis). If the
620
        # analysis is a duplicate, it returns the Sample Type of the sample
621
        # associated to the source analysis.
622
        item['st_uid'] = obj.getSampleTypeUID
623
624
        # Fill item's category
625
        self._folder_item_category(obj, item)
626
        # Fill item's row class
627
        self._folder_item_css_class(obj, item)
628
        # Fill result and/or result options
629
        self._folder_item_result(obj, item)
630
        # Fill calculation and interim fields
631
        self._folder_item_calculation(obj, item)
632
        # Fill method
633
        self._folder_item_method(obj, item)
634
        # Fill instrument
635
        self._folder_item_instrument(obj, item)
636
        # Fill analyst
637
        self._folder_item_analyst(obj, item)
638
        # Fill submitted by
639
        self._folder_item_submitted_by(obj, item)
640
        # Fill attachments
641
        self._folder_item_attachments(obj, item)
642
        # Fill uncertainty
643
        self._folder_item_uncertainty(obj, item)
644
        # Fill Detection Limits
645
        self._folder_item_detection_limits(obj, item)
646
        # Fill Specifications
647
        self._folder_item_specifications(obj, item)
648
        self._folder_item_out_of_range(obj, item)
649
        self._folder_item_result_range_compliance(obj, item)
650
        # Fill Partition
651
        self._folder_item_partition(obj, item)
652
        # Fill Due Date and icon if late/overdue
653
        self._folder_item_duedate(obj, item)
654
        # Fill verification criteria
655
        self._folder_item_verify_icons(obj, item)
656
        # Fill worksheet anchor/icon
657
        self._folder_item_assigned_worksheet(obj, item)
658
        # Fill accredited icon
659
        self._folder_item_accredited_icon(obj, item)
660
        # Fill hidden field (report visibility)
661
        self._folder_item_report_visibility(obj, item)
662
        # Renders additional icons to be displayed
663
        self._folder_item_fieldicons(obj)
664
        # Renders remarks toggle button
665
        self._folder_item_remarks(obj, item)
666
        # Renders the analysis conditions
667
        self._folder_item_conditions(obj, item)
668
669
        return item
670
671
    def folderitems(self):
672
        # This shouldn't be required here, but there are some views that calls
673
        # directly contents_table() instead of __call__, so before_render is
674
        # never called. :(
675
        self.before_render()
676
677
        # Gettin all the items
678
        items = super(AnalysesView, self).folderitems()
679
680
        # the TAL requires values for all interim fields on all
681
        # items, so we set blank values in unused cells
682
        for item in items:
683
            for field in self.interim_columns:
684
                if field not in item:
685
                    item[field] = ""
686
687
        # XXX order the list of interim columns
688
        interim_keys = self.interim_columns.keys()
689
        interim_keys.reverse()
690
691
        # add InterimFields keys to columns
692
        for col_id in interim_keys:
693
            if col_id not in self.columns:
694
                self.columns[col_id] = {
695
                    "title": self.interim_columns[col_id],
696
                    "input_width": "6",
697
                    "input_class": "ajax_calculate numeric",
698
                    "sortable": False,
699
                    "toggle": True,
700
                    "ajax": True,
701
                }
702
703
        if self.allow_edit:
704
            new_states = []
705
            for state in self.review_states:
706
                # InterimFields are displayed in review_state
707
                # They are anyway available through View.columns though.
708
                # In case of hidden fields, the calcs.py should check
709
                # calcs/services
710
                # for additional InterimFields!!
711
                pos = "Result" in state["columns"] and \
712
                      state["columns"].index("Result") or len(state["columns"])
713
                for col_id in interim_keys:
714
                    if col_id not in state["columns"]:
715
                        state["columns"].insert(pos, col_id)
716
                # retested column is added after Result.
717
                pos = "Result" in state["columns"] and \
718
                      state["columns"].index("Uncertainty") + 1 or len(
719
                    state["columns"])
720
                if "retested" in state["columns"]:
721
                    state["columns"].remove("retested")
722
                state["columns"].insert(pos, "retested")
723
                new_states.append(state)
724
            self.review_states = new_states
725
            # Allow selecting individual analyses
726
            self.show_select_column = True
727
728
        if self.show_categories:
729
            self.categories = map(lambda x: x[0],
730
                                  sorted(self.categories, key=lambda x: x[1]))
731
        else:
732
            self.categories.sort()
733
734
        # self.json_specs = json.dumps(self.specs)
735
        self.json_interim_fields = json.dumps(self.interim_fields)
736
        self.items = items
737
738
        # Method and Instrument columns must be shown or hidden at the
739
        # same time, because the value assigned to one causes
740
        # a value reassignment to the other (one method can be performed
741
        # by different instruments)
742
        self.columns["Method"]["toggle"] = self.show_methodinstr_columns
743
        self.columns["Instrument"]["toggle"] = self.show_methodinstr_columns
744
745
        return items
746
747
    def _folder_item_category(self, analysis_brain, item):
748
        """Sets the category to the item passed in
749
750
        :param analysis_brain: Brain that represents an analysis
751
        :param item: analysis' dictionary counterpart that represents a row
752
        """
753
        if not self.show_categories:
754
            return
755
756
        cat = analysis_brain.getCategoryTitle
757
        item["category"] = cat
758
        cat_order = self.analysis_categories_order.get(cat)
759
        if (cat, cat_order) not in self.categories:
760
            self.categories.append((cat, cat_order))
761
762
    def _folder_item_css_class(self, analysis_brain, item):
763
        """Sets the suitable css class name(s) to `table_row_class` from the
764
        item passed in, depending on the properties of the analysis object
765
766
        :param analysis_brain: Brain that represents an analysis
767
        :param item: analysis' dictionary counterpart that represents a row
768
        """
769
        meta_type = analysis_brain.meta_type
770
771
        # Default css names for table_row_class
772
        css_names = item.get('table_row_class', '').split()
773
        css_names.extend(['state-{}'.format(analysis_brain.review_state),
774
                          'type-{}'.format(meta_type.lower())])
775
776
        if meta_type == 'ReferenceAnalysis':
777
            css_names.append('qc-analysis')
778
779
        elif meta_type == 'DuplicateAnalysis':
780
            if analysis_brain.getAnalysisPortalType == 'ReferenceAnalysis':
781
                css_names.append('qc-analysis')
782
783
        item['table_row_class'] = ' '.join(css_names)
784
785
    def _folder_item_duedate(self, analysis_brain, item):
786
        """Set the analysis' due date to the item passed in.
787
788
        :param analysis_brain: Brain that represents an analysis
789
        :param item: analysis' dictionary counterpart that represents a row
790
        """
791
792
        # Note that if the analysis is a Reference Analysis, `getDueDate`
793
        # returns the date when the ReferenceSample expires. If the analysis is
794
        # a duplicate, `getDueDate` returns the due date of the source analysis
795
        due_date = analysis_brain.getDueDate
796
        if not due_date:
797
            return None
798
        due_date_str = self.ulocalized_time(due_date, long_format=0)
799
        item['DueDate'] = due_date_str
800
801
        # If the Analysis is late/overdue, display an icon
802
        capture_date = analysis_brain.getResultCaptureDate
803
        capture_date = capture_date or DateTime()
804
        if capture_date > due_date:
805
            # The analysis is late or overdue
806
            img = get_image('late.png', title=t(_("Late Analysis")),
807
                            width='16px', height='16px')
808
            item['replace']['DueDate'] = '{} {}'.format(due_date_str, img)
809
810
    def _folder_item_result(self, analysis_brain, item):
811
        """Set the analysis' result to the item passed in.
812
813
        :param analysis_brain: Brain that represents an analysis
814
        :param item: analysis' dictionary counterpart that represents a row
815
        """
816
817
        item["Result"] = ""
818
819
        if not self.has_permission(ViewResults, analysis_brain):
820
            # If user has no permissions, don"t display the result but an icon
821
            img = get_image("to_follow.png", width="16px", height="16px")
822
            item["before"]["Result"] = img
823
            return
824
825
        result = analysis_brain.getResult
826
        capture_date = analysis_brain.getResultCaptureDate
827
        capture_date_str = self.ulocalized_time(capture_date, long_format=0)
828
829
        item["Result"] = result
830
        item["CaptureDate"] = capture_date_str
831
        item["result_captured"] = capture_date_str
832
833
        # Get the analysis object
834
        obj = self.get_object(analysis_brain)
835
836
        # Edit mode enabled of this Analysis
837
        if self.is_analysis_edition_allowed(analysis_brain):
838
            # Allow to set Remarks
839
            item["allow_edit"].append("Remarks")
840
841
            # Set the results field editable
842
            if self.is_result_edition_allowed(analysis_brain):
843
                item["allow_edit"].append("Result")
844
845
            # Prepare result options
846
            choices = obj.getResultOptions()
847
            if choices:
848
                # N.B.we copy here the list to avoid persistent changes
849
                choices = copy(choices)
850
                choices_type = obj.getResultOptionsType()
851
                if choices_type == "select":
852
                    # By default set empty as the default selected choice
853
                    choices.insert(0, dict(ResultValue="", ResultText=""))
854
                item["choices"]["Result"] = choices
855
                item["result_type"] = choices_type
856
857
            elif obj.getStringResult():
858
                item["result_type"] = "string"
859
860
            else:
861
                item["result_type"] = "numeric"
862
863
        if not result:
864
            return
865
866
        formatted_result = obj.getFormattedResult(
867
            sciformat=int(self.scinot), decimalmark=self.dmk)
868
        item["formatted_result"] = formatted_result
869
870
    def _folder_item_calculation(self, analysis_brain, item):
871
        """Set the analysis' calculation and interims to the item passed in.
872
873
        :param analysis_brain: Brain that represents an analysis
874
        :param item: analysis' dictionary counterpart that represents a row
875
        """
876
877
        if not self.has_permission(ViewResults, analysis_brain):
878
            # Hide interims and calculation if user cannot view results
879
            return
880
881
        is_editable = self.is_analysis_edition_allowed(analysis_brain)
882
883
        # calculation
884
        calculation = self.get_calculation(analysis_brain)
885
        calculation_uid = api.get_uid(calculation) if calculation else ""
886
        calculation_title = api.get_title(calculation) if calculation else ""
887
        calculation_link = get_link_for(calculation) if calculation else ""
888
        item["calculation"] = calculation_uid
889
        item["Calculation"] = calculation_title
890
        item["replace"]["Calculation"] = calculation_link or _("Manual")
891
892
        # Set interim fields. Note we add the key 'formatted_value' to the list
893
        # of interims the analysis has already assigned.
894
        analysis_obj = self.get_object(analysis_brain)
895
        interim_fields = analysis_obj.getInterimFields() or list()
896
897
        # Copy to prevent to avoid persistent changes
898
        interim_fields = deepcopy(interim_fields)
899
900
        for interim_field in interim_fields:
901
            interim_keyword = interim_field.get('keyword', '')
902
            if not interim_keyword:
903
                continue
904
905
            interim_value = interim_field.get("value", "")
906
            interim_formatted = formatDecimalMark(interim_value, self.dmk)
907
            interim_field["formatted_value"] = interim_formatted
908
            item[interim_keyword] = interim_field
909
            item["class"][interim_keyword] = "interim"
910
911
            # Note: As soon as we have a separate content type for field
912
            #       analysis, we can solely rely on the field permission
913
            #       "senaite.core: Field: Edit Analysis Result"
914
            if is_editable:
915
                if self.has_permission(
916
                        FieldEditAnalysisResult, analysis_brain):
917
                    item["allow_edit"].append(interim_keyword)
918
919
            # Add this analysis' interim fields to the interim_columns list
920
            interim_hidden = interim_field.get("hidden", False)
921
            if not interim_hidden:
922
                interim_title = interim_field.get("title")
923
                self.interim_columns[interim_keyword] = interim_title
924
925
            # Does interim's results list needs to be rendered?
926
            choices = interim_field.get("choices")
927
            if choices:
928
                # Get the {value:text} dict
929
                choices = choices.split("|")
930
                choices = dict(map(lambda ch: ch.strip().split(":"), choices))
931
932
                # Generate the display list
933
                # [{"ResultValue": value, "ResultText": text},]
934
                headers = ["ResultValue", "ResultText"]
935
                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...
936
                item.setdefault("choices", {})[interim_keyword] = dl
937
938
                # Set the text as the formatted value
939
                text = choices.get(interim_value, "")
940
                interim_field["formatted_value"] = text
941
942
                if not is_editable:
943
                    # Display the text instead of the value
944
                    interim_field["value"] = text
945
946
                item[interim_keyword] = interim_field
947
948
        item["interimfields"] = interim_fields
949
        self.interim_fields[analysis_brain.UID] = interim_fields
950
951
    def _folder_item_method(self, analysis_brain, item):
952
        """Fills the analysis' method 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
        obj = self.get_object(analysis_brain)
958
        is_editable = self.is_analysis_edition_allowed(analysis_brain)
959
        method = obj.getMethod()
960
        method_title = method and api.get_title(method) or ""
961
        item["Method"] = method_title or _("Manual")
962
        if is_editable:
963
            method_vocabulary = self.get_methods_vocabulary(analysis_brain)
964
            if method_vocabulary:
965
                item["Method"] = obj.getRawMethod()
966
                item["choices"]["Method"] = method_vocabulary
967
                item["allow_edit"].append("Method")
968
                self.show_methodinstr_columns = True
969
        elif method_title:
970
            item["replace"]["Method"] = get_link(
971
                api.get_url(method), method_title, tabindex="-1")
972
            self.show_methodinstr_columns = True
973
974
    def _folder_item_instrument(self, analysis_brain, item):
975
        """Fills the analysis' instrument to the item passed in.
976
977
        :param analysis_brain: Brain that represents an analysis
978
        :param item: analysis' dictionary counterpart that represents a row
979
        """
980
        item["Instrument"] = ""
981
982
        # Instrument can be assigned to this analysis
983
        is_editable = self.is_analysis_edition_allowed(analysis_brain)
984
        self.show_methodinstr_columns = True
985
        instrument = self.get_instrument(analysis_brain)
986
987
        if is_editable:
988
            # Edition allowed
989
            voc = self.get_instruments_vocabulary(analysis_brain)
990
            if voc:
991
                # The service has at least one instrument available
992
                item["Instrument"] = instrument.UID() if instrument else ""
993
                item["choices"]["Instrument"] = voc
994
                item["allow_edit"].append("Instrument")
995
                return
996
997
        if instrument:
998
            # Edition not allowed
999
            instrument_title = instrument and instrument.Title() or ""
1000
            instrument_link = get_link(instrument.absolute_url(),
1001
                                       instrument_title, tabindex="-1")
1002
            item["Instrument"] = instrument_title
1003
            item["replace"]["Instrument"] = instrument_link
1004
            return
1005
        else:
1006
            item["Instrument"] = _("Manual")
1007
1008
    def _folder_item_analyst(self, obj, item):
1009
        is_editable = self.is_analysis_edition_allowed(obj)
1010
        if not is_editable:
1011
            item['Analyst'] = obj.getAnalystName
1012
            return
1013
1014
        # Analyst is editable
1015
        item['Analyst'] = obj.getAnalyst or api.get_current_user().id
1016
        item['choices']['Analyst'] = self.get_analysts()
1017
1018
    def _folder_item_submitted_by(self, obj, item):
1019
        submitted_by = obj.getSubmittedBy
1020
        if submitted_by:
1021
            user = self.get_user_by_id(submitted_by)
1022
            user_name = user and user.getProperty("fullname") or submitted_by
1023
            item['SubmittedBy'] = user_name
1024
1025
    @viewcache.memoize
1026
    def get_user_by_id(self, user_id):
1027
        return api.get_user(user_id)
1028
1029
    def _folder_item_attachments(self, obj, item):
1030
        if not self.has_permission(ViewResults, obj):
1031
            return
1032
1033
        attachments_names = []
1034
        attachments_html = []
1035
        analysis = self.get_object(obj)
1036
        for at in analysis.getAttachment():
1037
            at_file = at.getAttachmentFile()
1038
            url = "{}/at_download/AttachmentFile".format(api.get_url(at))
1039
            link = get_link(url, at_file.filename, tabindex="-1")
1040
            attachments_html.append(link)
1041
            attachments_names.append(at_file.filename)
1042
1043
        if attachments_html:
1044
            item["replace"]["Attachments"] = "<br/>".join(attachments_html)
1045
            item["Attachments"] = ", ".join(attachments_names)
1046
1047
        elif analysis.getAttachmentRequired():
1048
            img = get_image("warning.png", title=_("Attachment required"))
1049
            item["replace"]["Attachments"] = img
1050
1051
    def _folder_item_uncertainty(self, analysis_brain, item):
1052
        """Fills the analysis' uncertainty to the item passed in.
1053
1054
        :param analysis_brain: Brain that represents an analysis
1055
        :param item: analysis' dictionary counterpart that represents a row
1056
        """
1057
1058
        item["Uncertainty"] = ""
1059
1060
        if not self.has_permission(ViewResults, analysis_brain):
1061
            return
1062
1063
        result = analysis_brain.getResult
1064
1065
        obj = self.get_object(analysis_brain)
1066
        formatted = format_uncertainty(obj, result, decimalmark=self.dmk,
1067
                                       sciformat=int(self.scinot))
1068
        if formatted:
1069
            item["Uncertainty"] = formatted
1070
            item["before"]["Uncertainty"] = "± "
1071
            item["after"]["Uncertainty"] = obj.getUnit()
1072
1073
        if self.is_uncertainty_edition_allowed(analysis_brain):
1074
            item["allow_edit"].append("Uncertainty")
1075
1076
    def _folder_item_detection_limits(self, analysis_brain, item):
1077
        """Fills the analysis' detection limits to the item passed in.
1078
1079
        :param analysis_brain: Brain that represents an analysis
1080
        :param item: analysis' dictionary counterpart that represents a row
1081
        """
1082
        item["DetectionLimitOperand"] = ""
1083
1084
        if not self.is_analysis_edition_allowed(analysis_brain):
1085
            # Return immediately if the we are not in edit mode
1086
            return
1087
1088
        # TODO: Performance, we wake-up the full object here
1089
        obj = self.get_object(analysis_brain)
1090
1091
        # No Detection Limit Selection
1092
        if not obj.getDetectionLimitSelector():
1093
            return None
1094
1095
        # Show Detection Limit Operand Selector
1096
        item["DetectionLimitOperand"] = obj.getDetectionLimitOperand()
1097
        item["allow_edit"].append("DetectionLimitOperand")
1098
        self.columns["DetectionLimitOperand"]["toggle"] = True
1099
1100
        # Prepare selection list for LDL/UDL
1101
        choices = [
1102
            {"ResultValue": "", "ResultText": ""},
1103
            {"ResultValue": LDL, "ResultText": LDL},
1104
            {"ResultValue": UDL, "ResultText": UDL}
1105
        ]
1106
        # Set the choices to the item
1107
        item["choices"]["DetectionLimitOperand"] = choices
1108
1109
    def _folder_item_specifications(self, analysis_brain, item):
1110
        """Set the results range to the item passed in"""
1111
        analysis = self.get_object(analysis_brain)
1112
        results_range = analysis.getResultsRange()
1113
1114
        item["Specification"] = ""
1115
        if results_range:
1116
            item["Specification"] = get_formatted_interval(results_range, "")
1117
1118
    def _folder_item_out_of_range(self, analysis_brain, item):
1119
        """Displays an icon if result is out of range
1120
        """
1121
        if not self.has_permission(ViewResults, analysis_brain):
1122
            # Users without permissions to see the result should not be able
1123
            # to see if the result is out of range naither
1124
            return
1125
1126
        analysis = self.get_object(analysis_brain)
1127
        out_range, out_shoulders = is_out_of_range(analysis)
1128
        if out_range:
1129
            msg = _("Result out of range")
1130
            img = get_image("exclamation.png", title=msg)
1131
            if not out_shoulders:
1132
                msg = _("Result in shoulder range")
1133
                img = get_image("warning.png", title=msg)
1134
            self._append_html_element(item, "Result", img)
1135
1136
    def _folder_item_result_range_compliance(self, analysis_brain, item):
1137
        """Displays an icon if the range is different from the results ranges
1138
        defined in the Sample
1139
        """
1140
        if not IAnalysisRequest.providedBy(self.context):
1141
            return
1142
1143
        analysis = self.get_object(analysis_brain)
1144
        if is_result_range_compliant(analysis):
1145
            return
1146
1147
        # Non-compliant range, display an icon
1148
        service_uid = analysis_brain.getServiceUID
1149
        original = self.context.getResultsRange(search_by=service_uid)
1150
        original = get_formatted_interval(original, "")
1151
        msg = _("Result range is different from Specification: {}"
1152
                .format(original))
1153
        img = get_image("warning.png", title=msg)
1154
        self._append_html_element(item, "Specification", img)
1155
1156
    def _folder_item_verify_icons(self, analysis_brain, item):
1157
        """Set the analysis' verification icons to the item passed in.
1158
1159
        :param analysis_brain: Brain that represents an analysis
1160
        :param item: analysis' dictionary counterpart that represents a row
1161
        """
1162
        submitter = analysis_brain.getSubmittedBy
1163
        if not submitter:
1164
            # This analysis hasn't yet been submitted, no verification yet
1165
            return
1166
1167
        if analysis_brain.review_state == 'retracted':
1168
            # Don't display icons and additional info about verification
1169
            return
1170
1171
        verifiers = analysis_brain.getVerificators
1172
        in_verifiers = submitter in verifiers
1173
        if in_verifiers:
1174
            # If analysis has been submitted and verified by the same person,
1175
            # display a warning icon
1176
            msg = t(_("Submitted and verified by the same user: {}"))
1177
            icon = get_image('warning.png', title=msg.format(submitter))
1178
            self._append_html_element(item, 'state_title', icon)
1179
1180
        num_verifications = analysis_brain.getNumberOfRequiredVerifications
1181
        if num_verifications > 1:
1182
            # More than one verification required, place an icon and display
1183
            # the number of verifications done vs. total required
1184
            done = analysis_brain.getNumberOfVerifications
1185
            pending = num_verifications - done
1186
            ratio = float(done) / float(num_verifications) if done > 0 else 0
1187
            ratio = int(ratio * 100)
1188
            scale = ratio == 0 and 0 or (ratio / 25) * 25
1189
            anchor = "<a href='#' tabindex='-1' title='{} &#13;{} {}' " \
1190
                     "class='multi-verification scale-{}'>{}/{}</a>"
1191
            anchor = anchor.format(t(_("Multi-verification required")),
1192
                                   str(pending),
1193
                                   t(_("verification(s) pending")),
1194
                                   str(scale),
1195
                                   str(done),
1196
                                   str(num_verifications))
1197
            self._append_html_element(item, 'state_title', anchor)
1198
1199
        if analysis_brain.review_state != 'to_be_verified':
1200
            # The verification of analysis has already been done or first
1201
            # verification has not been done yet. Nothing to do
1202
            return
1203
1204
        # Check if the user has "Bika: Verify" privileges
1205
        if not self.has_permission(TransitionVerify):
1206
            # User cannot verify, do nothing
1207
            return
1208
1209
        username = api.get_current_user().id
1210
        if username not in verifiers:
1211
            # Current user has not verified this analysis
1212
            if submitter != username:
1213
                # Current user is neither a submitter nor a verifier
1214
                return
1215
1216
            # Current user is the same who submitted the result
1217
            if analysis_brain.isSelfVerificationEnabled:
1218
                # Same user who submitted can verify
1219
                title = t(_("Can verify, but submitted by current user"))
1220
                html = get_image('warning.png', title=title)
1221
                self._append_html_element(item, 'state_title', html)
1222
                return
1223
1224
            # User who submitted cannot verify
1225
            title = t(_("Cannot verify, submitted by current user"))
1226
            html = get_image('submitted-by-current-user.png', title=title)
1227
            self._append_html_element(item, 'state_title', html)
1228
            return
1229
1230
        # This user verified this analysis before
1231
        multi_verif = self.context.bika_setup.getTypeOfmultiVerification()
1232
        if multi_verif != 'self_multi_not_cons':
1233
            # Multi verification by same user is not allowed
1234
            title = t(_("Cannot verify, was verified by current user"))
1235
            html = get_image('submitted-by-current-user.png', title=title)
1236
            self._append_html_element(item, 'state_title', html)
1237
            return
1238
1239
        # Multi-verification by same user, but non-consecutively, is allowed
1240
        if analysis_brain.getLastVerificator != username:
1241
            # Current user was not the last user to verify
1242
            title = t(
1243
                _("Can verify, but was already verified by current user"))
1244
            html = get_image('warning.png', title=title)
1245
            self._append_html_element(item, 'state_title', html)
1246
            return
1247
1248
        # Last user who verified is the same as current user
1249
        title = t(_("Cannot verify, last verified by current user"))
1250
        html = get_image('submitted-by-current-user.png', title=title)
1251
        self._append_html_element(item, 'state_title', html)
1252
        return
1253
1254
    def _folder_item_assigned_worksheet(self, analysis_brain, item):
1255
        """Adds an icon to the item dict if the analysis is assigned to a
1256
        worksheet and if the icon is suitable for the current context
1257
1258
        :param analysis_brain: Brain that represents an analysis
1259
        :param item: analysis' dictionary counterpart that represents a row
1260
        """
1261
        if not IAnalysisRequest.providedBy(self.context):
1262
            # We want this icon to only appear if the context is an AR
1263
            return
1264
1265
        analysis_obj = self.get_object(analysis_brain)
1266
        worksheet = analysis_obj.getWorksheet()
1267
        if not worksheet:
1268
            # No worksheet assigned. Do nothing
1269
            return
1270
1271
        title = t(_("Assigned to: ${worksheet_id}",
1272
                    mapping={'worksheet_id': safe_unicode(worksheet.id)}))
1273
        img = get_image('worksheet.png', title=title)
1274
        anchor = get_link(worksheet.absolute_url(), img, tabindex="-1")
1275
        self._append_html_element(item, 'state_title', anchor)
1276
1277
    def _folder_item_accredited_icon(self, analysis_brain, item):
1278
        """Adds an icon to the item dictionary if it is an accredited analysis
1279
        """
1280
        full_obj = self.get_object(analysis_brain)
1281
        if full_obj.getAccredited():
1282
            img = get_image("accredited.png", title=t(_("Accredited")))
1283
            self._append_html_element(item, "Service", img)
1284
1285
    def _folder_item_partition(self, analysis_brain, item):
1286
        """Adds an anchor to the partition if the current analysis is from a
1287
        partition that does not match with the current context
1288
        """
1289
        if not IAnalysisRequest.providedBy(self.context):
1290
            return
1291
1292
        sample_id = analysis_brain.getRequestID
1293
        if sample_id != api.get_id(self.context):
1294
            if not self.show_partitions:
1295
                # Do not display the link
1296
                return
1297
1298
            part_url = analysis_brain.getRequestURL
1299
            kwargs = {"class": "small", "tabindex": "-1"}
1300
            url = get_link(part_url, value=sample_id, **kwargs)
1301
            title = item["replace"].get("Service") or item["Service"]
1302
            item["replace"]["Service"] = "{}<br/>{}".format(title, url)
1303
1304
    def _folder_item_report_visibility(self, analysis_brain, item):
1305
        """Set if the hidden field can be edited (enabled/disabled)
1306
1307
        :analysis_brain: Brain that represents an analysis
1308
        :item: analysis' dictionary counterpart to be represented as a row"""
1309
        # Users that can Add Analyses to an Analysis Request must be able to
1310
        # set the visibility of the analysis in results report, also if the
1311
        # current state of the Analysis Request (e.g. verified) does not allow
1312
        # the edition of other fields. Note that an analyst has no privileges
1313
        # by default to edit this value, cause this "visibility" field is
1314
        # related with results reporting and/or visibility from the client
1315
        # side. This behavior only applies to routine analyses, the visibility
1316
        # of QC analyses is managed in publish and are not visible to clients.
1317
        if 'Hidden' not in self.columns:
1318
            return
1319
1320
        full_obj = self.get_object(analysis_brain)
1321
        item['Hidden'] = full_obj.getHidden()
1322
1323
        # Hidden checkbox is not reachable by tabbing
1324
        item["tabindex"]["Hidden"] = "disabled"
1325
        if self.has_permission(FieldEditAnalysisHidden, obj=full_obj):
1326
            item['allow_edit'].append('Hidden')
1327
1328
    def _folder_item_fieldicons(self, analysis_brain):
1329
        """Resolves if field-specific icons must be displayed for the object
1330
        passed in.
1331
1332
        :param analysis_brain: Brain that represents an analysis
1333
        """
1334
        full_obj = self.get_object(analysis_brain)
1335
        uid = api.get_uid(full_obj)
1336
        for name, adapter in getAdapters((full_obj,), IFieldIcons):
1337
            alerts = adapter()
1338
            if not alerts or uid not in alerts:
1339
                continue
1340
            alerts = alerts[uid]
1341
            if uid not in self.field_icons:
1342
                self.field_icons[uid] = alerts
1343
                continue
1344
            self.field_icons[uid].extend(alerts)
1345
1346
    def _folder_item_remarks(self, analysis_brain, item):
1347
        """Renders the Remarks field for the passed in analysis
1348
1349
        If the edition of the analysis is permitted, adds the field into the
1350
        list of editable fields.
1351
1352
        :param analysis_brain: Brain that represents an analysis
1353
        :param item: analysis' dictionary counterpart that represents a row
1354
        """
1355
1356
        if self.analysis_remarks_enabled():
1357
            item["Remarks"] = analysis_brain.getRemarks
1358
1359
        if self.is_analysis_edition_allowed(analysis_brain):
1360
            item["allow_edit"].extend(["Remarks"])
1361
        else:
1362
            # render HTMLified text in readonly mode
1363
            item["Remarks"] = api.text_to_html(
1364
                analysis_brain.getRemarks, wrap=None)
1365
1366
    def _append_html_element(self, item, element, html, glue="&nbsp;",
1367
                             after=True):
1368
        """Appends an html value after or before the element in the item dict
1369
1370
        :param item: dictionary that represents an analysis row
1371
        :param element: id of the element the html must be added thereafter
1372
        :param html: element to append
1373
        :param glue: glue to use for appending
1374
        :param after: if the html content must be added after or before"""
1375
        position = after and 'after' or 'before'
1376
        item[position] = item.get(position, {})
1377
        original = item[position].get(element, '')
1378
        if not original:
1379
            item[position][element] = html
1380
            return
1381
        item[position][element] = glue.join([original, html])
1382
1383
    def _folder_item_conditions(self, analysis_brain, item):
1384
        """Renders the analysis conditions
1385
        """
1386
        analysis = self.get_object(analysis_brain)
1387
        conditions = analysis.getConditions()
1388
        if conditions:
1389
            conditions = map(lambda it: ": ".join([it["title"], it["value"]]),
1390
                             conditions)
1391
            conditions = "<br/>".join(conditions)
1392
            service = item["replace"].get("Service") or item["Service"]
1393
            item["replace"]["Service"] = "{}<br/>{}".format(service, conditions)
1394