Completed
Branch master (9edffc)
by Jordi
04:36
created

bika.lims.browser.worksheet.views.printview   F

Complexity

Total Complexity 77

Size/Duplication

Total Lines 544
Duplicated Lines 14.15 %

Importance

Changes 0
Metric Value
wmc 77
eloc 358
dl 77
loc 544
rs 2.24
c 0
b 0
f 0

23 Methods

Rating   Name   Duplication   Size   Complexity  
A PrintView._analyst_data() 0 9 1
A PrintView.getCSS() 21 21 4
A PrintView.getWSTemplates() 20 20 4
A PrintView.__init__() 0 3 1
A PrintView.splitList() 0 13 3
A PrintView.renderWSTemplate() 21 21 4
A PrintView.getWorksheet() 0 8 2
A PrintView._printedby_data() 0 20 4
A PrintView._lab_data() 0 27 2
A PrintView._createdby_data() 0 8 1
A PrintView.__call__() 15 33 5
A PrintView.get_analysis_data_by_title() 0 11 3
A PrintView.getWorksheets() 0 4 1
A PrintView.getNumColumns() 0 4 1
A PrintView._ws_data() 0 41 4
B PrintView._analysis_data() 0 56 7
F PrintView._analyses_data() 0 83 14
A PrintView._sample_data() 0 24 3
A PrintView._client_data() 0 11 2
A PrintView._sample_point() 0 13 3
A PrintView._ar_data() 0 28 4
A PrintView._flush_pdf() 0 5 1
A PrintView._sample_type() 0 13 3

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