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

PrintView.getWorksheet()   A

Complexity

Conditions 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 8
rs 10
c 0
b 0
f 0
cc 2
nop 1
1
# -*- coding: utf-8 -*-
2
#
3
# This file is part of SENAITE.CORE.
4
#
5
# SENAITE.CORE is free software: you can redistribute it and/or modify it under
6
# the terms of the GNU General Public License as published by the Free Software
7
# Foundation, version 2.
8
#
9
# This program is distributed in the hope that it will be useful, but WITHOUT
10
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12
# details.
13
#
14
# You should have received a copy of the GNU General Public License along with
15
# this program; if not, write to the Free Software Foundation, Inc., 51
16
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17
#
18
# Copyright 2018-2025 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
import os
22
import traceback
23
from operator import itemgetter
24
25
from bika.lims import api
26
from bika.lims import bikaMessageFactory as _
27
from bika.lims import logger
28
from bika.lims.api.analysis import is_out_of_range
29
from bika.lims.browser import BrowserView
30
from bika.lims.config import POINTS_OF_CAPTURE
31
from bika.lims.config import WS_TEMPLATES_ADDON_DIR
32
from bika.lims.interfaces import IReferenceAnalysis
33
from bika.lims.interfaces import IReferenceSample
34
from bika.lims.utils import format_supsub
35
from bika.lims.utils import formatDecimalMark
36
from bika.lims.utils import get_client
37
from bika.lims.utils import to_utf8
38
from bika.lims.utils.analysis import format_uncertainty
39
from DateTime import DateTime
40
from plone.memoize import view
41
from plone.resource.utils import queryResourceDirectory
42
from Products.CMFCore.utils import getToolByName
43
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
44
from senaite.core.config.registry import WS_PRINT_TMPL_RECORD
45
from senaite.core.p3compat import cmp
46
from senaite.core.registry import get_registry_record
47
48
49
class PrintView(BrowserView):
50
    """ Print view for a worksheet. This view acts as a placeholder, so
51
        the user can select the preferred options (AR by columns, AR by
52
        rows, etc.) for printing. Both a print button and pdf button
53
        are shown.
54
    """
55
56
    template = ViewPageTemplateFile("../templates/print.pt")
57
    _DEFAULT_NUMCOLS = 3
58
    _TEMPLATES_DIR = '../templates/print'
59
    _TEMPLATES_LIST = []
60
    _current_ws_index = 0
61
    _worksheets = []
62
63
    def __init__(self, context, request):
64
        super(PrintView, self).__init__(context, request)
65
        self._worksheets = [self.context]
66
        self._TEMPLATES_LIST = get_registry_record(WS_PRINT_TMPL_RECORD)
67
68
    def __call__(self):
69
        """ Entry point of PrintView.
70
            If context.portal_type is a Worksheet, then the PrintView
71
            is initialized to manage only that worksheet. If the
72
            context.portal_type is a WorksheetFolder and there are
73
            items selected in the request (items param), the PrintView
74
            will show the preview for all the selected Worksheets.
75
            By default, returns a HTML-encoded template, but if the
76
            request contains a param 'pdf' with value 1, will flush a
77
            pdf for the worksheet.
78
        """
79
80
        if self.context.portal_type == 'Worksheet':
81
            self._worksheets = [self.context]
82
83
        elif self.context.portal_type == 'WorksheetFolder' and \
84
                self.request.get('items', ''):
85
            uids = self.request.get('items').split(',')
86
            uc = getToolByName(self.context, 'uid_catalog')
87
            self._worksheets = [obj.getObject() for obj in uc(UID=uids)]
88
89
        else:
90
            # Warn and redirect to referer
91
            logger.warning('PrintView: type not allowed: %s' %
92
                           self.context.portal_type)
93
            self.destination_url = self.request.get_header(
94
                "referer", self.context.absolute_url())
95
96
        # Generate PDF?
97
        if self.request.form.get('pdf', '0') == '1':
98
            return self._flush_pdf()
99
        else:
100
            return self.template()
101
102
    @view.memoize
103
    def get_default_decimal_mark(self):
104
        """Returns the default decimal mark from the setup
105
        """
106
        setup = api.get_setup()
107
        return setup.getDecimalMark()
108
109
    def get_analyses_data_by_title(self, ar_data, title):
110
        """A template helper to pick an Analysis identified by the name of the
111
        current Analysis Service.
112
113
        ar_data is the dictionary structure which is returned by _ws_data
114
        """
115
        analyses = ar_data.get("analyses", [])
116
        analyses = filter(lambda an: an.get("title") == title, analyses)
