Passed
Pull Request — 2.x (#1928)
by Ramon
04:30
created

bika.lims.browser.analyses.view   F

Complexity

Total Complexity 179

Size/Duplication

Total Lines 1425
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 179
eloc 823
dl 0
loc 1425
rs 1.777
c 0
b 0
f 0

47 Methods

Rating   Name   Duplication   Size   Complexity  
A AnalysesView.is_uncertainty_edition_allowed() 0 24 4
A AnalysesView._folder_item_duedate() 0 24 3
A AnalysesView.get_object() 0 9 1
A AnalysesView._folder_item_method() 0 22 4
A AnalysesView._folder_item_category() 0 14 3
F AnalysesView.folderitems() 0 75 14
A AnalysesView.before_render() 0 5 1
A AnalysesView._folder_item_accredited_icon() 0 7 2
A AnalysesView.load_analysis_categories() 0 9 1
D AnalysesView._folder_item_verify_icons() 0 97 13
A AnalysesView.update() 0 6 1
A AnalysesView.get_analysts() 0 9 2
B AnalysesView.__init__() 0 167 2
A AnalysesView._folder_item_out_of_range() 0 17 4
A AnalysesView.get_methods_vocabulary() 0 27 3
A AnalysesView._folder_item_specifications() 0 8 2
A AnalysesView._append_html_element() 0 16 2
F AnalysesView._folder_item_calculation() 0 80 14
A AnalysesView._folder_item_report_visibility() 0 23 3
B AnalysesView._folder_item_result() 0 59 8
B AnalysesView.is_analysis_edition_allowed() 0 29 6
A AnalysesView.is_result_edition_allowed() 0 26 5
A AnalysesView.isItemAllowed() 0 15 4
A AnalysesView._on_method_change() 0 20 2
A AnalysesView._folder_item_fieldicons() 0 17 5
A AnalysesView._folder_item_analyst() 0 9 2
B AnalysesView._folder_item_instrument() 0 33 5
A AnalysesView._folder_item_submitted_by() 0 6 2
A AnalysesView.get_calculation() 0 8 1
A AnalysesView.show_partitions() 0 9 2
A AnalysesView._folder_item_assigned_worksheet() 0 22 3
A AnalysesView._folder_item_partition() 0 18 4
A AnalysesView.analysis_remarks_enabled() 0 5 1
B AnalysesView.append_partition_filters() 0 56 8
A AnalysesView._folder_item_conditions() 0 15 4
A AnalysesView._folder_item_uncertainty() 0 24 4
A AnalysesView._folder_item_remarks() 0 19 3
B AnalysesView.folderitem() 0 79 2
A AnalysesView._folder_item_css_class() 0 22 4
A AnalysesView.get_instrument() 0 8 1
A AnalysesView._folder_item_detection_limits() 0 32 3
A AnalysesView.senaite_theme() 0 6 1
B AnalysesView.get_instruments_vocabulary() 0 74 8
A AnalysesView.get_user_by_id() 0 3 1
A AnalysesView._folder_item_result_range_compliance() 0 19 3
A AnalysesView._folder_item_attachments() 0 21 5
A AnalysesView.has_permission() 0 14 3

How to fix   Complexity   

Complexity

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

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

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