[IMP] Multiple improvements: eval_param is set on the controller, the subprocess to get the version of wkhtmltopdf is only open at OpenERP start, better exceptions handling (try to avoid exceptions shallowing) and the rpc call from the webclient to know the version of wkhtmltopdf is only done once

bzr revid: sle@openerp.com-20140321164716-uksuu6hsjj7q3698
This commit is contained in:
Simon Lejeune 2014-03-21 17:47:16 +01:00
parent 5ac5805da0
commit 8581b1847f
3 changed files with 91 additions and 107 deletions

View File

@ -35,11 +35,13 @@ class ReportController(Controller):
@route('/report/<reportname>/<docids>', type='http', auth='user', website=True, multilang=True) @route('/report/<reportname>/<docids>', type='http', auth='user', website=True, multilang=True)
def report_html(self, reportname, docids): def report_html(self, reportname, docids):
cr, uid, context = request.cr, request.uid, request.context cr, uid, context = request.cr, request.uid, request.context
docids = self._eval_params(docids)
return request.registry['report'].get_html(cr, uid, docids, reportname, context=context) return request.registry['report'].get_html(cr, uid, docids, reportname, context=context)
@route('/report/pdf/report/<reportname>/<docids>', type='http', auth="user", website=True) @route('/report/pdf/report/<reportname>/<docids>', type='http', auth="user", website=True)
def report_pdf(self, reportname, docids): def report_pdf(self, reportname, docids):
cr, uid, context = request.cr, request.uid, request.context cr, uid, context = request.cr, request.uid, request.context
docids = self._eval_params(docids)
pdf = request.registry['report'].get_pdf(cr, uid, docids, reportname, context=context) pdf = request.registry['report'].get_pdf(cr, uid, docids, reportname, context=context)
pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf))] pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf))]
return request.make_response(pdf, headers=pdfhttpheaders) return request.make_response(pdf, headers=pdfhttpheaders)
@ -51,14 +53,14 @@ class ReportController(Controller):
def report_html_particular(self, reportname, **data): def report_html_particular(self, reportname, **data):
cr, uid, context = request.cr, request.uid, request.context cr, uid, context = request.cr, request.uid, request.context
report_obj = request.registry['report'] report_obj = request.registry['report']
data = report_obj.eval_params(data) # Sanitizing data = self._eval_params(data) # Sanitizing
return report_obj.get_html(cr, uid, [], reportname, data=data, context=context) return report_obj.get_html(cr, uid, [], reportname, data=data, context=context)
@route('/report/pdf/report/<reportname>', type='http', auth='user', website=True, multilang=True) @route('/report/pdf/report/<reportname>', type='http', auth='user', website=True, multilang=True)
def report_pdf_particular(self, reportname, **data): def report_pdf_particular(self, reportname, **data):
cr, uid, context = request.cr, request.uid, request.context cr, uid, context = request.cr, request.uid, request.context
report_obj = request.registry['report'] report_obj = request.registry['report']
data = report_obj.eval_params(data) # Sanitizing data = self._eval_params(data) # Sanitizing
pdf = report_obj.get_pdf(cr, uid, [], reportname, data=data, context=context) pdf = report_obj.get_pdf(cr, uid, [], reportname, data=data, context=context)
pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf))] pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf))]
return request.make_response(pdf, headers=pdfhttpheaders) return request.make_response(pdf, headers=pdfhttpheaders)
@ -121,4 +123,29 @@ class ReportController(Controller):
@route(['/report/check_wkhtmltopdf'], type='json', auth="user") @route(['/report/check_wkhtmltopdf'], type='json', auth="user")
def check_wkhtmltopdf(self): def check_wkhtmltopdf(self):
return request.registry['report'].check_wkhtmltopdf() return request.registry['report']._check_wkhtmltopdf()
def _eval_params(self, param):
"""Parse a dict generated by the webclient (javascript) into a python dict.
"""
if isinstance(param, dict):
for key, value in param.iteritems():
if value.lower() == 'false':
param[key] = False
elif value.lower() == 'true':
param[key] = True
elif ',' in value:
param[key] = [int(i) for i in value.split(',')]
else:
try:
param[key] = int(value)
except (ValueError, TypeError):
pass
else:
if isinstance(param, (str, unicode)):
param = [int(i) for i in param.split(',')]
if isinstance(param, list):
param = list(set(param))
if isinstance(param, int):
param = [param]
return param

