[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)
def report_html(self, reportname, docids):
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)
@route('/report/pdf/report/<reportname>/<docids>', type='http', auth="user", website=True)
def report_pdf(self, reportname, docids):
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)
pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf))]
return request.make_response(pdf, headers=pdfhttpheaders)
@ -51,14 +53,14 @@ class ReportController(Controller):
def report_html_particular(self, reportname, **data):
cr, uid, context = request.cr, request.uid, request.context
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)
@route('/report/pdf/report/<reportname>', type='http', auth='user', website=True, multilang=True)
def report_pdf_particular(self, reportname, **data):
cr, uid, context = request.cr, request.uid, request.context
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)
pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf))]
return request.make_response(pdf, headers=pdfhttpheaders)
@ -121,4 +123,29 @@ class ReportController(Controller):
@route(['/report/check_wkhtmltopdf'], type='json', auth="user")
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 subprocess
from datetime import datetime
from functools import partial
from distutils.version import LooseVersion
try:
from pyPdf import PdfFileWriter, PdfFileReader
@ -45,6 +46,24 @@ except ImportError:
_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):
_name = "report"
_description = "Report"
@ -55,7 +74,7 @@ class Report(osv.Model):
# 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
if dp:
decimal_precision_obj = self.pool['decimal.precision']
@ -101,9 +120,9 @@ class Report(osv.Model):
if digits is None:
if dp:
digits = self.get_digits(cr, uid, dp=dp)
digits = self._get_digits(cr, uid, dp=dp)
else:
digits = self.get_digits(cr, uid, value)
digits = self._get_digits(cr, uid, value)
if isinstance(value, (str, unicode)) and not value:
return ''
@ -192,8 +211,8 @@ class Report(osv.Model):
values.update({
'time': time,
'formatLang': lambda *args, **kwargs: self.formatLang(*args, cr=cr, uid=uid, **kwargs),
'get_digits': self.get_digits,
'formatLang': partial(self.formatLang, cr=cr, uid=uid),
'get_digits': self._get_digits,
'render_doc': render_doc,
'editable': True, # Will active inherit_branding
'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):
"""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.
# Otherwise, fallback on the generic html rendering.
try:
report_model_name = 'report.%s' % report_name
particularreport_obj = self.pool[report_model_name]
return particularreport_obj.render_html(cr, uid, ids, data=data, context=context)
except:
pass
report = self._get_report_from_name(cr, uid, report_name)
report_obj = self.pool[report.model]
docs = report_obj.browse(cr, uid, ids, context=context)
docargs = {
'doc_ids': ids,
'doc_model': report.model,
'docs': docs,
}
return self.render(cr, uid, [], report.report_name, docargs, context=context)
return particularreport_obj.render_html(cr, uid, ids, data={'form': data}, context=context)
except KeyError:
report = self._get_report_from_name(cr, uid, report_name)
report_obj = self.pool[report.model]
docs = report_obj.browse(cr, uid, ids, context=context)
docargs = {
'doc_ids': ids,
'doc_model': report.model,
'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):
"""This method generates and returns pdf version of a report.
@ -244,13 +250,6 @@ class Report(osv.Model):
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 html is None:
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)
# 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 = {}
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')
minimalhtml = """
<base href="{3}">
<base href="{base_url}">
<!DOCTYPE html>
<html style="height: 0;">
<head>
@ -315,11 +314,11 @@ class Report(osv.Model):
<link href="/web/static/lib/bootstrap/css/bootstrap.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"/>
<style type='text/css'>{0}</style>
{1}
<style type='text/css'>{css}</style>
{subst}
</head>
<body class="container" onload='subst()'>
{2}
{body}
</body>
</html>"""
@ -333,12 +332,12 @@ class Report(osv.Model):
for node in root.xpath("//div[@class='header']"):
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)
for node in root.xpath("//div[@class='footer']"):
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)
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
# 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
oemodelnode = node.find(".//*[@data-oe-model='" + report.model + "']")
oemodelnode = node.find(".//*[@data-oe-model='%s']" % report.model)
if oemodelnode is not None:
reportid = oemodelnode.get('data-oe-id', False)
if reportid is not False:
reportid = oemodelnode.get('data-oe-id')
if reportid:
reportid = int(reportid)
else:
reportid = False
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]))
except lxml.etree.XMLSyntaxError:
@ -414,27 +413,8 @@ class Report(osv.Model):
# Report generation helpers
#--------------------------------------------------------------------------
def check_wkhtmltopdf(self):
"""Check the presence of wkhtmltopdf and return its version. If wkhtmltopdf
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 _check_wkhtmltopdf(self):
return wkhtmltopdf_state
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
@ -450,14 +430,14 @@ class Report(osv.Model):
:returns: Content of the pdf as a string
"""
command = ['wkhtmltopdf']
command_args = []
tmp_dir = tempfile.gettempdir()
command_args = []
# Passing the cookie to wkhtmltopdf in order to resolve URL.
try:
from openerp.addons.web.http import request
command_args.extend(['--cookie', 'session_id', request.httprequest.cookies['session_id']])
except:
command_args.extend(['--cookie', 'session_id', request.session.sid])
except AttributeError:
pass
# Display arguments
@ -510,8 +490,8 @@ class Report(osv.Model):
content_file.flush()
try:
# If the server is running with only one worker, increase it to two to be able
# to serve the http request from wkhtmltopdf.
# If the server is running with only one worker, ask to create a secund to be able
# to serve the http request of wkhtmltopdf subprocess.
if config['workers'] == 1:
ppid = psutil.Process(os.getpid()).ppid
os.kill(ppid, signal.SIGTTIN)
@ -629,26 +609,3 @@ class Report(osv.Model):
content = merged.read()
merged.close()
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) {
var wkhtmltopdf_state;
instance.web.ActionManager = instance.web.ActionManager.extend({
ir_actions_report_xml: function(action, options) {
@ -12,7 +13,7 @@ openerp.report = function(instance) {
// QWeb reports
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) {
case 'qweb-html':
report_url = '/report/' + action.report_name;
@ -55,20 +56,19 @@ openerp.report = function(instance) {
} else {
// Trigger the download of the pdf report
var c = openerp.webclient.crashmanager;
var response = new Array()
response[0] = report_url
response[1] = action.report_type
var response = new Array();
response[0] = report_url;
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
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 \
system. The report will be shown in html.<br><br><a href="http://wkhtmltopdf.org/" target="_blank">\
wkhtmltopdf.org</a>'), true);
window.open(report_url.substring(12), '_blank', 'height=768,width=1024');
instance.web.unblockUI();
}
else {
} else {
if (presence == 'upgrade') {
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\