Passed
Push — 2.x ( b44b4f...d6c82c )
by Ramon
06:03
created

AnalysesView.is_method_column_required()   A

Complexity

Conditions 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 10
rs 10
c 0
b 0
f 0
cc 3
nop 1
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
        if not self.has_permission(ViewResults, analysis_brain):
851
            # If user has no permissions, don"t display the result but an icon
852
            img = get_image("to_follow.png", width="16px", height="16px")
853
            item["before"]["Result"] = img
854
            return
855
856
        result = analysis_brain.getResult
857
        capture_date = analysis_brain.getResultCaptureDate
858
        capture_date_str = self.ulocalized_time(capture_date, long_format=0)
859
860
        item["Result"] = result
861
        item["CaptureDate"] = capture_date_str
862
        item["result_captured"] = capture_date_str
863
864
        # Get the analysis object
865
        obj = self.get_object(analysis_brain)
866
867
        # Edit mode enabled of this Analysis
868
        if self.is_analysis_edition_allowed(analysis_brain):
869
            # Allow to set Remarks
870
            item["allow_edit"].append("Remarks")
871
872
            # Set the results field editable
873
            if self.is_result_edition_allowed(analysis_brain):
874
                item["allow_edit"].append("Result")
875
876
            # Prepare result options
877
            choices = obj.getResultOptions()
878
            if choices:
879
                # N.B.we copy here the list to avoid persistent changes
880
                choices = copy(choices)
881
                choices_type = obj.getResultOptionsType()
882
                if choices_type == "select":
883
                    # By default set empty as the default selected choice
884
                    choices.insert(0, dict(ResultValue="", ResultText=""))
885
                item["choices"]["Result"] = choices
886
                item["result_type"] = choices_type
887
888
            elif obj.getStringResult():
889
                item["result_type"] = "string"
890
891
            else:
892
                item["result_type"] = "numeric"
893
894
        if not result:
895
            return
896
897
        formatted_result = obj.getFormattedResult(
898
            sciformat=int(self.scinot), decimalmark=self.dmk)
899
        item["formatted_result"] = formatted_result
900
901
    def _folder_item_calculation(self, analysis_brain, item):
902
        """Set the analysis' calculation and interims to the item passed in.
903
904
        :param analysis_brain: Brain that represents an analysis
905
        :param item: analysis' dictionary counterpart that represents a row
906
        """
907
908
        if not self.has_permission(ViewResults, analysis_brain):
909
            # Hide interims and calculation if user cannot view results
910
            return
911
912
        is_editable = self.is_analysis_edition_allowed(analysis_brain)
913
914
        # calculation
915
        calculation = self.get_calculation(analysis_brain)
916
        calculation_uid = api.get_uid(calculation) if calculation else ""
917
        calculation_title = api.get_title(calculation) if calculation else ""
918
        calculation_link = get_link_for(calculation) if calculation else ""
919
        item["calculation"] = calculation_uid
920
        item["Calculation"] = calculation_title
921
        item["replace"]["Calculation"] = calculation_link or _("Manual")
922
923
        # Set interim fields. Note we add the key 'formatted_value' to the list
924
        # of interims the analysis has already assigned.
925
        analysis_obj = self.get_object(analysis_brain)
926
        interim_fields = analysis_obj.getInterimFields() or list()
927
928
        # Copy to prevent to avoid persistent changes
929
        interim_fields = deepcopy(interim_fields)
930
931
        for interim_field in interim_fields:
932
            interim_keyword = interim_field.get('keyword', '')
933
            if not interim_keyword:
934
                continue
935
936
            interim_value = interim_field.get("value", "")
937
            interim_formatted = formatDecimalMark(interim_value, self.dmk)
938
            interim_field["formatted_value"] = interim_formatted
939
            item[interim_keyword] = interim_field
940
            item["class"][interim_keyword] = "interim"
941
942
            # Note: As soon as we have a separate content type for field
