Passed
Push — 2.x ( 0959b6...617abd )
by Ramon
07:37
created

bika.lims.browser.instrument   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 902
Duplicated Lines 16.85 %

Importance

Changes 0
Metric Value
wmc 59
eloc 592
dl 152
loc 902
rs 4.08
c 0
b 0
f 0

29 Methods

Rating   Name   Duplication   Size   Complexity  
A InstrumentReferenceAnalysesView.isItemAllowed() 0 4 1
A InstrumentCertificationsView.localize_date() 0 4 1
A InstrumentValidationsView.folderitem() 0 23 3
A InstrumentScheduleView.localize_date() 0 4 1
A InstrumentReferenceAnalysesView._folder_item_instrument() 0 6 1
A InstrumentCalibrationsView.localize_date() 0 4 1
B ajaxGetInstrumentMethods.__call__() 0 26 5
B InstrumentValidationsView.__init__() 0 54 2
A InstrumentReferenceAnalysesView.__init__() 0 29 1
A InstrumentMaintenanceView.localize_date() 0 4 1
A InstrumentReferenceAnalysesViewView.__call__() 0 2 1
A InstrumentCalibrationsView.folderitem() 0 28 4
A InstrumentValidationsView.localize_date() 0 4 1
B InstrumentMaintenanceView.folderitem() 0 36 6
A InstrumentReferenceAnalysesViewView.__init__() 0 10 1
A InstrumentReferenceAnalysesViewView.get_analyses_table_view() 0 22 1
B InstrumentCalibrationsView.__init__() 0 54 2
B InstrumentMultifileView.__init__() 0 55 1
B InstrumentMaintenanceView.__init__() 77 77 1
A InstrumentReferenceAnalysesView.add_column() 0 11 4
B InstrumentScheduleView.__init__() 75 75 1
A InstrumentReferenceAnalysesView.folderitem() 0 28 2
A InstrumentMultifileView.get_file() 0 8 2
A InstrumentCertificationsView.get_document() 0 8 2
A InstrumentMultifileView.folderitem() 0 24 3
A InstrumentAutoImportLogsView.__init__() 0 26 1
B InstrumentCertificationsView.folderitem() 0 40 6
A InstrumentScheduleView.folderitem() 0 18 2
B InstrumentCertificationsView.__init__() 0 58 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like bika.lims.browser.instrument 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-2025 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
import json
22
23
import plone
24
from bika.lims import api
25
from bika.lims import bikaMessageFactory as _
26
from bika.lims.browser import BrowserView
27
from bika.lims.browser.analyses import AnalysesView
28
from bika.lims.browser.bika_listing import BikaListingView
29
from bika.lims.browser.chart.analyses import EvolutionChart
30
from bika.lims.browser.resultsimport.autoimportlogs import AutoImportLogsView
31
from bika.lims.content.instrumentmaintenancetask import \
32
    InstrumentMaintenanceTaskStatuses as mstatus
33
from bika.lims.utils import get_image
34
from bika.lims.utils import get_link
35
from bika.lims.utils import get_link_for
36
from senaite.core.i18n import translate as t
37
from plone.app.layout.globals.interfaces import IViewView
38
from Products.CMFCore.utils import getToolByName
39
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
40
from senaite.core.catalog import ANALYSIS_CATALOG
41
from senaite.core.catalog import SETUP_CATALOG
42
from zExceptions import Forbidden
43
from ZODB.POSException import POSKeyError
44
from zope.interface import implements
45
46
47
class InstrumentMaintenanceView(BikaListingView):
48
    """Listing view for instrument maintenance tasks
49
    """
50
51 View Code Duplication
    def __init__(self, context, request):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
52
        super(InstrumentMaintenanceView, self).__init__(context, request)
53
        self.catalog = SETUP_CATALOG
54
        self.contentFilter = {
55
            "portal_type": "InstrumentMaintenanceTask",
56
            "path": {
57
                "query": api.get_path(context),
58
                "depth": 1  # searching just inside the specified folder
59
            },
60
            "sort_on": "created",
61
            "sort_order": "descending",
62
        }
63
64
        self.form_id = "instrumentmaintenance"
