Passed
Push — 2.x ( b620cc...641e48 )
by Jordi
07:20
created

ReferenceResultsView.folderitem()   B

Complexity

Conditions 6

Size

Total Lines 45
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 28
dl 0
loc 45
rs 8.2746
c 0
b 0
f 0
cc 6
nop 4
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 collections
22
from datetime import datetime
23
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.utils import get_image
31
from bika.lims.utils import get_link
32
from bika.lims.utils import get_link_for
33
from plone.app.layout.globals.interfaces import IViewView
34
from plone.memoize import view
35
from Products.ATContentTypes.utils import DT2dt
36
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
37
from senaite.core.i18n import translate as t
38
from zope.interface import implements
39
40
41
class ViewView(BrowserView):
42
    """Reference Sample View
43
    """
44
    implements(IViewView)
45
    template = ViewPageTemplateFile("templates/referencesample_view.pt")
46
47
    def __init__(self, context, request):
48
        super(ViewView, self).__init__(context, request)
49
50
        self.icon = "{}/{}".format(
51
            self.portal_url,
52
            "++resource++bika.lims.images/referencesample_big.png")
53
54
    def __call__(self):
55
        self.results = {}  # {category_title: listofdicts}
56
        for r in self.context.getReferenceResults():
57
            service = api.get_object_by_uid(r["uid"])
58
            cat = service.getCategoryTitle()
59
            if cat not in self.results:
60
                self.results[cat] = []
61
            r["service"] = service
62
            self.results[cat].append(r)
63
        self.categories = self.results.keys()
64
        self.categories.sort()
65
        return self.template()
66
67
68
class ReferenceAnalysesViewView(BrowserView):
69
    """ View of Reference Analyses linked to the Reference Sample.
70
    """
71
72
    implements(IViewView)
73
    template = ViewPageTemplateFile(
74
        "templates/referencesample_analyses.pt")
75
76
    def __init__(self, context, request):
77
        super(ReferenceAnalysesViewView, self).__init__(context, request)
78
79
        self.title = self.context.translate(_("Reference Analyses"))
80
        self.icon = "{}/{}".format(
81
            self.portal_url,
82
            "++resource++bika.lims.images/referencesample_big.png")
83
84
    def __call__(self):
85
        return self.template()
86
87
    def get_analyses_table(self):
88
        """ Returns the table of Reference Analyses
89
        """
90
        return self.get_analyses_view().contents_table()
91
92
    def get_analyses_table_view(self):
93
        view_name = "table_referenceanalyses"
94
        view = api.get_view(
95
            view_name, context=self.context, request=self.request)
96
        # Call listing hooks
97
        view.update()
98
        view.before_render()
99
100
        # TODO Refactor QC Charts as React Components
101
        # The current QC Chart is rendered by looking at the value from a
102
        # hidden input with id "graphdata", that is rendered below the contents
103
        # listing (see referenceanalyses.pt).
104
        # The value is a json, is built by folderitem function and is returned
105
        # by self.chart.get_json(). This function is called directly by the
106
        # template on render, but the template itself does not directly render
107
        # the contents listing, but is done asyncronously.
108
        # Hence the function at this point returns an empty dictionary because
109
        # folderitems hasn't been called yet. As a result, the chart appears
110
        # empty. Here, we force folderitems function to be called in order to
111
        # ensure the graphdata is filled before the template is rendered.
112
        view.get_folderitems()
113
        return view
114
115
116
class ReferenceAnalysesView(AnalysesView):
117
    """Reference Analyses on this sample
118
    """
119
120 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...
121
        super(ReferenceAnalysesView, self).__init__(context, request)
122
123
        self.form_id = "{}_qcanalyses".format(api.get_uid(context))
124
        self.allow_edit = True
125
        self.show_select_column = True
126
        self.show_search = False
127
128
        self.contentFilter = {
129
            "portal_type": "ReferenceAnalysis",
130
            "path": {
131
                "query": "/".join(self.context.getPhysicalPath()),
132
                "level": 0},
133
            "sort_on": "getResultCaptureDate",
134
            "sort_order": "descending"
135
        }
136
137
        # insert the QC-specific columns
138
        self.add_column("getReferenceAnalysesGroupID",
139
                        title=_("QC Analysis ID"),
140
                        after="Service")
141
        self.add_column("Worksheet",
142
                        title=_("Worksheet"),
143
                        after="getReferenceAnalysesGroupID")
144
145
        self.chart = EvolutionChart()
146
147 View Code Duplication
    def add_column(self, id, **kwargs):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
148
        """Add the given column to all review states
149
        """
150
        after = kwargs.pop("after", "")
151
        self.columns[id] = kwargs
152
        for rv in self.review_states:
153
            cols = rv["columns"]
154
            index = len(cols)
155
            if after and after in cols:
156
                index = cols.index(after) + 1
157
            cols.insert(index, id)
158
159
    def folderitem(self, obj, item, index):