943
            #       analysis, we can solely rely on the field permission
944
            #       "senaite.core: Field: Edit Analysis Result"
945
            if is_editable:
946
                if self.has_permission(
947
                        FieldEditAnalysisResult, analysis_brain):
948
                    item["allow_edit"].append(interim_keyword)
949
950
            # Add this analysis' interim fields to the interim_columns list
951
            interim_hidden = interim_field.get("hidden", False)
952
            if not interim_hidden:
953
                interim_title = interim_field.get("title")
954
                self.interim_columns[interim_keyword] = interim_title
955
956
            # Does interim's results list needs to be rendered?
957
            choices = interim_field.get("choices")
958
            if choices:
959
                # Get the {value:text} dict
960
                choices = choices.split("|")
961
                choices = dict(map(lambda ch: ch.strip().split(":"), choices))
962
963
                # Generate the display list
964
                # [{"ResultValue": value, "ResultText": text},]
965
                headers = ["ResultValue", "ResultText"]
966
                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...
967
                item.setdefault("choices", {})[interim_keyword] = dl
968
969
                # Set the text as the formatted value
970
                text = choices.get(interim_value, "")
971
                interim_field["formatted_value"] = text
972
973
                if not is_editable:
974
                    # Display the text instead of the value
975
                    interim_field["value"] = text
976
977
                item[interim_keyword] = interim_field
978
979
        item["interimfields"] = interim_fields
980
        self.interim_fields[analysis_brain.UID] = interim_fields
981
982
    def _folder_item_method(self, analysis_brain, item):
983
        """Fills the analysis' method to the item passed in.
984
985
        :param analysis_brain: Brain that represents an analysis
986
        :param item: analysis' dictionary counterpart that represents a row
987
        """
988
        obj = self.get_object(analysis_brain)
989
        is_editable = self.is_analysis_edition_allowed(analysis_brain)
990
        if is_editable:
991
            method_vocabulary = self.get_methods_vocabulary(analysis_brain)
992
            item["Method"] = obj.getRawMethod()
993
            item["choices"]["Method"] = method_vocabulary
994
            item["allow_edit"].append("Method")
995
        else:
996
            item["Method"] = _("Manual")
997
            method = obj.getMethod()
998
            if method:
999
                item["Method"] = api.get_title(method)
1000
                item["replace"]["Method"] = get_link_for(method, tabindex="-1")
1001
1002
    def _on_method_change(self, uid=None, value=None, item=None, **kw):
1003
        """Update instrument and calculation when the method changes
1004
1005
        :param uid: object UID
1006
        :value: UID of the new method
1007
        :item: old folderitem
1008
1009
        :returns: updated folderitem
1010
        """
1011
        obj = api.get_object_by_uid(uid, None)
1012
        method = api.get_object_by_uid(value, None)
1013
1014
        if not all([obj, method, item]):
1015
            return None
1016
1017
        # update the available instruments
1018
        inst_vocab = self.get_instruments_vocabulary(obj, method=method)
1019
        item["choices"]["Instrument"] = inst_vocab
1020
1021
        return item
1022
1023
    def _folder_item_instrument(self, analysis_brain, item):
1024
        """Fills the analysis' instrument to the item passed in.
1025
1026
        :param analysis_brain: Brain that represents an analysis
1027
        :param item: analysis' dictionary counterpart that represents a row
1028
        """
1029
        item["Instrument"] = ""
1030
1031
        # Instrument can be assigned to this analysis
1032
        is_editable = self.is_analysis_edition_allowed(analysis_brain)
1033
        instrument = self.get_instrument(analysis_brain)
1034
1035
        if is_editable:
1036
            # Edition allowed
1037
            voc = self.get_instruments_vocabulary(analysis_brain)
1038
            item["Instrument"] = instrument.UID() if instrument else ""
1039
            item["choices"]["Instrument"] = voc
1040
            item["allow_edit"].append("Instrument")