117
        # Sort by creation date (so retests are always displayed at the bottom)
118
        return sorted(analyses, key=itemgetter("created"))
119
120
    def getWSTemplates(self):
121
        """ Returns a DisplayList with the available templates found in
122
            templates/worksheets
123
        """
124
        out = []
125
        for template_id in self._TEMPLATES_LIST:
126
            name_parts = template_id.replace(".pt", "").split(":")
127
            template_filename = " ".join(map(lambda p: p.capitalize(), name_parts[1].split("_")))
128
            out.append({
129
                "id": template_id,
130
                "title": "{} ({})".format(template_filename, name_parts[0]),
131
            })
132
        return out
133
134
    def renderWSTemplate(self):
135
        """ Returns the current worksheet rendered with the template
136
            specified in the request (param 'template').
137
            Moves the iterator to the next worksheet available.
138
        """
139
        templates_dir = self._TEMPLATES_DIR
140
        embedt = self.request.get('template', self._TEMPLATES_LIST[0])
141
        if embedt.find(':') >= 0:
142
            prefix, embedt = embedt.split(':')
143
            templates_dir = queryResourceDirectory(WS_TEMPLATES_ADDON_DIR, prefix).directory
144
        embed = ViewPageTemplateFile(os.path.join(templates_dir, embedt))
145
        reptemplate = ""
146
        try:
147
            reptemplate = embed(self)
148
        except Exception:
149
            tbex = traceback.format_exc()
150
            wsid = self._worksheets[self._current_ws_index].id
151
            reptemplate = "<div class='error-print'>%s - %s '%s':<pre>%s</pre></div>" % (wsid, _("Unable to load the template"), embedt, tbex)
152
        if self._current_ws_index < len(self._worksheets):
153
            self._current_ws_index += 1
154
        return reptemplate
155
156
    def getCSS(self):
157
        """ Returns the css style to be used for the current template.
158
            If the selected template is 'default.pt', this method will
159
            return the content from 'default.css'. If no css file found
160
            for the current template, returns empty string
161
        """
162
        template = self.request.get('template', self._TEMPLATES_LIST[0])
163
        content = ''
164
        if template.find(':') >= 0:
165
            prefix, template = template.split(':')
166
            resource = queryResourceDirectory(WS_TEMPLATES_ADDON_DIR, prefix)
167
            css = '{0}.css'.format(template[:-3])
168
            if css in resource.listDirectory():
169
                content = resource.readFile(css)
170
        else:
171
            this_dir = os.path.dirname(os.path.abspath(__file__))
172
            templates_dir = os.path.join(this_dir, self._TEMPLATES_DIR)
173
            path = '%s/%s.css' % (templates_dir, template[:-3])
174
            with open(path, 'r') as content_file:
175
                content = content_file.read()
176
        return content
177
178
    def getNumColumns(self):
179
        """ Returns the number of columns to display
180
        """
181
        return int(self.request.get('numcols', self._DEFAULT_NUMCOLS))
182
183
    def getWorksheets(self):
184
        """ Returns the list of worksheets to be printed
185
        """
186
        return self._worksheets
187
188
    def getWorksheet(self):
189
        """ Returns the current worksheet from the list. Returns None when
190
            the iterator reaches the end of the array.
191
        """
192
        ws = None
193
        if self._current_ws_index < len(self._worksheets):
194
            ws = self._ws_data(self._worksheets[self._current_ws_index])
195
        return ws
196
197
    def splitList(self, elements, chunksnum):
198
        """ Splits a list to a n lists with chunksnum number of elements
199
            each one.
200
            For a list [3,4,5,6,7,8,9] with chunksunum 4, the method
201
            will return the following list of groups:
202
            [[3,4,5,6],[7,8,9]]
203
        """
204
        if len(elements) < chunksnum:
205
            return [elements]
206
        groups = zip(*[elements[i::chunksnum] for i in range(chunksnum)])
207
        if len(groups) * chunksnum < len(elements):
208
            groups.extend([elements[-(len(elements) - len(groups) * chunksnum):]])
209
        return groups
210
211
    def _lab_data(self):
212
        """ Returns a dictionary that represents the lab object
213
            Keys: obj, title, url, address, confidence, accredited,
214
                  accreditation_body, accreditation_logo, logo
215
        """
216
        portal = self.context.portal_url.getPortalObject()
217
        lab = self.context.bika_setup.laboratory
218
        lab_address = lab.getPostalAddress() \
219
            or lab.getBillingAddress() \
220
            or lab.getPhysicalAddress()
221
        if lab_address:
222
            _keys = ['address', 'city', 'state', 'zip', 'country']