65
        self.title = self.context.translate(_("Instrument Maintenance"))
66
67
        self.icon = "{}/{}".format(
68
            self.portal_url,
69
            "++resource++bika.lims.images/instrumentmaintenance_big.png"
70
        )
71
        self.context_actions = {
72
            _("Add"): {
73
                "url": "createObject?type_name=InstrumentMaintenanceTask",
74
                "icon": "++resource++bika.lims.images/add.png"}
75
        }
76
77
        self.allow_edit = False
78
        self.show_select_column = False
79
        self.show_workflow_action_buttons = True
80
        self.pagesize = 30
81
82
        self.columns = {
83
            'getCurrentState': {'title': ''},
84
            'Title': {'title': _('Task'),
85
                      'index': 'sortable_title'},
86
            'getType': {'title': _('Task type', 'Type'), 'sortable': True},
87
            'getDownFrom': {'title': _('Down from'), 'sortable': True},
88
            'getDownTo': {'title': _('Down to'), 'sortable': True},
89
            'getMaintainer': {'title': _('Maintainer'), 'sortable': True},
90
        }
91
92
        self.review_states = [
93
            {
94
                "id": "default",
95
                "title": _("Open"),
96
                "contentFilter": {"is_active": True},
97
                "columns": [
98
                    "getCurrentState",
99
                    "Title",
100
                    "getType",
101
                    "getDownFrom",
102
                    "getDownTo",
103
                    "getMaintainer",
104
                ]
105
            }, {
106
                "id": "cancelled",
107
                "title": _("Cancelled"),
108
                "contentFilter": {"is_active": False},
109
                "columns": [
110
                    "getCurrentState",
111
                    "Title",
112
                    "getType",
113
                    "getDownFrom",
114
                    "getDownTo",
115
                    "getMaintainer",
116
                ]
117
            }, {
118
                "id": "all",
119
                "title": _("All"),
120
                "contentFilter": {},
121
                "columns": [
122
                    "getCurrentState",
123
                    "Title",
124
                    "getType",
125
                    "getDownFrom",
126
                    "getDownTo",
127
                    "getMaintainer"
128
                ]
129
            }
130
        ]
131
132
    def localize_date(self, date):
133
        """Return the localized date
134
        """
135
        return self.ulocalized_time(date, long_format=1)
136
137
    def folderitem(self, obj, item, index):
138
        """Augment folder listing item
139
        """
140
        obj = api.get_object(obj)
141
        url = item.get("url")
142
        title = item.get("Title")
143
144
        item["replace"]["Title"] = get_link(url, value=title)
145
        item["getType"] = _(obj.getType()[0])
146
        item["getDownFrom"] = self.localize_date(obj.getDownFrom())
147
        item["getDownTo"] = self.localize_date(obj.getDownTo())
148
        item["getMaintainer"] = obj.getMaintainer()
149
150
        status = obj.getCurrentState()
151
        statustext = obj.getCurrentStateI18n()
152
        statusimg = ""
153
154
        if status == mstatus.CLOSED:
155
            statusimg = "instrumentmaintenance_closed.png"
156
            item["state_class"] = "state-inactive"
157
        elif status == mstatus.CANCELLED:
158
            statusimg = "instrumentmaintenance_cancelled.png"
159
            item["state_class"] = "state-cancelled"
160
        elif status == mstatus.INQUEUE:
161
            statusimg = "instrumentmaintenance_inqueue.png"
162
            item["state_class"] = "state-open"
163
        elif status == mstatus.OVERDUE:
164
            statusimg = "instrumentmaintenance_overdue.png"
165
            item["state_class"] = "state-open"
166
        elif status == mstatus.PENDING:
167
            statusimg = "instrumentmaintenance_pending.png"
168
            item["state_class"] = "state-pending"
169
170
        item["replace"]["getCurrentState"] = get_image(
171
            statusimg, title=statustext)
172
        return item
173
174
175
class InstrumentCalibrationsView(BikaListingView):
176
    """Listing view for instrument calibrations
177
    """
178
179
    def __init__(self, context, request):
180
        super(InstrumentCalibrationsView, self).__init__(context, request)