1041
1042
        elif instrument:
1043
            # Edition not allowed
1044
            item["Instrument"] = api.get_title(instrument)
1045
            instrument_link = get_link_for(instrument, tabindex="-1")
1046
            item["replace"]["Instrument"] = instrument_link
1047
1048
        else:
1049
            item["Instrument"] = _("Manual")
1050
1051
    def _folder_item_analyst(self, obj, item):
1052
        is_editable = self.is_analysis_edition_allowed(obj)
1053
        if not is_editable:
1054
            item['Analyst'] = obj.getAnalystName
1055
            return
1056
1057
        # Analyst is editable
1058
        item['Analyst'] = obj.getAnalyst or api.get_current_user().id
1059
        item['choices']['Analyst'] = self.get_analysts()
1060
1061
    def _folder_item_submitted_by(self, obj, item):
1062
        submitted_by = obj.getSubmittedBy
1063
        if submitted_by:
1064
            user = self.get_user_by_id(submitted_by)
1065
            user_name = user and user.getProperty("fullname") or submitted_by
1066
            item['SubmittedBy'] = user_name
1067
1068
    @viewcache.memoize
1069
    def get_user_by_id(self, user_id):
1070
        return api.get_user(user_id)
1071
1072
    def _folder_item_attachments(self, obj, item):
1073
        if not self.has_permission(ViewResults, obj):
1074
            return
1075
1076
        attachments_names = []
1077
        attachments_html = []
1078
        analysis = self.get_object(obj)
1079
        for at in analysis.getAttachment():
1080
            at_file = at.getAttachmentFile()
1081
            url = "{}/at_download/AttachmentFile".format(api.get_url(at))
1082
            link = get_link(url, at_file.filename, tabindex="-1")
1083
            attachments_html.append(link)
1084
            attachments_names.append(at_file.filename)
1085
1086
        if attachments_html:
1087
            item["replace"]["Attachments"] = "<br/>".join(attachments_html)
1088
            item["Attachments"] = ", ".join(attachments_names)
1089
1090
        elif analysis.getAttachmentRequired():
1091
            img = get_image("warning.png", title=_("Attachment required"))
1092
            item["replace"]["Attachments"] = img
1093
1094
    def _folder_item_uncertainty(self, analysis_brain, item):
1095
        """Fills the analysis' uncertainty to the item passed in.
1096
1097
        :param analysis_brain: Brain that represents an analysis
1098
        :param item: analysis' dictionary counterpart that represents a row
1099
        """
1100
1101
        item["Uncertainty"] = ""
1102
1103
        if not self.has_permission(ViewResults, analysis_brain):
1104
            return
1105
1106
        result = analysis_brain.getResult
1107
1108
        obj = self.get_object(analysis_brain)
1109
        formatted = format_uncertainty(obj, result, decimalmark=self.dmk,
1110
                                       sciformat=int(self.scinot))
1111
        if formatted:
1112
            item["Uncertainty"] = formatted
1113
            item["before"]["Uncertainty"] = "± "
1114
            item["after"]["Uncertainty"] = obj.getUnit()
1115
1116
        if self.is_uncertainty_edition_allowed(analysis_brain):
1117
            item["allow_edit"].append("Uncertainty")
1118
1119
    def _folder_item_detection_limits(self, analysis_brain, item):
1120
        """Fills the analysis' detection limits to the item passed in.
1121
1122
        :param analysis_brain: Brain that represents an analysis
1123
        :param item: analysis' dictionary counterpart that represents a row
1124
        """
1125
        item["DetectionLimitOperand"] = ""
1126
1127
        if not self.is_analysis_edition_allowed(analysis_brain):
1128
            # Return immediately if the we are not in edit mode
1129
            return
1130
1131
        # TODO: Performance, we wake-up the full object here
1132
        obj = self.get_object(analysis_brain)
1133
1134
        # No Detection Limit Selection