223
            _list = ["<div>%s</div>" % lab_address.get(v) for v in _keys
224
                     if lab_address.get(v)]
225
            lab_address = "".join(_list)
226
        else:
227
            lab_address = ''
228
229
        return {'obj': lab,
230
                'title': to_utf8(lab.Title()),
231
                'url': to_utf8(lab.getLabURL()),
232
                'address': to_utf8(lab_address),
233
                'confidence': lab.getConfidence(),
234
                'accredited': lab.getLaboratoryAccredited(),
235
                'accreditation_body': to_utf8(lab.getAccreditationBody()),
236
                'accreditation_logo': lab.getAccreditationBodyLogo(),
237
                'logo': "%s/logo_print.png" % portal.absolute_url()}
238
239
    def _ws_data(self, ws):
240
        """ Creates an ws dict, accessible from the view and from each
241
            specific template.
242
            Keys: obj, id, url, template_title, remarks, date_printed,
243
                ars, createdby, analyst, printedby, analyses_titles,
244
                portal, laboratory
245
        """
246
        data = {
247
            'obj': ws,
248
            'id': ws.id,
249
            'url': ws.absolute_url(),
250
            'template_title': ws.getWorksheetTemplateTitle(),
251
            'remarks': ws.getRemarks(),
252
            'date_printed': self.ulocalized_time(DateTime(), long_format=1),
253
            'date_created': self.ulocalized_time(ws.created(), long_format=1),
254
        }
255
256
        # Sub-objects
257
        data['ars'] = self._analyses_data(ws)
258
        data['createdby'] = self._createdby_data(ws)
259
        data['analyst'] = self._analyst_data(ws)
260
        data['printedby'] = self._printedby_data(ws)
261
262
        # Unify the analyses titles for the template
263
        # N.B. The Analyses come in sorted, so don't use a set() to unify them,
264
        #      because it sorts the Analyses alphabetically
265
        ans = []
266
        for ar in data['ars']:
267
            for an in ar['analyses']:
268
                title = an["title"]
269
                if title in ans:
270
                    continue
271
                ans.append(title)
272
        data['analyses_titles'] = ans
273
274
        portal = self.context.portal_url.getPortalObject()
275
        data['portal'] = {'obj': portal,
276
                          'url': portal.absolute_url()}
277
        data['laboratory'] = self._lab_data()
278
279
        return data
280
281
    def _createdby_data(self, ws):
282
        """ Returns a dict that represents the user who created the ws
283
            Keys: username, fullmame, email
284
        """
285
        username = ws.getOwner().getUserName()
286
        return {'username': username,
287
                'fullname': to_utf8(self.user_fullname(username)),
288
                'email': to_utf8(self.user_email(username))}
289
290
    def _analyst_data(self, ws):
291
        """ Returns a dict that represent the analyst assigned to the
292
            worksheet.
293
            Keys: username, fullname, email
294
        """
295
        username = ws.getAnalyst()
296
        return {'username': username,
297
                'fullname': to_utf8(self.user_fullname(username)),
298
                'email': to_utf8(self.user_email(username))}
299
300
    def _printedby_data(self, ws):
301
        """ Returns a dict that represents the user who prints the ws
302
            Keys: username, fullname, email
303
        """
304
        data = {}
305
        member = self.context.portal_membership.getAuthenticatedMember()
306
        if member:
307
            username = member.getUserName()
308
            data['username'] = username
309
            data['fullname'] = to_utf8(self.user_fullname(username))
310
            data['email'] = to_utf8(self.user_email(username))
311
312
            c = [x for x in self.senaite_catalog_setup(portal_type='LabContact')
313
                 if x.getObject().getUsername() == username]
314
            if c:
315
                sf = c[0].getObject().getSignature()
316
                if sf:
317
                    data['signature'] = sf.absolute_url() + "/Signature"
318
319
        return data
320
321
    def _analyses_data(self, ws):
322
        """ Returns a list of dicts. Each dict represents an analysis
323
            assigned to the worksheet
324
        """
325
        ans = ws.getAnalyses()
326
        layout = ws.getLayout()
327
        pos_count = 0
328
        prev_pos = 0
329
        ars = {}
330
331
        # mapping of analysis UID -> position in layout
332
        uid_to_pos_mapping = dict(
333
            map(lambda row: (row["analysis_uid"], row["position"]), layout))
334
335
        for an in ans:
336
            # Build the analysis-specific dict
337
            if an.portal_type == "DuplicateAnalysis":
338
                andict = self._analysis_data(an.getAnalysis())
339
                andict['id'] = an.getReferenceAnalysesGroupID()