181
        self.catalog = SETUP_CATALOG
182
        self.contentFilter = {
183
            "portal_type": "InstrumentCalibration",
184
            "path": {
185
                "query": api.get_path(context),
186
                "depth": 1  # searching just inside the specified folder
187
            },
188
            "sort_on": "created",
189
            "sort_order": "descending",
190
        }
191
192
        self.form_id = "instrumentcalibrations"
193
        self.title = self.context.translate(_("Instrument Calibrations"))
194
        self.icon = "{}/{}".format(
195
            self.portal_url,
196
            "++resource++bika.lims.images/instrumentcalibration_big.png"
197
        )
198
        self.context_actions = {
199
            _("Add"): {
200
                "url": "createObject?type_name=InstrumentCalibration",
201
                "icon": "++resource++bika.lims.images/add.png"}
202
        }
203
204
        self.allow_edit = False
205
        self.show_select_column = False
206
        self.show_workflow_action_buttons = True
207
        self.pagesize = 30
208
209
        # instrument calibrations
210
        calibrations = self.context.getCalibrations()
211
        # current running calibrations
212
        self.active_calibrations = filter(
213
            lambda c: c.isCalibrationInProgress(), calibrations)
214
        self.latest_calibration = self.context.getLatestValidCalibration()
215
216
        self.columns = {
217
            "Title": {"title": _("Task"),
218
                      "index": "sortable_title"},
219
            "getDownFrom": {"title": _("Down from")},
220
            "getDownTo": {"title": _("Down to")},
221
            "getCalibrator": {"title": _("Calibrator")},
222
        }
223
        self.review_states = [
224
            {
225
                "id": "default",
226
                "title": _("All"),
227
                "contentFilter": {},
228
                "columns": [
229
                    "Title",
230
                    "getDownFrom",
231
                    "getDownTo",
232
                    "getCalibrator",
233
                ]
234
            }
235
        ]
236
237
    def localize_date(self, date):
238
        """Return the localized date
239
        """
240
        return self.ulocalized_time(date, long_format=1)
241
242
    def folderitem(self, obj, item, index):
243
        """Augment folder listing item
244
        """
245
        obj = api.get_object(obj)
246
        url = item.get("url")
247
        title = item.get("Title")
248
        calibrator = obj.getCalibrator()
249
250
        item["getDownFrom"] = self.localize_date(obj.getDownFrom())
251
        item["getDownTo"] = self.localize_date(obj.getDownTo())
252
        item["getCalibrator"] = ""
253
        if calibrator:
254
            props = api.get_user_properties(calibrator)
255
            name = props.get("fullname", calibrator)
256
            item["getCalibrator"] = name
257
        item["replace"]["Title"] = get_link(url, value=title)
258
259
        # calibration with the most remaining days
260
        if obj == self.latest_calibration:
261
            item["state_class"] = "state-published"
262
        # running calibrations
263
        elif obj in self.active_calibrations:
264
            item["state_class"] = "state-active"
265
        # inactive calibrations
266
        else:
267
            item["state_class"] = "state-inactive"
268
269
        return item
270
271
272
class InstrumentValidationsView(BikaListingView):
273
    """Listing view for instrument validations
274
    """
275
276
    def __init__(self, context, request):
277
        super(InstrumentValidationsView, self).__init__(context, request)
278
        self.catalog = SETUP_CATALOG
279
        self.contentFilter = {
280
            "portal_type": "InstrumentValidation",
281
            "path": {
282
                "query": api.get_path(context),
283
                "depth": 1  # searching just inside the specified folder
284
            },
285
            "sort_on": "created",
286
            "sort_order": "descending",
287
        }
288
289
        self.form_id = "instrumentvalidations"
290
        self.title = self.context.translate(_("Instrument Validations"))
291
        self.icon = "{}/{}".format(
292
            self.portal_url,
293
            "++resource++bika.lims.images/instrumentvalidation_big.png"
294
        )
295
        self.context_actions = {
296
            _("Add"): {
297
                "url": "createObject?type_name=InstrumentValidation",
298
                "icon": "++resource++bika.lims.images/add.png"}
299
        }
300
301
        self.allow_edit = False
