Passed
Push — 2.x ( a73005...5cd7ad )
by Ramon
11:13
created

senaite.core.browser.samples.rejection.report   A

Complexity

Total Complexity 22

Size/Duplication

Total Lines 218
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 22
eloc 107
dl 0
loc 218
rs 10
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A RejectionReport.get_rejected_by() 0 17 1
A RejectionReport.rejection_date() 0 5 1
A RejectionReport.laboratory() 0 6 1
A RejectionReport.get_contact_base_properties() 0 12 1
A RejectionReport.to_pdf() 0 12 1
A RejectionReport.layout_css() 0 5 1
A RejectionReport.__call__() 0 13 2
A RejectionReport.get_contact_properties() 0 25 4
A RejectionReport.get_rejection_reasons() 0 14 3
A RejectionReport.long_date() 0 4 1
A RejectionReport.available_reasons() 0 11 2
A RejectionReport.get_authorized_by() 0 23 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-2024 by it's authors.
19
# Some rights reserved, see README and LICENSE.
20
21
from datetime import datetime
22
23
from bika.lims import api
24
from Products.Five.browser import BrowserView
25
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
26
from senaite.core.api import dtime
27
from senaite.impress.interfaces import IPublisher
28
from zope.component import getUtility
29
30
DIN_A4_PORTRAIT = """
31
/** DIN A4 portrait **/
32
@page {
33
  size: 210.0mm 297.0mm;
34
  margin: 20.0mm;
35
}
36
@media print {
37
  .report {
38
    width: 170.0mm;
39
    height: 257.0mm;
40
  }
41
}
42
@media screen {
43
  .report {
44
    width: 210.0mm;
45
    height: 297.0mm;
46
    padding: 20.0mm !important;
47
  }
48
}
49
"""
50
51
52
class RejectionReport(BrowserView):
53
    """
54
    View that renders the template to be used for the generation of the pdf
55
    representing the rejection report
56
    """
57
    template = ViewPageTemplateFile("templates/rejection_report.pt")
58
59
    def __call__(self):
60
        if not self.request.get("pdf"):
61
            return self.template()
62
63
        pdf = self.to_pdf()
64
        filename = "%s-rejected" % api.get_id(self.context)
65
        self.request.response.setHeader(
66
            "Content-Disposition", "attachment; filename=%s.pdf" % filename)
67
        self.request.response.setHeader("Content-Type", "application/pdf")
68
        self.request.response.setHeader("Content-Length", len(pdf))
69
        self.request.response.setHeader("Cache-Control", "no-store")
70
        self.request.response.setHeader("Pragma", "no-cache")
71
        self.request.response.write(pdf)
72
73
    def to_pdf(self):
74
        """Returns a pdf of this report
75
        """
76
        # generate the html from the template
77
        html = self.template()
78
        # convert to unicode
79
        html = api.safe_unicode(html)
80
        # get the impress publisher with default css
81
        publisher = getUtility(IPublisher)
82
        publisher.add_inline_css(self.layout_css)
83
        # generate the pdf
84
        return publisher.write_pdf(html)
85
86
    @property
87
    def laboratory(self):
88
        """Returns the laboratory object
89
        """
90
        setup = api.get_setup()
91
        return setup.laboratory
92
93
    @property
94
    def available_reasons(self):
95
        """Returns available rejection reasons
96
        """
97
        setup = api.get_setup()
98
        reasons = setup.getRejectionReasons()
99
        # XXX getRejectionReasons returns a list with a single dict
100
        reasons = reasons[0] if reasons else {}
101
        # XXX Exclude 'checkbox' (used to toggle reasons in setup)
102
        reasons = [reasons[key] for key in reasons.keys() if key != 'checkbox']
103
        return sorted(reasons)
104
105
    @property
106
    def layout_css(self):
107
        """Returns the CSS for the page and content layout
108
        """
109
        return DIN_A4_PORTRAIT
110
111
    @property
112
    def rejection_date(self):
113
        """Returns the current date
114
        """
115
        return datetime.now()
116
117
    def long_date(self, date):
118
        """Returns the date localized in long format
119
        """
120
        return dtime.to_localized_time(date, long_format=True)
121
122
    def get_contact_base_properties(self):
123
        """Returns a dict with the basic information about a contact
124
        """
125
        return {
126
            "fullname": "",
127
            "salutation": "",
128
            "signature": "",
129
            "job_title": "",
130
            "phone": "",
131
            "department": "",
132
            "email": "",
133
            "uid": "",
134
        }
135
136
    def get_contact_properties(self, contact):
137
        """Returns a dictionary with information about the contact
138
        """
139
        if not contact:
140
            return {}
141
142
        # fill properties with contact additional info
143
        contact_url = api.get_url(contact)
144
        signature = contact.getSignature()
145
        signature = "{}/Signature".format(contact_url) if signature else ""
146
        department = contact.getDefaultDepartment()
147
        department = api.get_title(department) if department else ""
148
149
        properties = self.get_contact_base_properties()
150
        properties.update({
151
            "fullname": api.to_utf8(contact.getFullname()),
152
            "salutation": api.to_utf8(contact.getSalutation()),
153
            "signature": signature,
154
            "job_title": api.to_utf8(contact.getJobTitle()),
155
            "phone": contact.getBusinessPhone(),
156
            "department": api.to_utf8(department),
157
            "email": contact.getEmailAddress(),
158
            "uid": api.get_uid(contact),
159
        })
160
        return properties
161
162
    def get_rejected_by(self):
163
        """Returns a dict with information about the rejecter, giving priority
164
        to the contact over the user
165
        """
166
        properties = self.get_contact_base_properties()
167
168
        # overwrite with user info
169
        user = api.get_current_user()
170
        user_properties = api.get_user_properties(user)
171
        properties.update(user_properties)
172
173
        # overwrite with contact info
174
        contact = api.get_user_contact(user)
175
        contact_properties = self.get_contact_properties(contact)
176
        properties.update(contact_properties)
177
178
        return properties
179
180
    def get_authorized_by(self):
181
        """Returns a list of dicts with the information about the laboratory
182
        contacts that are in charge of this samples as responsibles of the
183
        departments from its tests
184
        """
185
        rejecter = self.get_rejected_by()
186
        rejecter_uid = rejecter.get("uid")
187
        authorized_by = {}
188
        for department in self.context.getDepartments():
189
            manager = department.getManager()
190
            properties = self.get_contact_properties(manager)
191
            if not properties:
192
                continue
193
194
            # skip rejecter
195
            contact_uid = api.get_uid(manager)
196
            if rejecter_uid == contact_uid:
197
                continue
198
199
            # skip duplicates
200
            authorized_by[contact_uid] = properties
201
202
        return list(authorized_by.values())
203
204
    def get_rejection_reasons(self, keyword):
205
        """
206
        Returns a list with the rejection reasons as strings
207
        :return: list of rejection reasons as strings or an empty list
208
        """
209
        # selected reasons
210
        reasons = self.context.getRejectionReasons()
211
        # XXX getRejectionReasons returns a list of one dict?
212
        reasons = reasons[0] if reasons else {}
213
        reasons = reasons.get(keyword) or []
214
        # XXX 'other' keyword returns a single item instead of a list
215
        if not isinstance(reasons, (list, tuple)):
216
            reasons = [reasons]
217
        return reasons
218