340
                andict['obj'] = an
341
                andict['type'] = "DuplicateAnalysis"
342
                andict['reftype'] = 'd'
343
            else:
344
                andict = self._analysis_data(an)
345
346
            andict["created"] = api.get_creation_date(an)
347
348
            # Analysis position
349
            pos = uid_to_pos_mapping.get(an.UID(), 0)
350
351
            # compensate for possible bad data (dbw#104)
352
            if isinstance(pos, (list, tuple)) and pos[0] == 'new':
353
                pos = prev_pos
354
355
            pos = int(pos)
356
            prev_pos = pos
357
358
            # This will allow to sort automatically all the analyses,
359
            # also if they have the same initial position.
360
            andict['tmp_position'] = (pos * 100) + pos_count
361
            andict['position'] = pos
362
            pos_count += 1
363
364
            # Look for the analysis request, client and sample info and
365
            # group the analyses per Analysis Request
366
            reqid = andict['request_id']
367
            if an.portal_type in ("ReferenceAnalysis", "DuplicateAnalysis"):
368
                reqid = an.getReferenceAnalysesGroupID()
369
370
            if reqid not in ars:
371
                arobj = an.aq_parent
372
                if an.portal_type == "DuplicateAnalysis":
373
                    arobj = an.getAnalysis().aq_parent
374
375
                ar = self._ar_data(arobj)
376
                ar['client'] = self._client_data(arobj.aq_parent)
377
                ar["sample"] = dict()
378
                if IReferenceSample.providedBy(arobj):
379
                    ar['sample'] = self._sample_data(an.getSample())
380
                else:
381
                    ar['sample'] = self._sample_data(an.getRequest())
382
                ar['analyses'] = []
383
                ar['tmp_position'] = andict['tmp_position']
384
                ar['position'] = andict['position']
385
                if an.portal_type in ("ReferenceAnalysis", "DuplicateAnalysis"):
386
                    ar['id'] = an.getReferenceAnalysesGroupID()
387
                    ar['url'] = an.absolute_url()
388
            else:
389
                ar = ars[reqid]
390
                if (andict['tmp_position'] < ar['tmp_position']):
391
                    ar['tmp_position'] = andict['tmp_position']
392
                    ar['position'] = andict['position']
393
394
            # Sort analyses by position
395
            ans = ar['analyses']
396
            ans.append(andict)
397
            ans.sort(lambda x, y: cmp(x.get('tmp_position'), y.get('tmp_position')))
398
            ar['analyses'] = ans
399
            ars[reqid] = ar
400
401
        ars = ars.values()
402
403
        # Sort analysis requests by position
404
        ars.sort(lambda x, y: cmp(x.get('tmp_position'), y.get('tmp_position')))
405
        return ars
406
407
    def _analysis_data(self, analysis):
408
        """ Returns a dict that represents the analysis
409
        """
410
        client = get_client(analysis)
411
        if client:
412
            decimalmark = client.getDecimalMark()
413
        else:
414
            # no client found – this happens for QC analyses
415
            decimalmark = self.get_default_decimal_mark()
416
        keyword = analysis.getKeyword()
417
        andict = {
418
            'obj': analysis,
419
            'id': analysis.id,
420
            'title': analysis.Title(),
421
            'keyword': keyword,
422
            'scientific_name': analysis.getScientificName(),
423
            'accredited': analysis.getAccredited(),
424
            'point_of_capture': to_utf8(POINTS_OF_CAPTURE.getValue(analysis.getPointOfCapture())),
425
            'category': to_utf8(analysis.getCategoryTitle()),
426
            'result': analysis.getResult(),
427
            'unit': to_utf8(analysis.getUnit()),
428
            'formatted_unit': format_supsub(to_utf8(analysis.getUnit())),
429
            'capture_date': analysis.getResultCaptureDate(),
430
            'request_id': analysis.aq_parent.getId(),
431
            'formatted_result': '',
432
            'uncertainty': analysis.getUncertainty(),
433
            'formatted_uncertainty': '',
434
            'retested': analysis.isRetest(),
435
            'remarks': to_utf8(analysis.getRemarks()),
436
            'outofrange': False,
437
            'type': analysis.portal_type,
438
            'reftype': analysis.getReferenceType() if hasattr(
439
                analysis, 'getReferenceType') else None,
440
            'worksheet': None,
441
            'specs': {},
442
            'formatted_specs': '',
443
            'review_state': api.get_workflow_status_of(analysis),
444
        }
445
446
        andict['refsample'] = analysis.getSample().id \
447
            if IReferenceAnalysis.providedBy(analysis) \