1135
        if not obj.getDetectionLimitSelector():
1136
            return None
1137
1138
        # Show Detection Limit Operand Selector
1139
        item["DetectionLimitOperand"] = obj.getDetectionLimitOperand()
1140
        item["allow_edit"].append("DetectionLimitOperand")
1141
        self.columns["DetectionLimitOperand"]["toggle"] = True
1142
1143
        # Prepare selection list for LDL/UDL
1144
        choices = [
1145
            {"ResultValue": "", "ResultText": ""},
1146
            {"ResultValue": LDL, "ResultText": LDL},
1147
            {"ResultValue": UDL, "ResultText": UDL}
1148
        ]
1149
        # Set the choices to the item
1150
        item["choices"]["DetectionLimitOperand"] = choices
1151
1152
    def _folder_item_specifications(self, analysis_brain, item):
1153
        """Set the results range to the item passed in"""
1154
        analysis = self.get_object(analysis_brain)
1155
        results_range = analysis.getResultsRange()
1156
1157
        item["Specification"] = ""
1158
        if results_range:
1159
            item["Specification"] = get_formatted_interval(results_range, "")
1160
1161
    def _folder_item_out_of_range(self, analysis_brain, item):
1162
        """Displays an icon if result is out of range
1163
        """
1164
        if not self.has_permission(ViewResults, analysis_brain):
1165
            # Users without permissions to see the result should not be able
1166
            # to see if the result is out of range naither
1167
            return
1168
1169
        analysis = self.get_object(analysis_brain)
1170
        out_range, out_shoulders = is_out_of_range(analysis)
1171
        if out_range:
1172
            msg = _("Result out of range")
1173
            img = get_image("exclamation.png", title=msg)
1174
            if not out_shoulders:
1175
                msg = _("Result in shoulder range")
1176
                img = get_image("warning.png", title=msg)
1177
            self._append_html_element(item, "Result", img)
1178
1179
    def _folder_item_result_range_compliance(self, analysis_brain, item):
1180
        """Displays an icon if the range is different from the results ranges
1181
        defined in the Sample
1182
        """
1183
        if not IAnalysisRequest.providedBy(self.context):
1184
            return
1185
1186
        analysis = self.get_object(analysis_brain)
1187
        if is_result_range_compliant(analysis):
1188
            return
1189
1190
        # Non-compliant range, display an icon
1191
        service_uid = analysis_brain.getServiceUID
1192
        original = self.context.getResultsRange(search_by=service_uid)
1193
        original = get_formatted_interval(original, "")
1194
        msg = _("Result range is different from Specification: {}"
1195
                .format(original))
1196
        img = get_image("warning.png", title=msg)
1197
        self._append_html_element(item, "Specification", img)
1198
1199
    def _folder_item_verify_icons(self, analysis_brain, item):
1200
        """Set the analysis' verification icons to the item passed in.
1201
1202
        :param analysis_brain: Brain that represents an analysis
1203
        :param item: analysis' dictionary counterpart that represents a row
1204
        """
1205
        submitter = analysis_brain.getSubmittedBy
1206
        if not submitter:
1207
            # This analysis hasn't yet been submitted, no verification yet
1208
            return
1209
1210
        if analysis_brain.review_state == 'retracted':
1211
            # Don't display icons and additional info about verification
1212
            return
1213
1214
        verifiers = analysis_brain.getVerificators
1215
        in_verifiers = submitter in verifiers
1216
        if in_verifiers:
1217
            # If analysis has been submitted and verified by the same person,
1218
            # display a warning icon
1219
            msg = t(_("Submitted and verified by the same user: {}"))
1220
            icon = get_image('warning.png', title=msg.format(submitter))
1221
            self._append_html_element(item, 'state_title', icon)
1222
1223
        num_verifications = analysis_brain.getNumberOfRequiredVerifications
1224
        if num_verifications > 1:
1225
            # More than one verification required, place an icon and display
