Passed
Pull Request — 2.x (#1988)
by Jordi
05:57
created

AnalysesView.is_multi_interim()   A

Complexity

Conditions 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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