Passed
Push — master ( b60410...062e8c )
by Jordi
05:05
created

build.bika.lims.browser.analyses.view   F

Complexity

Total Complexity 148

Size/Duplication

Total Lines 1215
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 148
eloc 685
dl 0
loc 1215
rs 1.915
c 0
b 0
f 0

37 Methods

Rating   Name   Duplication   Size   Complexity  
A bika.lims.browser.analyses.view.AnalysesView.is_uncertainty_edition_allowed() 0 24 4
A bika.lims.browser.analyses.view.AnalysesView._folder_item_duedate() 0 24 3
A bika.lims.browser.analyses.view.AnalysesView.get_object() 0 13 3
A bika.lims.browser.analyses.view.AnalysesView._folder_item_method() 0 21 4
A bika.lims.browser.analyses.view.AnalysesView._folder_item_category() 0 14 3
F bika.lims.browser.analyses.view.AnalysesView.folderitems() 0 75 14
A bika.lims.browser.analyses.view.AnalysesView.before_render() 0 5 1
A bika.lims.browser.analyses.view.AnalysesView.load_analysis_categories() 0 9 1
A bika.lims.browser.analyses.view.AnalysesView._folder_item_reflex_icons() 0 13 2
D bika.lims.browser.analyses.view.AnalysesView._folder_item_verify_icons() 0 97 13
A bika.lims.browser.analyses.view.AnalysesView.update() 0 5 1
A bika.lims.browser.analyses.view.AnalysesView.is_analysis_instrument_valid() 0 17 2
A bika.lims.browser.analyses.view.AnalysesView.get_analysts() 0 9 2
B bika.lims.browser.analyses.view.AnalysesView.__init__() 0 159 2
A bika.lims.browser.analyses.view.AnalysesView.get_methods_vocabulary() 0 26 3
A bika.lims.browser.analyses.view.AnalysesView._append_html_element() 0 16 2
A bika.lims.browser.analyses.view.AnalysesView._folder_item_specifications() 0 20 4
B bika.lims.browser.analyses.view.AnalysesView._folder_item_calculation() 0 48 7
A bika.lims.browser.analyses.view.AnalysesView._folder_item_report_visibility() 0 20 3
B bika.lims.browser.analyses.view.AnalysesView._folder_item_result() 0 48 6
B bika.lims.browser.analyses.view.AnalysesView.is_analysis_edition_allowed() 0 36 7
A bika.lims.browser.analyses.view.AnalysesView.is_result_edition_allowed() 0 26 5
A bika.lims.browser.analyses.view.AnalysesView.isItemAllowed() 0 15 4
A bika.lims.browser.analyses.view.AnalysesView._folder_item_fieldicons() 0 17 5
A bika.lims.browser.analyses.view.AnalysesView._folder_item_analyst() 0 9 2
B bika.lims.browser.analyses.view.AnalysesView._folder_item_instrument() 0 36 6
A bika.lims.browser.analyses.view.AnalysesView._folder_item_assigned_worksheet() 0 22 3
A bika.lims.browser.analyses.view.AnalysesView.analysis_remarks_enabled() 0 5 1
A bika.lims.browser.analyses.view.AnalysesView._folder_item_uncertainty() 0 24 4
A bika.lims.browser.analyses.view.AnalysesView._folder_item_remarks() 0 15 3
A bika.lims.browser.analyses.view.AnalysesView.folderitem() 0 70 2
A bika.lims.browser.analyses.view.AnalysesView._folder_item_css_class() 0 22 4
A bika.lims.browser.analyses.view.AnalysesView.get_instrument() 0 10 1
A bika.lims.browser.analyses.view.AnalysesView._folder_item_detection_limits() 0 32 3
B bika.lims.browser.analyses.view.AnalysesView.get_instruments_vocabulary() 0 53 7
B bika.lims.browser.analyses.view.AnalysesView._folder_item_attachments() 0 45 8
A bika.lims.browser.analyses.view.AnalysesView.has_permission() 0 14 3

How to fix   Complexity   

Complexity

Complex classes like build.bika.lims.browser.analyses.view often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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