1226
            # the number of verifications done vs. total required
1227
            done = analysis_brain.getNumberOfVerifications
1228
            pending = num_verifications - done
1229
            ratio = float(done) / float(num_verifications) if done > 0 else 0
1230
            ratio = int(ratio * 100)
1231
            scale = ratio == 0 and 0 or (ratio / 25) * 25
1232
            anchor = "<a href='#' tabindex='-1' title='{} &#13;{} {}' " \
1233
                     "class='multi-verification scale-{}'>{}/{}</a>"
1234
            anchor = anchor.format(t(_("Multi-verification required")),
1235
                                   str(pending),
1236
                                   t(_("verification(s) pending")),
1237
                                   str(scale),
1238
                                   str(done),
1239
                                   str(num_verifications))
1240
            self._append_html_element(item, 'state_title', anchor)
1241
1242
        if analysis_brain.review_state != 'to_be_verified':
1243
            # The verification of analysis has already been done or first
1244
            # verification has not been done yet. Nothing to do
1245
            return
1246
1247
        # Check if the user has "Bika: Verify" privileges
1248
        if not self.has_permission(TransitionVerify):
1249
            # User cannot verify, do nothing
1250
            return
1251
1252
        username = api.get_current_user().id
1253
        if username not in verifiers:
1254
            # Current user has not verified this analysis
1255
            if submitter != username:
1256
                # Current user is neither a submitter nor a verifier
1257
                return
1258
1259
            # Current user is the same who submitted the result
1260
            if analysis_brain.isSelfVerificationEnabled:
1261
                # Same user who submitted can verify
1262
                title = t(_("Can verify, but submitted by current user"))
1263
                html = get_image('warning.png', title=title)
1264
                self._append_html_element(item, 'state_title', html)
1265
                return
1266
1267
            # User who submitted cannot verify
1268
            title = t(_("Cannot verify, submitted by current user"))
1269
            html = get_image('submitted-by-current-user.png', title=title)
1270
            self._append_html_element(item, 'state_title', html)
1271
            return
1272
1273
        # This user verified this analysis before
1274
        multi_verif = self.context.bika_setup.getTypeOfmultiVerification()
1275
        if multi_verif != 'self_multi_not_cons':
1276
            # Multi verification by same user is not allowed
1277
            title = t(_("Cannot verify, was verified by current user"))
1278
            html = get_image('submitted-by-current-user.png', title=title)
1279
            self._append_html_element(item, 'state_title', html)
1280
            return
1281
1282
        # Multi-verification by same user, but non-consecutively, is allowed
1283
        if analysis_brain.getLastVerificator != username:
1284
            # Current user was not the last user to verify
1285
            title = t(
1286
                _("Can verify, but was already verified by current user"))
1287
            html = get_image('warning.png', title=title)
1288
            self._append_html_element(item, 'state_title', html)
1289
            return
1290
1291
        # Last user who verified is the same as current user
1292
        title = t(_("Cannot verify, last verified by current user"))
1293
        html = get_image('submitted-by-current-user.png', title=title)
1294
        self._append_html_element(item, 'state_title', html)
1295
        return
1296
1297
    def _folder_item_assigned_worksheet(self, analysis_brain, item):
1298
        """Adds an icon to the item dict if the analysis is assigned to a
1299
        worksheet and if the icon is suitable for the current context
1300
1301
        :param analysis_brain: Brain that represents an analysis
1302
        :param item: analysis' dictionary counterpart that represents a row
1303
        """
1304
        if not IAnalysisRequest.providedBy(self.context):
1305
            # We want this icon to only appear if the context is an AR
1306
            return
1307
1308
        analysis_obj = self.get_object(analysis_brain)
1309
        worksheet = analysis_obj.getWorksheet()
1310
        if not worksheet:
1311
            # No worksheet assigned. Do nothing
1312
            return