160
        """Service triggered each time an item is iterated in folderitems.
161
162
        The use of this service prevents the extra-loops in child objects.
163
164
        :obj: the instance of the class to be foldered
165
        :item: dict containing the properties of the object to be used by
166
            the template
167
        :index: current index of the item
168
        """
169
        item = super(ReferenceAnalysesView, self).folderitem(obj, item, index)
170
171
        analysis = self.get_object(obj)
172
        ws = analysis.getWorksheet()
173
        if ws:
174
            item["Worksheet"] = api.get_uid(ws)
175
            item["replace"]["Worksheet"] = get_link_for(ws)
176
177
        # Add the analysis to the QC Chart
178
        self.chart.add_analysis(obj)
179
        return item
180
181
182
class ReferenceResultsView(BikaListingView):
183
    """Listing of all reference results
184
    """
185
186
    def __init__(self, context, request):
187
        super(ReferenceResultsView, self).__init__(context, request)
188
189
        self.catalog = "senaite_catalog_setup"
190
        self.contentFilter = {
191
            "portal_type": "AnalysisService",
192
            "UID": self.get_reference_results().keys(),
193
            "is_active": True,
194
            "sort_on": "sortable_title",
195
            "sort_order": "ascending",
196
        }
197
        self.context_actions = {}
198
        self.title = self.context.translate(_("Reference Values"))
199
        self.icon = "{}/{}".format(
200
            self.portal_url,
201
            "/++resource++bika.lims.images/referencesample_big.png"
202
        )
203
204
        self.show_select_row = False
205
        self.show_workflow_action_buttons = False
206
        self.show_select_column = False
207
        self.pagesize = 999999
208
        self.show_search = False
209
210
        # Categories
211
        self.categories = []
212
        if self.show_categories_enabled():
213
            self.show_categories = True
214
            self.expand_all_categories = True
215
216
        self.columns = collections.OrderedDict((
217
            ("Title", {
218
                "title": _("Analysis Service"),
219
                "sortable": False}),
220
            ("result", {
221
                "title": _("Expected Result"),
222
                "sortable": False}),
223
            ("error", {
224
                "title": _("Permitted Error %"),
225
                "sortable": False}),
226
            ("min", {
227
                "title": _("Min"),
228
                "sortable": False}),
229
            ("max", {
230
                "title": _("Max"),
231
                "sortable": False}),
232
        ))
233
234
        self.review_states = [
235
            {
236
                "id": "default",
237
                "title": _("All"),
238
                "contentFilter": {},
239
                "columns": self.columns.keys()
240
            }
241
        ]
242
243
    def update(self):
244
        """Update hook
245
        """
246
        super(ReferenceResultsView, self).update()
247
        self.categories.sort()
248
249
    @view.memoize
250
    def show_categories_enabled(self):
251
        """Check in the setup if categories are enabled
252
        """
253
        return self.context.bika_setup.getCategoriseAnalysisServices()
254
255
    @view.memoize
256
    def get_reference_results(self):
257
        """Return a mapping of Analysis Service -> Reference Results
258
        """
259
        referenceresults = self.context.getReferenceResults()
260
        return dict(map(lambda rr: (rr.get("uid"), rr), referenceresults))
261
262
    def folderitem(self, obj, item, index):
263
        """Service triggered each time an item is iterated in folderitems.
264
265
        The use of this service prevents the extra-loops in child objects.
266
267
        :obj: the instance of the class to be foldered
268
        :item: dict containing the properties of the object to be used by
269
            the template
270
        :index: current index of the item
271
        """
272
273
        obj = api.get_object(obj)
274
        uid = api.get_uid(obj)
275
        url = api.get_url(obj)
276
        title = api.get_title(obj)
277
278
        # get the category
279
        if self.show_categories_enabled():
280
            category = obj.getCategoryTitle()
281
            if category not in self.categories:
282
                self.categories.append(category)
283
            item["category"] = category
284
285
        ref_results = self.get_reference_results()
286
        ref_result = ref_results.get(uid)
287
288
        item["Title"] = title
289
        item["replace"]["Title"] = get_link(url, value=title)
290
        item["result"] = ref_result.get("result")
291
        item["min"] = ref_result.get("min")
292
        item["max"] = ref_result.get("max")
293
        item["error"] = ref_result.get("error")
294
295
        # Icons
296
        after_icons = ""
297
        if obj.getAccredited():
298
            after_icons += get_image(
299
                "accredited.png", title=_("Accredited"))
300
        if obj.getAttachmentRequired():
301
            after_icons += get_image(
302
                "attach_reqd.png", title=_("Attachment required"))
303
        if after_icons:
304
            item["after"]["Title"] = after_icons
305
306
        return item
307
308
309
class ReferenceSamplesView(BikaListingView):
310
    """Main reference samples folder view
311
    """
312
313
    def __init__(self, context, request):
314
        super(ReferenceSamplesView, self).__init__(context, request)
315
316
        self.catalog = "senaite_catalog"
317
318
        self.contentFilter = {
319
            "portal_type": "ReferenceSample",
320
            "sort_on": "sortable_title",
321
            "sort_order": "ascending",
322
            "path": {"query": ["/"], "level": 0},
323
        }