302
        self.show_select_column = False
303
        self.show_workflow_action_buttons = True
304
        self.pagesize = 30
305
306
        # instrument validations
307
        validations = self.context.getValidations()
308
        # current running validations
309
        self.active_validations = filter(
310
            lambda v: v.isValidationInProgress(), validations)
311
        self.latest_validation = self.context.getLatestValidValidation()
312
313
        self.columns = {
314
            "Title": {"title": _("Task"),
315
                      "index": "sortable_title"},
316
            "getDownFrom": {"title": _("Down from")},
317
            "getDownTo": {"title": _("Down to")},
318
            "getValidator": {"title": _("Validator")},
319
        }
320
        self.review_states = [
321
            {
322
                "id": "default",
323
                "title": _("All"),
324
                "contentFilter": {},
325
                "columns": [
326
                    "Title",
327
                    "getDownFrom",
328
                    "getDownTo",
329
                    "getValidator",
330
                ]
331
            }
332
        ]
333
334
    def localize_date(self, date):
335
        """Return the localized date
336
        """
337
        return self.ulocalized_time(date, long_format=1)
338
339
    def folderitem(self, obj, item, index):
340
        """Augment folder listing item
341
        """
342
        obj = api.get_object(obj)
343
        url = item.get("url")
344
        title = item.get("Title")
345
346
        item["getDownFrom"] = self.localize_date(obj.getDownFrom())
347
        item["getDownTo"] = self.localize_date(obj.getDownTo())
348
        item["getValidator"] = obj.getValidator()
349
        item["replace"]["Title"] = get_link(url, value=title)
350
351
        # validation with the most remaining days
352
        if obj == self.latest_validation:
353
            item["state_class"] = "state-published"
354
        # running validations
355
        elif obj in self.active_validations:
356
            item["state_class"] = "state-active"
357
        # inactive validations
358
        else:
359
            item["state_class"] = "state-inactive"
360
361
        return item
362
363
364
class InstrumentScheduleView(BikaListingView):
365
    """Listing view for instrument scheduled tasks
366
    """
367
368 View Code Duplication
    def __init__(self, context, request):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
369
        super(InstrumentScheduleView, self).__init__(context, request)
370
        self.catalog = SETUP_CATALOG
371
        self.contentFilter = {
372
            "portal_type": "InstrumentScheduledTask",
373
            "path": {
374
                "query": api.get_path(context),
375
                "depth": 1  # searching just inside the specified folder
376
            },
377
            "sort_on": "created",
378
            "sort_order": "descending",
379
        }
380
381
        self.form_id = "instrumentschedule"
382
        self.title = self.context.translate(_("Instrument Scheduled Tasks"))
383
384
        self.icon = "{}/{}".format(
385
            self.portal_url,
386
            "++resource++bika.lims.images/instrumentschedule_big.png"
387
        )
388
        self.context_actions = {
389
            _("Add"): {
390
                "url": "createObject?type_name=InstrumentScheduledTask",
391
                "icon": "++resource++bika.lims.images/add.png"}
392
        }
393
394
        self.allow_edit = False
395
        self.show_select_column = False
396
        self.show_workflow_action_buttons = True
397
        self.pagesize = 30
398
399
        self.columns = {
400
            "Title": {"title": _("Scheduled task"),
401
                      "index": "sortable_title"},
402
            "getType": {"title": _("Task type", "Type")},
403
            "getCriteria": {"title": _("Criteria")},
404
            "creator": {"title": _("Created by")},
405
            "created": {"title": _("Created")},
406
        }
407
408
        self.review_states = [
409
            {
410
                "id": "default",
411
                "title": _("Active"),
412
                "contentFilter": {"is_active": True},
413
                "transitions": [{"id": "deactivate"}, ],
414
                "columns": [
415
                    "Title",
416
                    "getType",
417
                    "getCriteria",
418
                    "creator",
419
                    "created",
420
                ]
421
            }, {
422
                "id": "inactive",
423
                "title": _("Inactive"),
424
                "contentFilter": {'is_active': False},
425
                "transitions": [{"id": "activate"}, ],
426
                "columns": [
427
                    "Title",
428
                    "getType",
429
                    "getCriteria",
430
                    "creator",
431
                    "created"
432
                ]
433
            }, {
434
                "id": "all",
435
                "title": _("All"),
436
                "contentFilter": {},
437
                "columns": [
438
                    "Title",
439
                    "getType",
440
                    "getCriteria",
441
                    "creator",
442
                    "created",
443
                ]
444
            }
445
        ]