448
            else analysis.getRequestID()
449
450
        specs = analysis.getResultsRange()
451
        andict['specs'] = specs
452
        scinot = self.context.bika_setup.getScientificNotationReport()
453
        andict['formatted_result'] = analysis.getFormattedResult(specs=specs, sciformat=int(scinot), decimalmark=decimalmark)
454
455
        fs = ''
456
        if specs.get('min', None) and specs.get('max', None):
457
            fs = '%s - %s' % (specs['min'], specs['max'])
458
        elif specs.get('min', None):
459
            fs = '> %s' % specs['min']
460
        elif specs.get('max', None):
461
            fs = '< %s' % specs['max']
462
        andict['formatted_specs'] = formatDecimalMark(fs, decimalmark)
463
        andict['formatted_uncertainty'] = format_uncertainty(
464
            analysis, decimalmark=decimalmark, sciformat=int(scinot))
465
466
        # Out of range?
467
        andict['outofrange'] = is_out_of_range(analysis)[0]
468
        return andict
469
470
    def _sample_data(self, sample):
471
        """ Returns a dict that represents the sample
472
            Keys: obj, id, url, client_sampleid, date_sampled,
473
                  sampling_date, sampler, date_received, composite,
474
                  date_expired, date_disposal, date_disposed, remarks
475
        """
476
        data = {}
477
        if sample:
478
            data = {'obj': sample,
479
                    'id': sample.id,
480
                    'url': sample.absolute_url(),
481
                    'date_sampled': self.ulocalized_time(
482
                        sample.getDateSampled(), long_format=True),
483
                    'date_received': self.ulocalized_time(
484
                        sample.getDateReceived(), long_format=0),
485
                    }
486
487
            if sample.portal_type == "ReferenceSample":
488
                data['sample_type'] = None
489
                data['sample_point'] = None
490
            else:
491
                data['sample_type'] = self._sample_type(sample)
492
                data['sample_point'] = self._sample_point(sample)
493
        return data
494
495
    def _sample_type(self, sample=None):
496
        """ Returns a dict that represents the sample type assigned to
497
            the sample specified
498
            Keys: obj, id, title, url
499
        """
500
        data = {}
501
        sampletype = sample.getSampleType() if sample else None
502
        if sampletype:
503
            data = {'obj': sampletype,
504
                    'id': sampletype.id,
505
                    'title': sampletype.Title(),
506
                    'url': sampletype.absolute_url()}
507
        return data
508
509
    def _sample_point(self, sample=None):
510
        """ Returns a dict that represents the sample point assigned to
511
            the sample specified
512
            Keys: obj, id, title, url
513
        """
514
        samplepoint = sample.getSamplePoint() if sample else None
515
        data = {}
516
        if samplepoint:
517
            data = {'obj': samplepoint,
518
                    'id': samplepoint.id,
519
                    'title': samplepoint.Title(),
520
                    'url': samplepoint.absolute_url()}
521
        return data
522
523
    def _ar_data(self, ar):
524
        """ Returns a dict that represents the analysis request
525
        """
526
        if not ar:
527
            return {}
528
529
        if ar.portal_type == "AnalysisRequest":
530
            return {'obj': ar,
531
                    'id': ar.getId(),
532
                    'date_received': self.ulocalized_time(
533
                        ar.getDateReceived(), long_format=0),
534
                    'date_sampled': self.ulocalized_time(
535
                        ar.getDateSampled(), long_format=True),
536
                    'url': ar.absolute_url(), }
537
        elif ar.portal_type == "ReferenceSample":
538
            return {'obj': ar,
539
                    'id': ar.id,
540
                    'date_received': self.ulocalized_time(
541
                        ar.getDateReceived(), long_format=0),
542
                    'date_sampled': self.ulocalized_time(
543
                        ar.getDateSampled(), long_format=True),
544
                    'url': ar.absolute_url(), }
545
        else:
546
            return {'obj': ar,
547
                    'id': ar.id,
548
                    'date_received': "",
549
                    'date_sampled': "",
550
                    'url': ar.absolute_url(), }
551
552
    def _client_data(self, client):
553
        """ Returns a dict that represents the client specified
554
            Keys: obj, id, url, name
555
        """
556
        data = {}
557
        if client:
558
            data['obj'] = client
559
            data['id'] = client.id
560
            data['url'] = client.absolute_url()
561
            data['name'] = to_utf8(client.getName())
562
        return data
563
564
    def _flush_pdf(self):
565
        """ Generates a PDF using the current layout as the template and
566
            returns the chunk of bytes.
567
        """
568
        return ""
569