324
325
        self.context_actions = {}
326
        self.title = self.context.translate(_("Reference Samples"))
327
        self.icon = "{}/{}".format(
328
            self.portal_url,
329
            "++resource++bika.lims.images/referencesample_big.png")
330
331
        self.show_select_column = True
332
        self.pagesize = 25
333
334
        self.columns = collections.OrderedDict((
335
            ("ID", {
336
                "title": _("ID"),
337
                "index": "id"}),
338
            ("Title", {
339
                "title": _("Title"),
340
                "index": "sortable_title",
341
                "toggle": True}),
342
            ("Supplier", {
343
                "title": _("Supplier"),
344
                "toggle": True}),
345
            ("Manufacturer", {
346
                "title": _("Manufacturer"),
347
                "toggle": True}),
348
            ("Definition", {
349
                "title": _("Reference Definition"),
350
                "toggle": True}),
351
            ("DateSampled", {
352
                "title": _("Date Sampled"),
353
                "index": "getDateSampled",
354
                "toggle": True}),
355
            ("DateReceived", {
356
                "title": _("Date Received"),
357
                "index": "getDateReceived",
358
                "toggle": True}),
359
            ("DateOpened", {
360
                "title": _("Date Opened"),
361
                "toggle": True}),
362
            ("ExpiryDate", {
363
                "title": _("Expiry Date"),
364
                "index": "getExpiryDate",
365
                "toggle": True}),
366
            ("state_title", {
367
                "title": _("State"),
368
                "toggle": True}),
369
        ))
370
371
        self.review_states = [
372
            {
373
                "id": "default",
374
                "title": _("Current"),
375
                "contentFilter": {"review_state": "current"},
376
                "columns": self.columns.keys(),
377
            }, {
378
                "id": "expired",
379
                "title": _("Expired"),
380
                "contentFilter": {"review_state": "expired"},
381
                "columns": self.columns.keys(),
382
            }, {
383
                "id": "disposed",
384
                "title": _("Disposed"),
385
                "contentFilter": {"review_state": "disposed"},
386
                "columns": self.columns.keys(),
387
            }, {
388
                "id": "all",
389
                "title": _("All"),
390
                "contentFilter": {},
391
                "columns": self.columns.keys(),
392
            }
393
        ]
394
395
    def before_render(self):
396
        """Before template render hook
397
        """
398
        super(ReferenceSamplesView, self).before_render()
399
400
    def folderitem(self, obj, item, index):
401
        """Applies new properties to the item (Client) that is currently being
402
        rendered as a row in the list
403
404
        :param obj: client to be rendered as a row in the list
405
        :param item: dict representation of the client, suitable for the list
406
        :param index: current position of the item within the list
407
        :type obj: ATContentType/DexterityContentType
408
        :type item: dict
409
        :type index: int
410
        :return: the dict representation of the item
411
        :rtype: dict
412
        """
413
414
        obj = api.get_object(obj)
415
416
        # XXX Refactor expiration to a proper place
417
        # ---------------------------- 8< -------------------------------------
418
        if item.get("review_state", "current") == "current":
419
            # Check expiry date
420
            exdate = obj.getExpiryDate()
421
            if exdate:
422
                expirydate = DT2dt(exdate).replace(tzinfo=None)
423
                if (datetime.today() > expirydate):
424
                    # Trigger expiration
425
                    self.workflow.doActionFor(obj, "expire")
426
                    item["review_state"] = "expired"
427
                    item["obj"] = obj
428
429
        if self.contentFilter.get('review_state', '') \
430
           and item.get('review_state', '') == 'expired':
431
            # This item must be omitted from the list
432
            return None
433
        # ---------------------------- >8 -------------------------------------
434
435
        url = api.get_url(obj)
436
        id = api.get_id(obj)
437
438
        item["ID"] = id
439
        item["replace"]["ID"] = get_link(url, value=id)
440
        item["DateSampled"] = self.ulocalized_time(
441
            obj.getDateSampled(), long_format=True)
442
        item["DateReceived"] = self.ulocalized_time(obj.getDateReceived())
443
        item["DateOpened"] = self.ulocalized_time(obj.getDateOpened())
444
        item["ExpiryDate"] = self.ulocalized_time(obj.getExpiryDate())
445
446
        manufacturer = obj.getManufacturer()
447
        item["replace"]["Manufacturer"] = get_link_for(manufacturer)
448
449
        supplier = api.get_parent(obj)
450
        item["replace"]["Supplier"] = get_link_for(supplier)
451
452
        ref_definition = obj.getReferenceDefinition()
453
        item["replace"]["Definition"] = get_link_for(ref_definition)
454
455
        # Icons
456
        after_icons = ''
457
        if obj.getBlank():
458
            after_icons += get_image(
459
                "blank.png", title=t(_("Blank")))
460
        if obj.getHazardous():
461
            after_icons += get_image(
462
                "hazardous.png", title=t(_("Hazardous")))
463
        if after_icons:
464
            item["after"]["ID"] = after_icons
465
466
        return item
467