446
447
    def localize_date(self, date):
448
        """Return the localized date
449
        """
450
        return self.ulocalized_time(date, long_format=1)
451
452
    def folderitem(self, obj, item, index):
453
        """Augment folder listing item
454
        """
455
        obj = api.get_object(obj)
456
        url = item.get("url")
457
        title = item.get("Title")
458
        creator = obj.Creator()
459
460
        item["replace"]["Title"] = get_link(url, value=title)
461
        item["created"] = self.localize_date(obj.created())
462
        item["getType"] = _(obj.getType()[0])
463
        item["creator"] = ""
464
        if creator:
465
            props = api.get_user_properties(creator)
466
            name = props.get("fullname", creator)
467
            item["creator"] = name
468
469
        return item
470
471
472
class InstrumentReferenceAnalysesViewView(BrowserView):
473
    """View of Reference Analyses linked to the Instrument.
474
475
    Only shows the Reference Analyses (Control and Blanks), the rest of regular
476
    and duplicate analyses linked to this instrument are not displayed.
477
478
    The Reference Analyses from an Instrument can be from Worksheets (QC
479
    analysis performed regularly for any Analysis Request) or attached directly
480
    to the instrument, without being linked to any Worksheet).
481
482
    In this case, the Reference Analyses are created automatically by the
483
    instrument import tool.
484
    """
485
486
    implements(IViewView)
487
    template = ViewPageTemplateFile(
488
        "templates/instrument_referenceanalyses.pt")
489
490
    def __init__(self, context, request):
491
        super(InstrumentReferenceAnalysesViewView, self).__init__(
492
            context, request)
493
494
        self.title = self.context.translate(_("Internal Calibration Tests"))
495
        self.icon = "{}/{}".format(
496
            self.portal_url,
497
            "++resource++bika.lims.images/referencesample_big.png"
498
        )
499
        self._analysesview = None
500
501
    def __call__(self):
502
        return self.template()
503
504
    def get_analyses_table_view(self):
505
        view_name = "table_instrument_referenceanalyses"
506
        view = api.get_view(
507
            view_name, context=self.context, request=self.request)
508
        # Call listing hooks
509
        view.update()
510
        view.before_render()
511
512
        # TODO Refactor QC Charts as React Components
513
        # The current QC Chart is rendered by looking at the value from a hidden
514
        # input with id "graphdata", that is rendered below the contents listing
515
        # (see instrument_referenceanalyses.pt).
516
        # The value is a json, is built by folderitem function and is returned
517
        # by self.chart.get_json(). This function is called directly by the
518
        # template on render, but the template itself does not directly render
519
        # the contents listing, but is done asyncronously.
520
        # Hence the function at this point returns an empty dictionary because
521
        # folderitems hasn't been called yet. As a result, the chart appears
522
        # empty. Here, we force folderitems function to be called in order to
523
        # ensure the graphdata is filled before the template is rendered.
524
        view.get_folderitems()
525
        return view
526
527
528
class InstrumentReferenceAnalysesView(AnalysesView):
529
    """View for the table of Reference Analyses linked to the Instrument.
530
531
    Only shows the Reference Analyses (Control and Blanks), the rest of regular
532
    and duplicate analyses linked to this instrument are not displayed.
533
    """
534
535
    def __init__(self, context, request, **kwargs):
536
        AnalysesView.__init__(self, context, request, **kwargs)
537
538
        self.form_id = "{}_qcanalyses".format(api.get_uid(context))
539
        self.allow_edit = True
540
        self.show_select_column = True
541
        self.show_search = False
542
543
        self.catalog = ANALYSIS_CATALOG
544
545
        self.contentFilter = {
546
            "portal_type": "ReferenceAnalysis",
547
            "getInstrumentUID": api.get_uid(self.context),
548
            "sort_on": "getResultCaptureDate",
549
            "sort_order": "reverse"
550
        }