View File

@ -35,6 +35,7 @@ import lxml.html
import cStringIO import cStringIO
import subprocess import subprocess
from datetime import datetime from datetime import datetime
from functools import partial
from distutils.version import LooseVersion from distutils.version import LooseVersion
try: try:
from pyPdf import PdfFileWriter, PdfFileReader from pyPdf import PdfFileWriter, PdfFileReader
@ -45,6 +46,24 @@ except ImportError:
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
"""Check the presence of wkhtmltopdf and return its version."""
wkhtmltopdf_state = 'install'
try:
process = subprocess.Popen(
['wkhtmltopdf', '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
except OSError:
_logger.error('You need wkhtmltopdf to print a pdf version of the reports.')
else:
out, err = process.communicate()
version = out.splitlines()[1].strip()
version = version.split(' ')[1]
if LooseVersion(version) < LooseVersion('0.12.0'):
_logger.warning('Upgrade wkhtmltopdf to (at least) 0.12.0')
wkhtmltopdf_state = 'upgrade'
wkhtmltopdf_state = 'ok'
class Report(osv.Model): class Report(osv.Model):
_name = "report" _name = "report"
_description = "Report" _description = "Report"
@ -55,7 +74,7 @@ class Report(osv.Model):
# Extension of ir_ui_view.render with arguments frequently used in reports # Extension of ir_ui_view.render with arguments frequently used in reports
#-------------------------------------------------------------------------- #--------------------------------------------------------------------------
def get_digits(self, cr, uid, obj=None, f=None, dp=None): def _get_digits(self, cr, uid, obj=None, f=None, dp=None):
d = DEFAULT_DIGITS = 2 d = DEFAULT_DIGITS = 2
if dp: if dp:
decimal_precision_obj = self.pool['decimal.precision'] decimal_precision_obj = self.pool['decimal.precision']
@ -101,9 +120,9 @@ class Report(osv.Model):
if digits is None: if digits is None:
if dp: if dp:
digits = self.get_digits(cr, uid, dp=dp) digits = self._get_digits(cr, uid, dp=dp)
else: else:
digits = self.get_digits(cr, uid, value) digits = self._get_digits(cr, uid, value)
if isinstance(value, (str, unicode)) and not value: if isinstance(value, (str, unicode)) and not value:
return '' return ''
@ -192,8 +211,8 @@ class Report(osv.Model):
values.update({ values.update({
'time': time, 'time': time,
'formatLang': lambda *args, **kwargs: self.formatLang(*args, cr=cr, uid=uid, **kwargs), 'formatLang': partial(self.formatLang, cr=cr, uid=uid),
'get_digits': self.get_digits, 'get_digits': self._get_digits,
'render_doc': render_doc, 'render_doc': render_doc,
'editable': True, # Will active inherit_branding 'editable': True, # Will active inherit_branding
'res_company': self.pool['res.users'].browse(cr, uid, uid).company_id 'res_company': self.pool['res.users'].browse(cr, uid, uid).company_id
@ -208,35 +227,22 @@ class Report(osv.Model):
def get_html(self, cr, uid, ids, report_name, data=None, context=None): def get_html(self, cr, uid, ids, report_name, data=None, context=None):
"""This method generates and returns html version of a report. """This method generates and returns html version of a report.
""" """
if context is None:
context = {}
if isinstance(ids, (str, unicode)):
ids = [int(i) for i in ids.split(',')]
if isinstance(ids, list):
ids = list(set(ids))
if isinstance(ids, int):
ids = [ids]
# If the report is using a custom model to render its html, we must use it. # If the report is using a custom model to render its html, we must use it.
# Otherwise, fallback on the generic html rendering. # Otherwise, fallback on the generic html rendering.
try: try:
report_model_name = 'report.%s' % report_name report_model_name = 'report.%s' % report_name
particularreport_obj = self.pool[report_model_name] particularreport_obj = self.pool[report_model_name]
return particularreport_obj.render_html(cr, uid, ids, data=data, context=context) return particularreport_obj.render_html(cr, uid, ids, data={'form': data}, context=context)
except: except KeyError:
pass report = self._get_report_from_name(cr, uid, report_name)
report_obj = self.pool[report.model]
report = self._get_report_from_name(cr, uid, report_name) docs = report_obj.browse(cr, uid, ids, context=context)
report_obj = self.pool[report.model] docargs = {
docs = report_obj.browse(cr, uid, ids, context=context) 'doc_ids': ids,
'doc_model': report.model,
docargs = { 'docs': docs,
'doc_ids': ids, }
'doc_model': report.model, return self.render(cr, uid, [], report.report_name, docargs, context=context)
'docs': docs,
}
return self.render(cr, uid, [], report.report_name, docargs, context=context)
def get_pdf(self, cr, uid, ids, report_name, html=None, data=None, context=None): def get_pdf(self, cr, uid, ids, report_name, html=None, data=None, context=None):
"""This method generates and returns pdf version of a report. """This method generates and returns pdf version of a report.
@ -244,13 +250,6 @@ class Report(osv.Model):
if context is None: if context is None:
context = {} context = {}
if isinstance(ids, (str, unicode)):
ids = [int(i) for i in ids.split(',')]
if isinstance(ids, list):
ids = list(set(ids))
if isinstance(ids, int):
ids = [ids]
if html is None: if html is None:
html = self.get_html(cr, uid, ids, report_name, data=data, context=context) html = self.get_html(cr, uid, ids, report_name, data=data, context=context)
@ -260,7 +259,7 @@ class Report(osv.Model):
report = self._get_report_from_name(cr, uid, report_name) report = self._get_report_from_name(cr, uid, report_name)
# Check attachment_use field. If set to true and an existing pdf is already saved, load # Check attachment_use field. If set to true and an existing pdf is already saved, load
# this one now. If not, mark save it. # this one now. Else, mark save it.
save_in_attachment = {} save_in_attachment = {}
if report.attachment_use is True: if report.attachment_use is True:
@ -307,7 +306,7 @@ class Report(osv.Model):
base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url') base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url')
minimalhtml = """ minimalhtml = """
<base href="{3}"> <base href="{base_url}">
<!DOCTYPE html> <!DOCTYPE html>
<html style="height: 0;"> <html style="height: 0;">
<head> <head>
@ -315,11 +314,11 @@ class Report(osv.Model):
<link href="/web/static/lib/bootstrap/css/bootstrap.css" rel="stylesheet"/> <link href="/web/static/lib/bootstrap/css/bootstrap.css" rel="stylesheet"/>
<link href="/website/static/src/css/website.css" rel="stylesheet"/> <link href="/website/static/src/css/website.css" rel="stylesheet"/>
<link href="/web/static/lib/fontawesome/css/font-awesome.css" rel="stylesheet"/> <link href="/web/static/lib/fontawesome/css/font-awesome.css" rel="stylesheet"/>
<style type='text/css'>{0}</style> <style type='text/css'>{css}</style>
{1} {subst}
</head> </head>
<body class="container" onload='subst()'> <body class="container" onload='subst()'>
{2} {body}
</body> </body>
</html>""" </html>"""
@ -333,12 +332,12 @@ class Report(osv.Model):
for node in root.xpath("//div[@class='header']"): for node in root.xpath("//div[@class='header']"):
body = lxml.html.tostring(node) body = lxml.html.tostring(node)
header = minimalhtml.format(css, subst, body, base_url) header = minimalhtml.format(css=css, subst=subst, body=body, base_url=base_url)
headerhtml.append(header) headerhtml.append(header)
for node in root.xpath("//div[@class='footer']"): for node in root.xpath("//div[@class='footer']"):
body = lxml.html.tostring(node) body = lxml.html.tostring(node)
footer = minimalhtml.format(css, subst, body, base_url) footer = minimalhtml.format(css=css, subst=subst, body=body, base_url=base_url)
footerhtml.append(footer) footerhtml.append(footer)
for node in root.xpath("//div[@class='page']"): for node in root.xpath("//div[@class='page']"):
@ -346,16 +345,16 @@ class Report(osv.Model):
# must set a relation between report ids and report's content. We use the QWeb # must set a relation between report ids and report's content. We use the QWeb
# branding in order to do so: searching after a node having a data-oe-model # branding in order to do so: searching after a node having a data-oe-model
# attribute with the value of the current report model and read its oe-id attribute # attribute with the value of the current report model and read its oe-id attribute
oemodelnode = node.find(".//*[@data-oe-model='" + report.model + "']") oemodelnode = node.find(".//*[@data-oe-model='%s']" % report.model)
if oemodelnode is not None: if oemodelnode is not None:
reportid = oemodelnode.get('data-oe-id', False) reportid = oemodelnode.get('data-oe-id')
if reportid is not False: if reportid:
reportid = int(reportid) reportid = int(reportid)
else: else:
reportid = False reportid = False
body = lxml.html.tostring(node) body = lxml.html.tostring(node)
reportcontent = minimalhtml.format(css, '', body, base_url) reportcontent = minimalhtml.format(css=css, subst='', body=body, base_url=base_url)
contenthtml.append(tuple([reportid, reportcontent])) contenthtml.append(tuple([reportid, reportcontent]))
except lxml.etree.XMLSyntaxError: except lxml.etree.XMLSyntaxError:
@ -414,27 +413,8 @@ class Report(osv.Model):
# Report generation helpers # Report generation helpers
#-------------------------------------------------------------------------- #--------------------------------------------------------------------------
def check_wkhtmltopdf(self): def _check_wkhtmltopdf(self):
"""Check the presence of wkhtmltopdf and return its version. If wkhtmltopdf return wkhtmltopdf_state
cannot be found, return False.
"""
try:
process = subprocess.Popen(['wkhtmltopdf', '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = process.communicate()
if err:
raise
version = out.splitlines()[1].strip()
version = version.split(' ')[1]
if LooseVersion(version) < LooseVersion('0.12.0'):
_logger.warning('Upgrade WKHTMLTOPDF to (at least) 0.12.0')
return 'upgrade'
return True
except:
_logger.error('You need WKHTMLTOPDF to print a pdf version of this report.')
return False
def _generate_wkhtml_pdf(self, cr, uid, headers, footers, bodies, landscape, paperformat, spec_paperformat_args=None, save_in_attachment=None): def _generate_wkhtml_pdf(self, cr, uid, headers, footers, bodies, landscape, paperformat, spec_paperformat_args=None, save_in_attachment=None):
"""Execute wkhtmltopdf as a subprocess in order to convert html given in input into a pdf """Execute wkhtmltopdf as a subprocess in order to convert html given in input into a pdf
@ -450,14 +430,14 @@ class Report(osv.Model):
:returns: Content of the pdf as a string :returns: Content of the pdf as a string
""" """
command = ['wkhtmltopdf'] command = ['wkhtmltopdf']
command_args = []
tmp_dir = tempfile.gettempdir() tmp_dir = tempfile.gettempdir()
command_args = []
# Passing the cookie to wkhtmltopdf in order to resolve URL. # Passing the cookie to wkhtmltopdf in order to resolve URL.
try: try:
from openerp.addons.web.http import request from openerp.addons.web.http import request
command_args.extend(['--cookie', 'session_id', request.httprequest.cookies['session_id']]) command_args.extend(['--cookie', 'session_id', request.session.sid])
except: except AttributeError:
pass pass
# Display arguments # Display arguments
@ -510,8 +490,8 @@ class Report(osv.Model):
content_file.flush() content_file.flush()
try: try:
# If the server is running with only one worker, increase it to two to be able # If the server is running with only one worker, ask to create a secund to be able
# to serve the http request from wkhtmltopdf. # to serve the http request of wkhtmltopdf subprocess.
if config['workers'] == 1: if config['workers'] == 1:
ppid = psutil.Process(os.getpid()).ppid ppid = psutil.Process(os.getpid()).ppid
os.kill(ppid, signal.SIGTTIN) os.kill(ppid, signal.SIGTTIN)
@ -629,26 +609,3 @@ class Report(osv.Model):
content = merged.read() content = merged.read()
merged.close() merged.close()
return content return content
def eval_params(self, dict_param):
"""Parse a dict generated by the webclient (javascript) into a python dict.
"""
for key, value in dict_param.iteritems():
if value.lower() == 'false':
dict_param[key] = False
elif value.lower() == 'true':
dict_param[key] = True
elif ',' in value:
dict_param[key] = [int(i) for i in value.split(',')]
elif '%2C' in value:
dict_param[key] = [int(i) for i in value.split('%2C')]
else:
try:
i = int(value)
dict_param[key] = i
except (ValueError, TypeError):
pass
data = {}
data['form'] = dict_param
return data

View File

@ -1,4 +1,5 @@
openerp.report = function(instance) { openerp.report = function(instance) {
var wkhtmltopdf_state;
instance.web.ActionManager = instance.web.ActionManager.extend({ instance.web.ActionManager = instance.web.ActionManager.extend({
ir_actions_report_xml: function(action, options) { ir_actions_report_xml: function(action, options) {
@ -12,7 +13,7 @@ openerp.report = function(instance) {
// QWeb reports // QWeb reports
if ('report_type' in action && (action.report_type == 'qweb-html' || action.report_type == 'qweb-pdf' || action.report_type == 'controller')) { if ('report_type' in action && (action.report_type == 'qweb-html' || action.report_type == 'qweb-pdf' || action.report_type == 'controller')) {
var report_url = '' var report_url = '';
switch (action.report_type) { switch (action.report_type) {
case 'qweb-html': case 'qweb-html':
report_url = '/report/' + action.report_name; report_url = '/report/' + action.report_name;
@ -55,20 +56,19 @@ openerp.report = function(instance) {
} else { } else {
// Trigger the download of the pdf report // Trigger the download of the pdf report
var c = openerp.webclient.crashmanager; var c = openerp.webclient.crashmanager;
var response = new Array() var response = new Array();
response[0] = report_url response[0] = report_url;
response[1] = action.report_type response[1] = action.report_type;
openerp.session.rpc('/report/check_wkhtmltopdf').then(function (presence) { (wkhtmltopdf_state = wkhtmltopdf_state || openerp.session.rpc('/report/check_wkhtmltopdf')).then(function (presence) {
// Fallback of qweb-pdf if wkhtmltopdf is not installed // Fallback of qweb-pdf if wkhtmltopdf is not installed
if (!presence && action.report_type == 'qweb-pdf') { if (presence == 'install' && action.report_type == 'qweb-pdf') {
self.do_notify(_t('Report'), _t('Unable to find Wkhtmltopdf on this \ self.do_notify(_t('Report'), _t('Unable to find Wkhtmltopdf on this \
system. The report will be shown in html.<br><br><a href="http://wkhtmltopdf.org/" target="_blank">\ system. The report will be shown in html.<br><br><a href="http://wkhtmltopdf.org/" target="_blank">\
wkhtmltopdf.org</a>'), true); wkhtmltopdf.org</a>'), true);
window.open(report_url.substring(12), '_blank', 'height=768,width=1024'); window.open(report_url.substring(12), '_blank', 'height=768,width=1024');
instance.web.unblockUI(); instance.web.unblockUI();
} } else {
else {
if (presence == 'upgrade') { if (presence == 'upgrade') {
self.do_notify(_t('Report'), _t('You should upgrade your version of\ self.do_notify(_t('Report'), _t('You should upgrade your version of\
Wkhtmltopdf to at least 0.12.0 in order to get a correct display of headers and footers as well as\ Wkhtmltopdf to at least 0.12.0 in order to get a correct display of headers and footers as well as\