Unify and refactor exception handling in framework and addons. The generic `except_osv` is now deprecated, and replaced by more specialized exception subtypes: - `UserError` (renamed from Warning, as it conflicts with the built-in `Warning`) raised when a non-technical error occurs during a business operation. It could be a missing information in the data provided by the user, or a misconfiguration. - `AccessError`: raised when any operation is denied because the user conducting it does not have the required access rights. - `AccessDenied`: raised when an operation that requires authenticated access is attempted via an unauthenticated request. - `MissingError`: raised when an operation is attempted on a record that does not exist. - `ValidationError`: raised when an operation violates a SQL or Python constraint. - All other exceptions are internal errors due to a system problem or bug, and raised untouched to the client-side, which should display a traceback. All exceptions take a single message argument. The `test_exceptions` module has been updated to showcase both new and old (deprecated) exceptions. A great many old `except_osv` had a useless title with "Error!" or "Warning", those have been removed, as this is handled by the client-side widget that displays the messages. This commit introduces a more consistent policy for logging errors and warnings: - All messages that do not require administrator attention should be logged at INFO level or lower. This includes all errors that are notified to the user in a friendly manner, even for access right problems or validation errors during business operations. - All messages that indicate a likely misconfiguration or malicious use by the users should be logged at WARNING level, as they typically require administrator attention. - All other unhandled internal errors cannot typically be handled by the user and should be logged at ERROR or higher level, as they require immediate administrator attention.
381 lines
16 KiB
Python
381 lines
16 KiB
Python
# -*- coding: utf-8 -*-
|
|
##############################################################################
|
|
#
|
|
# Copyright (c) 2010 Camptocamp SA (http://www.camptocamp.com)
|
|
# All Right Reserved
|
|
#
|
|
# Author : Nicolas Bessi (Camptocamp)
|
|
# Contributor(s) : Florent Xicluna (Wingo SA)
|
|
#
|
|
# WARNING: This program as such is intended to be used by professional
|
|
# programmers who take the whole responsability of assessing all potential
|
|
# consequences resulting from its eventual inadequacies and bugs
|
|
# End users who are looking for a ready-to-use solution with commercial
|
|
# garantees and support are strongly adviced to contract a Free Software
|
|
# Service Company
|
|
#
|
|
# This program is Free Software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License
|
|
# as published by the Free Software Foundation; either version 2
|
|
# of the License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
#
|
|
##############################################################################
|
|
|
|
import subprocess
|
|
import os
|
|
import sys
|
|
from openerp import report
|
|
import tempfile
|
|
import time
|
|
import logging
|
|
from functools import partial
|
|
|
|
from report_helper import WebKitHelper
|
|
import openerp
|
|
from openerp.modules.module import get_module_resource
|
|
from openerp.report.report_sxw import *
|
|
from openerp import tools
|
|
from openerp.tools.translate import _
|
|
from urllib import urlencode, quote as quote
|
|
from openerp.exceptions import UserError
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
try:
|
|
# We use a jinja2 sandboxed environment to render mako templates.
|
|
# Note that the rendering does not cover all the mako syntax, in particular
|
|
# arbitrary Python statements are not accepted, and not all expressions are
|
|
# allowed: only "public" attributes (not starting with '_') of objects may
|
|
# be accessed.
|
|
# This is done on purpose: it prevents incidental or malicious execution of
|
|
# Python code that may break the security of the server.
|
|
from jinja2.sandbox import SandboxedEnvironment
|
|
mako_template_env = SandboxedEnvironment(
|
|
block_start_string="<%",
|
|
block_end_string="%>",
|
|
variable_start_string="${",
|
|
variable_end_string="}",
|
|
comment_start_string="<%doc>",
|
|
comment_end_string="</%doc>",
|
|
line_statement_prefix="%",
|
|
line_comment_prefix="##",
|
|
trim_blocks=True, # do not output newline after blocks
|
|
autoescape=True, # XML/HTML automatic escaping
|
|
)
|
|
mako_template_env.globals.update({
|
|
'str': str,
|
|
'quote': quote,
|
|
'urlencode': urlencode,
|
|
})
|
|
except ImportError:
|
|
_logger.warning("jinja2 not available, templating features will not work!")
|
|
|
|
def mako_template(text):
|
|
"""Build a Mako template.
|
|
|
|
This template uses UTF-8 encoding
|
|
"""
|
|
|
|
return mako_template_env.from_string(text)
|
|
|
|
_extender_functions = {}
|
|
|
|
def webkit_report_extender(report_name):
|
|
"""
|
|
A decorator to define functions to extend the context used in a template rendering.
|
|
report_name must be the xml id of the desired report (it is mandatory to indicate the
|
|
module in that xml id).
|
|
|
|
The given function will be called at the creation of the report. The following arguments
|
|
will be passed to it (in this order):
|
|
- pool The model pool.
|
|
- cr The cursor.
|
|
- uid The user id.
|
|
- localcontext The context given to the template engine to render the templates for the
|
|
current report. This is the context that should be modified.
|
|
- context The OpenERP context.
|
|
"""
|
|
def fct1(fct):
|
|
lst = _extender_functions.get(report_name)
|
|
if not lst:
|
|
lst = []
|
|
_extender_functions[report_name] = lst
|
|
lst.append(fct)
|
|
return fct
|
|
return fct1
|
|
|
|
|
|
class WebKitParser(report_sxw):
|
|
"""Custom class that use webkit to render HTML reports
|
|
Code partially taken from report openoffice. Thanks guys :)
|
|
"""
|
|
def __init__(self, name, table, rml=False, parser=rml_parse,
|
|
header=True, store=False, register=True):
|
|
self.localcontext = {}
|
|
report_sxw.__init__(self, name, table, rml, parser,
|
|
header, store, register=register)
|
|
|
|
def get_lib(self, cursor, uid):
|
|
"""Return the lib wkhtml path"""
|
|
proxy = self.pool['ir.config_parameter']
|
|
webkit_path = proxy.get_param(cursor, uid, 'webkit_path')
|
|
|
|
if not webkit_path:
|
|
try:
|
|
defpath = os.environ.get('PATH', os.defpath).split(os.pathsep)
|
|
if hasattr(sys, 'frozen'):
|
|
defpath.append(os.getcwd())
|
|
if tools.config['root_path']:
|
|
defpath.append(os.path.dirname(tools.config['root_path']))
|
|
webkit_path = tools.which('wkhtmltopdf', path=os.pathsep.join(defpath))
|
|
except IOError:
|
|
webkit_path = None
|
|
|
|
if webkit_path:
|
|
return webkit_path
|
|
|
|
raise UserError(_('Wkhtmltopdf library path is not set') + ' ' +
|
|
_('Please install executable on your system'
|
|
' (sudo apt-get install wkhtmltopdf) or download it from here:'
|
|
' http://code.google.com/p/wkhtmltopdf/downloads/list and set the'
|
|
' path in the ir.config_parameter with the webkit_path key.'
|
|
'Minimal version is 0.9.9'))
|
|
|
|
def generate_pdf(self, comm_path, report_xml, header, footer, html_list, webkit_header=False):
|
|
"""Call webkit in order to generate pdf"""
|
|
if not webkit_header:
|
|
webkit_header = report_xml.webkit_header
|
|
fd, out_filename = tempfile.mkstemp(suffix=".pdf",
|
|
prefix="webkit.tmp.")
|
|
file_to_del = [out_filename]
|
|
if comm_path:
|
|
command = [comm_path]
|
|
else:
|
|
command = ['wkhtmltopdf']
|
|
|
|
command.append('--quiet')
|
|
# default to UTF-8 encoding. Use <meta charset="latin-1"> to override.
|
|
command.extend(['--encoding', 'utf-8'])
|
|
if header :
|
|
with tempfile.NamedTemporaryFile(suffix=".head.html",
|
|
delete=False) as head_file:
|
|
head_file.write(self._sanitize_html(header.encode('utf-8')))
|
|
file_to_del.append(head_file.name)
|
|
command.extend(['--header-html', head_file.name])
|
|
if footer :
|
|
with tempfile.NamedTemporaryFile(suffix=".foot.html",
|
|
delete=False) as foot_file:
|
|
foot_file.write(self._sanitize_html(footer.encode('utf-8')))
|
|
file_to_del.append(foot_file.name)
|
|
command.extend(['--footer-html', foot_file.name])
|
|
|
|
if webkit_header.margin_top :
|
|
command.extend(['--margin-top', str(webkit_header.margin_top).replace(',', '.')])
|
|
if webkit_header.margin_bottom :
|
|
command.extend(['--margin-bottom', str(webkit_header.margin_bottom).replace(',', '.')])
|
|
if webkit_header.margin_left :
|
|
command.extend(['--margin-left', str(webkit_header.margin_left).replace(',', '.')])
|
|
if webkit_header.margin_right :
|
|
command.extend(['--margin-right', str(webkit_header.margin_right).replace(',', '.')])
|
|
if webkit_header.orientation :
|
|
command.extend(['--orientation', str(webkit_header.orientation).replace(',', '.')])
|
|
if webkit_header.format :
|
|
command.extend(['--page-size', str(webkit_header.format).replace(',', '.')])
|
|
count = 0
|
|
for html in html_list :
|
|
with tempfile.NamedTemporaryFile(suffix="%d.body.html" %count,
|
|
delete=False) as html_file:
|
|
count += 1
|
|
html_file.write(self._sanitize_html(html.encode('utf-8')))
|
|
file_to_del.append(html_file.name)
|
|
command.append(html_file.name)
|
|
command.append(out_filename)
|
|
stderr_fd, stderr_path = tempfile.mkstemp(text=True)
|
|
file_to_del.append(stderr_path)
|
|
try:
|
|
status = subprocess.call(command, stderr=stderr_fd)
|
|
os.close(stderr_fd) # ensure flush before reading
|
|
stderr_fd = None # avoid closing again in finally block
|
|
fobj = open(stderr_path, 'r')
|
|
error_message = fobj.read()
|
|
fobj.close()
|
|
if not error_message:
|
|
error_message = _('No diagnosis message was provided')
|
|
else:
|
|
error_message = _('The following diagnosis message was provided:\n') + error_message
|
|
if status :
|
|
raise UserError(_("The command 'wkhtmltopdf' failed with error code = %s. Message: %s") % (status, error_message))
|
|
with open(out_filename, 'rb') as pdf_file:
|
|
pdf = pdf_file.read()
|
|
os.close(fd)
|
|
finally:
|
|
if stderr_fd is not None:
|
|
os.close(stderr_fd)
|
|
for f_to_del in file_to_del:
|
|
try:
|
|
os.unlink(f_to_del)
|
|
except (OSError, IOError), exc:
|
|
_logger.error('cannot remove file %s: %s', f_to_del, exc)
|
|
return pdf
|
|
|
|
def translate_call(self, parser_instance, src):
|
|
"""Translate String."""
|
|
ir_translation = self.pool['ir.translation']
|
|
name = self.tmpl and 'addons/' + self.tmpl or None
|
|
res = ir_translation._get_source(parser_instance.cr, parser_instance.uid,
|
|
name, 'report', parser_instance.localcontext.get('lang', 'en_US'), src)
|
|
if res == src:
|
|
# no translation defined, fallback on None (backward compatibility)
|
|
res = ir_translation._get_source(parser_instance.cr, parser_instance.uid,
|
|
None, 'report', parser_instance.localcontext.get('lang', 'en_US'), src)
|
|
if not res :
|
|
return src
|
|
return res
|
|
|
|
# override needed to keep the attachments storing procedure
|
|
def create_single_pdf(self, cursor, uid, ids, data, report_xml, context=None):
|
|
"""generate the PDF"""
|
|
|
|
# just try to find an xml id for the report
|
|
cr = cursor
|
|
pool = openerp.registry(cr.dbname)
|
|
found_xml_ids = pool["ir.model.data"].search(cr, uid, [["model", "=", "ir.actions.report.xml"], \
|
|
["res_id", "=", report_xml.id]], context=context)
|
|
xml_id = None
|
|
if found_xml_ids:
|
|
xml_id = pool["ir.model.data"].read(cr, uid, found_xml_ids[0], ["module", "name"])
|
|
xml_id = "%s.%s" % (xml_id["module"], xml_id["name"])
|
|
|
|
if context is None:
|
|
context={}
|
|
htmls = []
|
|
if report_xml.report_type != 'webkit':
|
|
return super(WebKitParser,self).create_single_pdf(cursor, uid, ids, data, report_xml, context=context)
|
|
|
|
parser_instance = self.parser(cursor,
|
|
uid,
|
|
self.name2,
|
|
context=context)
|
|
|
|
self.pool = pool
|
|
objs = self.getObjects(cursor, uid, ids, context)
|
|
parser_instance.set_context(objs, data, ids, report_xml.report_type)
|
|
|
|
template = False
|
|
|
|
if report_xml.report_file :
|
|
path = get_module_resource(*report_xml.report_file.split('/'))
|
|
if path and os.path.exists(path) :
|
|
template = file(path).read()
|
|
if not template and report_xml.report_webkit_data :
|
|
template = report_xml.report_webkit_data
|
|
if not template :
|
|
raise UserError(_('Webkit report template not found!'))
|
|
header = report_xml.webkit_header.html
|
|
footer = report_xml.webkit_header.footer_html
|
|
if not header and report_xml.use_global_header:
|
|
raise UserError(_('No header defined for this Webkit report!') + " " + _('Please set a header in company settings.'))
|
|
if not report_xml.use_global_header :
|
|
header = ''
|
|
default_head = get_module_resource('report_webkit', 'default_header.html')
|
|
with open(default_head,'r') as f:
|
|
header = f.read()
|
|
css = report_xml.webkit_header.css
|
|
if not css :
|
|
css = ''
|
|
|
|
translate_call = partial(self.translate_call, parser_instance)
|
|
body_mako_tpl = mako_template(template)
|
|
helper = WebKitHelper(cursor, uid, report_xml.id, context)
|
|
parser_instance.localcontext['helper'] = helper
|
|
parser_instance.localcontext['css'] = css
|
|
parser_instance.localcontext['_'] = translate_call
|
|
|
|
# apply extender functions
|
|
additional = {}
|
|
if xml_id in _extender_functions:
|
|
for fct in _extender_functions[xml_id]:
|
|
fct(pool, cr, uid, parser_instance.localcontext, context)
|
|
|
|
if report_xml.precise_mode:
|
|
ctx = dict(parser_instance.localcontext)
|
|
for obj in parser_instance.localcontext['objects']:
|
|
ctx['objects'] = [obj]
|
|
try :
|
|
html = body_mako_tpl.render(dict(ctx))
|
|
htmls.append(html)
|
|
except Exception, e:
|
|
msg = u"%s" % e
|
|
_logger.info(msg, exc_info=True)
|
|
raise UserError(msg)
|
|
else:
|
|
try :
|
|
html = body_mako_tpl.render(dict(parser_instance.localcontext))
|
|
htmls.append(html)
|
|
except Exception, e:
|
|
msg = u"%s" % e
|
|
_logger.info(msg, exc_info=True)
|
|
raise UserError(msg)
|
|
head_mako_tpl = mako_template(header)
|
|
try :
|
|
head = head_mako_tpl.render(dict(parser_instance.localcontext, _debug=False))
|
|
except Exception, e:
|
|
raise UserError(tools.ustr(e))
|
|
foot = False
|
|
if footer :
|
|
foot_mako_tpl = mako_template(footer)
|
|
try :
|
|
foot = foot_mako_tpl.render(dict(parser_instance.localcontext))
|
|
except Exception, e:
|
|
msg = u"%s" % e
|
|
_logger.info(msg, exc_info=True)
|
|
raise UserError(msg)
|
|
if report_xml.webkit_debug :
|
|
try :
|
|
deb = head_mako_tpl.render(dict(parser_instance.localcontext, _debug=tools.ustr("\n".join(htmls))))
|
|
except Exception, e:
|
|
msg = u"%s" % e
|
|
_logger.info(msg, exc_info=True)
|
|
raise UserError(msg)
|
|
return (deb, 'html')
|
|
bin = self.get_lib(cursor, uid)
|
|
pdf = self.generate_pdf(bin, report_xml, head, foot, htmls)
|
|
return (pdf, 'pdf')
|
|
|
|
def create(self, cursor, uid, ids, data, context=None):
|
|
"""We override the create function in order to handle generator
|
|
Code taken from report openoffice. Thanks guys :) """
|
|
pool = openerp.registry(cursor.dbname)
|
|
ir_obj = pool['ir.actions.report.xml']
|
|
report_xml_ids = ir_obj.search(cursor, uid,
|
|
[('report_name', '=', self.name[7:])], context=context)
|
|
if report_xml_ids:
|
|
report_xml = ir_obj.browse(cursor, uid, report_xml_ids[0], context=context)
|
|
else:
|
|
return super(WebKitParser, self).create(cursor, uid, ids, data, context)
|
|
|
|
setattr(report_xml, 'use_global_header', self.header if report_xml.header else False)
|
|
|
|
if report_xml.report_type != 'webkit':
|
|
return super(WebKitParser, self).create(cursor, uid, ids, data, context)
|
|
result = self.create_source_pdf(cursor, uid, ids, data, report_xml, context)
|
|
if not result:
|
|
return (False,False)
|
|
return result
|
|
|
|
def _sanitize_html(self, html):
|
|
"""wkhtmltopdf expects the html page to declare a doctype.
|
|
"""
|
|
if html and html[:9].upper() != "<!DOCTYPE":
|
|
html = "<!DOCTYPE html>\n" + html
|
|
return html
|