551
552
        # insert the QC-specific columns
553
        self.add_column("getReferenceAnalysesGroupID",
554
                        title=_("QC Sample ID"),
555
                        after="Service")
556
557
        self.add_column("Partition",
558
                        title=_("Reference Sample"),
559
                        after="getReferenceAnalysesGroupID")
560
561
        self.add_column("Retractions", title=_("Retractions"))
562
563
        self.chart = EvolutionChart()
564
565
    def add_column(self, id, **kwargs):
566
        """Add the given column to all review states
567
        """
568
        after = kwargs.pop("after", "")
569
        self.columns[id] = kwargs
570
        for rv in self.review_states:
571
            cols = rv["columns"]
572
            index = len(cols)
573
            if after and after in cols:
574
                index = cols.index(after) + 1
575
            cols.insert(index, id)
576
577
    def isItemAllowed(self, obj):
578
        allowed = super(InstrumentReferenceAnalysesView,
579
                        self).isItemAllowed(obj)
580
        return allowed or obj.getResult != ""
581
582
    def folderitem(self, obj, item, index):
583
        item = super(InstrumentReferenceAnalysesView,
584
                     self).folderitem(obj, item, index)
585
586
        # get the full object
587
        analysis = self.get_object(obj)
588
589
        # Partition is used to group/toggle QC Analyses
590
        sample = analysis.getSample()
591
        item["replace"]["Partition"] = get_link(api.get_url(sample),
592
                                                api.get_id(sample))
593
594
        # Get retractions field
595
        item["Retractions"] = ""
596
        report = analysis.getRetractedAnalysesPdfReport()
597
        if report:
598
            url = api.get_url(analysis)
599
            href = "{}/at_download/RetractedAnalysesPdfReport".format(url)
600
            attrs = {"class": "pdf", "target": "_blank"}
601
            title = _("Retractions")
602
            link = get_link(href, title, **attrs)
603
            item["Retractions"] = title
604
            item["replace"]["Retractions"] = link
605
606
        # Add the analysis to the QC Chart
607
        self.chart.add_analysis(analysis)
608
609
        return item
610
611
    def _folder_item_instrument(self, analysis_brain, item):
612
        """Always display the current instrument in read-only mode
613
        """
614
        instrument = self.get_instrument(analysis_brain)
615
        item["Instrument"] = api.get_uid(instrument)
616
        item["replace"]["Instrument"] = get_link_for(instrument, tabindex="-1")
617
618
619
class InstrumentCertificationsView(BikaListingView):
620
    """Listing view for instrument certifications
621
    """
622
623
    def __init__(self, context, request, **kwargs):
624
        BikaListingView.__init__(self, context, request, **kwargs)
625
        self.catalog = SETUP_CATALOG
626
        self.contentFilter = {
627
            "portal_type": "InstrumentCertification",
628
            "path": {
629
                "query": api.get_path(context),
630
                "depth": 1  # searching just inside the specified folder
631
            },
632
            "sort_on": "created",
633
            "sort_order": "descending",
634
        }
635
636
        self.form_id = "instrumentcertifications"
637
        self.title = self.context.translate(_("Calibration Certificates"))
638
        self.icon = "{}/{}".format(
639
            self.portal_url,
640
            "++resource++bika.lims.images/instrumentcertification_big.png"
641
        )
642
        self.context_actions = {
643
            _("Add"): {
644
                "url": "createObject?type_name=InstrumentCertification",
645
                "icon": "++resource++bika.lims.images/add.png"
646
            }
647
        }
648
649
        self.allow_edit = False
650
        self.show_select_column = False
651
        self.show_workflow_action_buttons = True
652
        self.pagesize = 30
653
654
        # latest valid certificate UIDs
655
        self.valid_certificates = self.context.getValidCertifications()
656
        self.latest_certificate = self.context.getLatestValidCertification()