1313
1314
        title = t(_("Assigned to: ${worksheet_id}",
1315
                    mapping={'worksheet_id': safe_unicode(worksheet.id)}))
1316
        img = get_image('worksheet.png', title=title)
1317
        anchor = get_link(worksheet.absolute_url(), img, tabindex="-1")
1318
        self._append_html_element(item, 'state_title', anchor)
1319
1320
    def _folder_item_accredited_icon(self, analysis_brain, item):
1321
        """Adds an icon to the item dictionary if it is an accredited analysis
1322
        """
1323
        full_obj = self.get_object(analysis_brain)
1324
        if full_obj.getAccredited():
1325
            img = get_image("accredited.png", title=t(_("Accredited")))
1326
            self._append_html_element(item, "Service", img)
1327
1328
    def _folder_item_partition(self, analysis_brain, item):
1329
        """Adds an anchor to the partition if the current analysis is from a
1330
        partition that does not match with the current context
1331
        """
1332
        if not IAnalysisRequest.providedBy(self.context):
1333
            return
1334
1335
        sample_id = analysis_brain.getRequestID
1336
        if sample_id != api.get_id(self.context):
1337
            if not self.show_partitions:
1338
                # Do not display the link
1339
                return
1340
1341
            part_url = analysis_brain.getRequestURL
1342
            kwargs = {"class": "small", "tabindex": "-1"}
1343
            url = get_link(part_url, value=sample_id, **kwargs)
1344
            title = item["replace"].get("Service") or item["Service"]
1345
            item["replace"]["Service"] = "{}<br/>{}".format(title, url)
1346
1347
    def _folder_item_report_visibility(self, analysis_brain, item):
1348
        """Set if the hidden field can be edited (enabled/disabled)
1349
1350
        :analysis_brain: Brain that represents an analysis
1351
        :item: analysis' dictionary counterpart to be represented as a row"""
1352
        # Users that can Add Analyses to an Analysis Request must be able to
1353
        # set the visibility of the analysis in results report, also if the
1354
        # current state of the Analysis Request (e.g. verified) does not allow
1355
        # the edition of other fields. Note that an analyst has no privileges
1356
        # by default to edit this value, cause this "visibility" field is
1357
        # related with results reporting and/or visibility from the client
1358
        # side. This behavior only applies to routine analyses, the visibility
1359
        # of QC analyses is managed in publish and are not visible to clients.
1360
        if 'Hidden' not in self.columns:
1361
            return
1362
1363
        full_obj = self.get_object(analysis_brain)
1364
        item['Hidden'] = full_obj.getHidden()
1365
1366
        # Hidden checkbox is not reachable by tabbing
1367
        item["tabindex"]["Hidden"] = "disabled"
1368
        if self.has_permission(FieldEditAnalysisHidden, obj=full_obj):
1369
            item['allow_edit'].append('Hidden')
1370
1371
    def _folder_item_fieldicons(self, analysis_brain):
1372
        """Resolves if field-specific icons must be displayed for the object
1373
        passed in.
1374
1375
        :param analysis_brain: Brain that represents an analysis
1376
        """
1377
        full_obj = self.get_object(analysis_brain)
1378
        uid = api.get_uid(full_obj)
1379
        for name, adapter in getAdapters((full_obj,), IFieldIcons):
1380
            alerts = adapter()
1381
            if not alerts or uid not in alerts:
1382
                continue
1383
            alerts = alerts[uid]
1384
            if uid not in self.field_icons:
1385
                self.field_icons[uid] = alerts
1386
                continue
1387
            self.field_icons[uid].extend(alerts)
1388
1389
    def _folder_item_remarks(self, analysis_brain, item):
1390
        """Renders the Remarks field for the passed in analysis
1391
1392
        If the edition of the analysis is permitted, adds the field into the
1393
        list of editable fields.
1394
1395
        :param analysis_brain: Brain that represents an analysis
1396
        :param item: analysis' dictionary counterpart that represents a row
1397
        """