657
658
        self.columns = {
659
            "Title": {"title": _("Cert. Num"), "index": "sortable_title"},
660
            "getAgency": {"title": _("Agency"), "sortable": False},
661
            "getDate": {"title": _("Date"), "sortable": False},
662
            "getValidFrom": {"title": _("Valid from"), "sortable": False},
663
            "getValidTo": {"title": _("Valid to"), "sortable": False},
664
            "getDocument": {"title": _("Document"), "sortable": False},
665
        }
666
667
        self.review_states = [
668
            {
669
                "id": "default",
670
                "title": _("All"),
671
                "contentFilter": {},
672
                "columns": [
673
                    "Title",
674
                    "getAgency",
675
                    "getDate",
676
                    "getValidFrom",
677
                    "getValidTo",
678
                    "getDocument",
679
                ],
680
                "transitions": []
681
            }
682
        ]
683
684
    def get_document(self, certificate):
685
        """Return the document of the given document
686
        """
687
        try:
688
            return certificate.getDocument()
689
        except POSKeyError:  # POSKeyError: "No blob file"
690
            # XXX When does this happen?
691
            return None
692
693
    def localize_date(self, date):
694
        """Return the localized date
695
        """
696
        return self.ulocalized_time(date, long_format=0)
697
698
    def folderitem(self, obj, item, index):
699
        """Augment folder listing item with additional data
700
        """
701
        obj = api.get_object(obj)
702
        url = item.get("url")
703
        title = item.get("Title")
704
705
        item["replace"]["Title"] = get_link(url, value=title)
706
        item["getDate"] = self.localize_date(obj.getDate())
707
        item["getValidFrom"] = self.localize_date(obj.getValidFrom())
708
        item["getValidTo"] = self.localize_date(obj.getValidTo())
709
710
        if obj.getInternal() is True:
711
            item["replace"]["getAgency"] = ""
712
            item["state_class"] = "%s %s" % \
713
                (item["state_class"], "internalcertificate")
714
715
        item["getDocument"] = ""
716
        item["replace"]["getDocument"] = ""
717
        doc = self.get_document(obj)
718
        if doc and doc.get_size() > 0:
719
            filename = doc.filename
720
            download_url = "{}/at_download/Document".format(url)
721
            anchor = get_link(download_url, filename)
722
            item["getDocument"] = filename
723
            item["replace"]["getDocument"] = anchor
724
725
        # Latest valid certificate
726
        if obj == self.latest_certificate:
727
            item["state_class"] = "state-published"
728
        # Valid certificate
729
        elif obj in self.valid_certificates:
730
            item["state_class"] = "state-valid state-published"
731
        # Invalid certificates
732
        else:
733
            img = get_image("exclamation.png", title=t(_("Out of date")))
734
            item["replace"]["getValidTo"] = "%s %s" % (item["getValidTo"], img)
735
            item["state_class"] = "state-invalid"
736
737
        return item
738
739
740
class InstrumentAutoImportLogsView(AutoImportLogsView):
741
    """Logs of Auto-Imports of this instrument.
742
    """
743
744
    def __init__(self, context, request, **kwargs):
745
        AutoImportLogsView.__init__(self, context, request, **kwargs)
746
        del self.columns["Instrument"]
747
        self.review_states[0]["columns"].remove("Instrument")
748
        self.contentFilter = {
749
            "portal_type": "AutoImportLog",
750
            "path": {
751
                "query": api.get_path(context),
752
            },
753
            "sort_on": "created",
754
            "sort_order": "descending",
755
        }
756
757
        instrument = self.context.Title()
758
        self.title = self.context.translate(
759
            _(u"Auto Import Logs of %s" % api.safe_unicode(instrument)))
760
        self.icon = "{}/{}".format(
761
            self.portal_url,
762
            "++resource++bika.lims.images/instrumentcertification_big.png"
763
        )
764
        self.context_actions = {}
765
766
        self.allow_edit = False
767
        self.show_select_column = False
768
        self.show_workflow_action_buttons = True
769
        self.pagesize = 30
770
771
772
class InstrumentMultifileView(BikaListingView):
773
    """Listing view for instrument multi files
774
    """
775
776
    def __init__(self, context, request):
777
        super(InstrumentMultifileView, self).__init__(context, request)
778
779
        self.catalog = "senaite_catalog_setup"
780
        self.contentFilter = {
781
            "portal_type": "Multifile",
782
            "path": {
783
                "query": api.get_path(context),
784
                "depth": 1  # searching just inside the specified folder
785
            },
786
            "sort_on": "created",
787
            "sort_order": "descending",
788
        }
789
790
        self.form_id = "instrumentfiles"
791
        self.title = self.context.translate(_("Instrument Files"))
792
        self.icon = "{}/{}".format(
793
            self.portal_url,
794
            "++resource++bika.lims.images/instrumentcertification_big.png"
795
        )
796
        self.context_actions = {
797
            _("Add"): {
798
                "url": "createObject?type_name=Multifile",
799
                "icon": "++resource++bika.lims.images/add.png"
800
            }
801
        }
802
803
        self.allow_edit = False
804
        self.show_select_column = False
805
        self.show_workflow_action_buttons = True
806
        self.pagesize = 30
807
808
        self.columns = {
809
            "DocumentID": {"title": _("Document ID"),
810
                           "index": "sortable_title"},
811
            "DocumentVersion": {"title": _("Document Version"),
812
                                "index": "sortable_title"},
813
            "DocumentLocation": {"title": _("Document Location"),
814
                                 "index": "sortable_title"},
815
            "DocumentType": {"title": _("Document Type"),
816
                             "index": "sortable_title"},
817
            "FileDownload": {"title": _("File")}
818
        }
819
820
        self.review_states = [
821
            {
822
                "id": "default",
823
                "title": _("All"),
824
                "contentFilter": {},
825
                "columns": [
826
                    "DocumentID",
827
                    "DocumentVersion",
828
                    "DocumentLocation",
829
                    "DocumentType",
830
                    "FileDownload"
831
                ]
832
            },
833
        ]
834
835
    def get_file(self, obj):
836
        """Return the file of the given object
837
        """
838
        try:
839
            return obj.getFile()
840
        except POSKeyError:  # POSKeyError: "No blob file"
841
            # XXX When does this happen?
842
            return None
843
844
    def folderitem(self, obj, item, index):
845
        """Augment folder listing item with additional data
846
        """
847
        obj = api.get_object(obj)
848
        url = item.get("url")
849
        title = obj.getDocumentID()
850
851
        item["replace"]["DocumentID"] = get_link(url, title)
852
853
        item["FileDownload"] = ""
854
        item["replace"]["FileDownload"] = ""
855
        file = self.get_file(obj)
856
        if file and file.get_size() > 0:
857
            filename = file.filename
858
            download_url = "{}/at_download/File".format(url)
859
            anchor = get_link(download_url, filename)
860
            item["FileDownload"] = filename
861
            item["replace"]["FileDownload"] = anchor
862
863
        item["DocumentVersion"] = obj.getDocumentVersion()
864
        item["DocumentLocation"] = obj.getDocumentLocation()
865
        item["DocumentType"] = obj.getDocumentType()
866
867
        return item
868
869
870
class ajaxGetInstrumentMethods(BrowserView):
871
    """ Returns the method assigned to the defined instrument.
872
        uid: unique identifier of the instrument
873
    """
874
    # Modified to return multiple methods after enabling multiple method
875
    # for intruments.
876
    def __call__(self):
877
        out = {
878
            "title": None,
879
            "instrument": None,
880
            "methods": [],
881
        }
882
        try:
883
            plone.protect.CheckAuthenticator(self.request)
884
        except Forbidden:
885
            return json.dumps(out)
886
        bsc = getToolByName(self, "senaite_catalog_setup")
887
        results = bsc(portal_type="Instrument",
888
                      UID=self.request.get("uid", "0"))
889
        instrument = results[0] if results and len(results) == 1 else None
890
        if instrument:
891
            instrument_obj = instrument.getObject()
892
            out["title"] = instrument_obj.Title()
893
            out["instrument"] = instrument.UID
894
            # Handle multiple Methods per instrument
895
            methods = instrument_obj.getMethods()
896
            for method in methods:
897
                out["methods"].append({
898
                    "uid": method.UID(),
899
                    "title": method.Title(),
900
                })
901
        return json.dumps(out)
902