1398
1399
        if self.analysis_remarks_enabled():
1400
            item["Remarks"] = analysis_brain.getRemarks
1401
1402
        if self.is_analysis_edition_allowed(analysis_brain):
1403
            item["allow_edit"].extend(["Remarks"])
1404
        else:
1405
            # render HTMLified text in readonly mode
1406
            item["Remarks"] = api.text_to_html(
1407
                analysis_brain.getRemarks, wrap=None)
1408
1409
    def _append_html_element(self, item, element, html, glue="&nbsp;",
1410
                             after=True):
1411
        """Appends an html value after or before the element in the item dict
1412
1413
        :param item: dictionary that represents an analysis row
1414
        :param element: id of the element the html must be added thereafter
1415
        :param html: element to append
1416
        :param glue: glue to use for appending
1417
        :param after: if the html content must be added after or before"""
1418
        position = after and 'after' or 'before'
1419
        item[position] = item.get(position, {})
1420
        original = item[position].get(element, '')
1421
        if not original:
1422
            item[position][element] = html
1423
            return
1424
        item[position][element] = glue.join([original, html])
1425
1426
    def _folder_item_conditions(self, analysis_brain, item):
1427
        """Renders the analysis conditions
1428
        """
1429
        analysis = self.get_object(analysis_brain)
1430
1431
        if not IRoutineAnalysis.providedBy(analysis):
1432
            return
1433
1434
        conditions = analysis.getConditions()
1435
        if conditions:
1436
            conditions = map(lambda it: ": ".join([it["title"], it["value"]]),
1437
                             conditions)
1438
            conditions = "<br/>".join(conditions)
1439
            service = item["replace"].get("Service") or item["Service"]
1440
            item["replace"]["Service"] = "{}<br/>{}".format(service, conditions)
1441
1442
    def is_method_required(self, analysis):
1443
        """Returns whether the render of the selection list with methods is
1444
        required for the method passed-in, even if only option "None" is
1445
        displayed for selection
1446
        """
1447
        # Always return true if the analysis has a method assigned
1448
        obj = self.get_object(analysis)
1449
        method = obj.getMethod()
1450
        if method:
1451
            return True
1452
1453
        methods = obj.getAllowedMethods()
1454
        return len(methods) > 0
1455
1456
    def is_instrument_required(self, analysis):
1457
        """Returns whether the render of the selection list with instruments is
1458
        required for the analysis passed-in, even if only option "None" is
1459
        displayed for selection.
1460
        :param analysis: Brain or object that represents an analysis
1461
        """
1462
        # If method selection list is required, the instrument selection too
1463
        if self.is_method_required(analysis):
1464
            return True
1465
        
1466
        # Always return true if the analysis has an instrument assigned
1467
        if self.get_instrument(analysis):
1468
            return True
1469
1470
        obj = self.get_object(analysis)
1471
        instruments = obj.getAllowedInstruments()
1472
        # There is no need to check for the instruments of the method assigned
1473
        # to # the analysis (if any), because the instruments rendered in the
1474
        # selection list are always a subset of the allowed instruments when
1475
        # a method is selected
1476
        return len(instruments) > 0
1477
1478
    def is_method_column_required(self):
1479
        """Returns whether the method column has to be rendered or not.
1480
        Returns True if at least one of the analyses from the listing requires
1481
        the list for method selection to be rendered
1482
        """
1483
        for item in self.items:
1484
            obj = item.get("obj")
1485
            if self.is_method_required(obj):
1486
                return True
1487
        return False
1488
1489
    def is_instrument_column_required(self):
1490
        """Returns whether the instrument column has to be rendered or not.
1491
        Returns True if at least one of the analyses from the listing requires
1492
        the list for instrument selection to be rendered
1493
        """
1494
        for item in self.items:
1495
            obj = item.get("obj")
1496
            if self.is_instrument_required(obj):
1497
                return True
1498
        return False
1499