[FIX] website: remove backslash from redirect with enable_editor
[odoo/odoo.git] / addons / report_webkit / webkit_report.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 # Copyright (c) 2010 Camptocamp SA (http://www.camptocamp.com)
5 # All Right Reserved
6 #
7 # Author : Nicolas Bessi (Camptocamp)
8 # Contributor(s) : Florent Xicluna (Wingo SA)
9 #
10 # WARNING: This program as such is intended to be used by professional
11 # programmers who take the whole responsability of assessing all potential
12 # consequences resulting from its eventual inadequacies and bugs
13 # End users who are looking for a ready-to-use solution with commercial
14 # garantees and support are strongly adviced to contract a Free Software
15 # Service Company
16 #
17 # This program is Free Software; you can redistribute it and/or
18 # modify it under the terms of the GNU General Public License
19 # as published by the Free Software Foundation; either version 2
20 # of the License, or (at your option) any later version.
21 #
22 # This program is distributed in the hope that it will be useful,
23 # but WITHOUT ANY WARRANTY; without even the implied warranty of
24 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
25 # GNU General Public License for more details.
26 #
27 # You should have received a copy of the GNU General Public License
28 # along with this program; if not, write to the Free Software
29 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
30 #
31 ##############################################################################
32
33 import subprocess
34 import os
35 import sys
36 from openerp import report
37 import tempfile
38 import time
39 import logging
40
41 from report_helper import WebKitHelper
42 import openerp
43 from openerp.modules.module import get_module_resource
44 from openerp.report.report_sxw import *
45 from openerp import tools
46 from openerp.tools.translate import _
47 from openerp.osv.osv import except_osv
48 from urllib import urlencode, quote as quote
49
50 _logger = logging.getLogger(__name__)
51
52 try:
53     # We use a jinja2 sandboxed environment to render mako templates.
54     # Note that the rendering does not cover all the mako syntax, in particular
55     # arbitrary Python statements are not accepted, and not all expressions are
56     # allowed: only "public" attributes (not starting with '_') of objects may
57     # be accessed.
58     # This is done on purpose: it prevents incidental or malicious execution of
59     # Python code that may break the security of the server.
60     from jinja2.sandbox import SandboxedEnvironment
61     mako_template_env = SandboxedEnvironment(
62         block_start_string="<%",
63         block_end_string="%>",
64         variable_start_string="${",
65         variable_end_string="}",
66         comment_start_string="<%doc>",
67         comment_end_string="</%doc>",
68         line_statement_prefix="%",
69         line_comment_prefix="##",
70         trim_blocks=True,               # do not output newline after blocks
71         autoescape=True,                # XML/HTML automatic escaping
72     )
73     mako_template_env.globals.update({
74         'str': str,
75         'quote': quote,
76         'urlencode': urlencode,
77     })
78 except ImportError:
79     _logger.warning("jinja2 not available, templating features will not work!")
80
81 def mako_template(text):
82     """Build a Mako template.
83
84     This template uses UTF-8 encoding
85     """
86
87     return mako_template_env.from_string(text)
88
89 _extender_functions = {}
90
91 def webkit_report_extender(report_name):
92     """
93     A decorator to define functions to extend the context used in a template rendering.
94     report_name must be the xml id of the desired report (it is mandatory to indicate the
95     module in that xml id).
96
97     The given function will be called at the creation of the report. The following arguments
98     will be passed to it (in this order):
99     - pool The model pool.
100     - cr The cursor.
101     - uid The user id.
102     - localcontext The context given to the template engine to render the templates for the
103         current report. This is the context that should be modified.
104     - context The OpenERP context.
105     """
106     def fct1(fct):
107         lst = _extender_functions.get(report_name)
108         if not lst:
109             lst = []
110             _extender_functions[report_name] = lst
111         lst.append(fct)
112         return fct
113     return fct1
114
115 class WebKitParser(report_sxw):
116     """Custom class that use webkit to render HTML reports
117        Code partially taken from report openoffice. Thanks guys :)
118     """
119     def __init__(self, name, table, rml=False, parser=rml_parse,
120         header=True, store=False, register=True):
121         self.parser_instance = False
122         self.localcontext = {}
123         report_sxw.__init__(self, name, table, rml, parser,
124             header, store, register=register)
125
126     def get_lib(self, cursor, uid):
127         """Return the lib wkhtml path"""
128         proxy = self.pool['ir.config_parameter']
129         webkit_path = proxy.get_param(cursor, uid, 'webkit_path')
130
131         if not webkit_path:
132             try:
133                 defpath = os.environ.get('PATH', os.defpath).split(os.pathsep)
134                 if hasattr(sys, 'frozen'):
135                     defpath.append(os.getcwd())
136                     if tools.config['root_path']:
137                         defpath.append(os.path.dirname(tools.config['root_path']))
138                 webkit_path = tools.which('wkhtmltopdf', path=os.pathsep.join(defpath))
139             except IOError:
140                 webkit_path = None
141
142         if webkit_path:
143             return webkit_path
144
145         raise except_osv(
146                          _('Wkhtmltopdf library path is not set'),
147                          _('Please install executable on your system' \
148                          ' (sudo apt-get install wkhtmltopdf) or download it from here:' \
149                          ' http://code.google.com/p/wkhtmltopdf/downloads/list and set the' \
150                          ' path in the ir.config_parameter with the webkit_path key.' \
151                          'Minimal version is 0.9.9')
152                         )
153
154     def generate_pdf(self, comm_path, report_xml, header, footer, html_list, webkit_header=False):
155         """Call webkit in order to generate pdf"""
156         if not webkit_header:
157             webkit_header = report_xml.webkit_header
158         tmp_dir = tempfile.gettempdir()
159         out_filename = tempfile.mktemp(suffix=".pdf", prefix="webkit.tmp.")
160         file_to_del = [out_filename]
161         if comm_path:
162             command = [comm_path]
163         else:
164             command = ['wkhtmltopdf']
165
166         command.append('--quiet')
167         # default to UTF-8 encoding.  Use <meta charset="latin-1"> to override.
168         command.extend(['--encoding', 'utf-8'])
169         if header :
170             head_file = file( os.path.join(
171                                   tmp_dir,
172                                   str(time.time()) + '.head.html'
173                                  ),
174                                 'w'
175                             )
176             head_file.write(header.encode('utf-8'))
177             head_file.close()
178             file_to_del.append(head_file.name)
179             command.extend(['--header-html', head_file.name])
180         if footer :
181             foot_file = file(  os.path.join(
182                                   tmp_dir,
183                                   str(time.time()) + '.foot.html'
184                                  ),
185                                 'w'
186                             )
187             foot_file.write(footer.encode('utf-8'))
188             foot_file.close()
189             file_to_del.append(foot_file.name)
190             command.extend(['--footer-html', foot_file.name])
191
192         if webkit_header.margin_top :
193             command.extend(['--margin-top', str(webkit_header.margin_top).replace(',', '.')])
194         if webkit_header.margin_bottom :
195             command.extend(['--margin-bottom', str(webkit_header.margin_bottom).replace(',', '.')])
196         if webkit_header.margin_left :
197             command.extend(['--margin-left', str(webkit_header.margin_left).replace(',', '.')])
198         if webkit_header.margin_right :
199             command.extend(['--margin-right', str(webkit_header.margin_right).replace(',', '.')])
200         if webkit_header.orientation :
201             command.extend(['--orientation', str(webkit_header.orientation).replace(',', '.')])
202         if webkit_header.format :
203             command.extend(['--page-size', str(webkit_header.format).replace(',', '.')])
204         count = 0
205         for html in html_list :
206             html_file = file(os.path.join(tmp_dir, str(time.time()) + str(count) +'.body.html'), 'w')
207             count += 1
208             html_file.write(html.encode('utf-8'))
209             html_file.close()
210             file_to_del.append(html_file.name)
211             command.append(html_file.name)
212         command.append(out_filename)
213         stderr_fd, stderr_path = tempfile.mkstemp(text=True)
214         file_to_del.append(stderr_path)
215         try:
216             status = subprocess.call(command, stderr=stderr_fd)
217             os.close(stderr_fd) # ensure flush before reading
218             stderr_fd = None # avoid closing again in finally block
219             fobj = open(stderr_path, 'r')
220             error_message = fobj.read()
221             fobj.close()
222             if not error_message:
223                 error_message = _('No diagnosis message was provided')
224             else:
225                 error_message = _('The following diagnosis message was provided:\n') + error_message
226             if status :
227                 raise except_osv(_('Webkit error' ),
228                                  _("The command 'wkhtmltopdf' failed with error code = %s. Message: %s") % (status, error_message))
229             pdf_file = open(out_filename, 'rb')
230             pdf = pdf_file.read()
231             pdf_file.close()
232         finally:
233             if stderr_fd is not None:
234                 os.close(stderr_fd)
235             for f_to_del in file_to_del:
236                 try:
237                     os.unlink(f_to_del)
238                 except (OSError, IOError), exc:
239                     _logger.error('cannot remove file %s: %s', f_to_del, exc)
240         return pdf
241
242     def translate_call(self, src):
243         """Translate String."""
244         ir_translation = self.pool['ir.translation']
245         name = self.tmpl and 'addons/' + self.tmpl or None
246         res = ir_translation._get_source(self.parser_instance.cr, self.parser_instance.uid,
247                                          name, 'report', self.parser_instance.localcontext.get('lang', 'en_US'), src)
248         if res == src:
249             # no translation defined, fallback on None (backward compatibility)
250             res = ir_translation._get_source(self.parser_instance.cr, self.parser_instance.uid,
251                                              None, 'report', self.parser_instance.localcontext.get('lang', 'en_US'), src)
252         if not res :
253             return src
254         return res
255
256     # override needed to keep the attachments storing procedure
257     def create_single_pdf(self, cursor, uid, ids, data, report_xml, context=None):
258         """generate the PDF"""
259
260         # just try to find an xml id for the report
261         cr = cursor
262         pool = openerp.registry(cr.dbname)
263         found_xml_ids = pool["ir.model.data"].search(cr, uid, [["model", "=", "ir.actions.report.xml"], \
264             ["res_id", "=", report_xml.id]], context=context)
265         xml_id = None
266         if found_xml_ids:
267             xml_id = pool["ir.model.data"].read(cr, uid, found_xml_ids[0], ["module", "name"])
268             xml_id = "%s.%s" % (xml_id["module"], xml_id["name"])
269
270         if context is None:
271             context={}
272         htmls = []
273         if report_xml.report_type != 'webkit':
274             return super(WebKitParser,self).create_single_pdf(cursor, uid, ids, data, report_xml, context=context)
275
276         self.parser_instance = self.parser(cursor,
277                                            uid,
278                                            self.name2,
279                                            context=context)
280
281         self.pool = pool
282         objs = self.getObjects(cursor, uid, ids, context)
283         self.parser_instance.set_context(objs, data, ids, report_xml.report_type)
284
285         template =  False
286
287         if report_xml.report_file :
288             path = get_module_resource(*report_xml.report_file.split('/'))
289             if path and os.path.exists(path) :
290                 template = file(path).read()
291         if not template and report_xml.report_webkit_data :
292             template =  report_xml.report_webkit_data
293         if not template :
294             raise except_osv(_('Error!'), _('Webkit report template not found!'))
295         header = report_xml.webkit_header.html
296         footer = report_xml.webkit_header.footer_html
297         if not header and report_xml.header:
298             raise except_osv(
299                   _('No header defined for this Webkit report!'),
300                   _('Please set a header in company settings.')
301               )
302         if not report_xml.header :
303             header = ''
304             default_head = get_module_resource('report_webkit', 'default_header.html')
305             with open(default_head,'r') as f:
306                 header = f.read()
307         css = report_xml.webkit_header.css
308         if not css :
309             css = ''
310
311         body_mako_tpl = mako_template(template)
312         helper = WebKitHelper(cursor, uid, report_xml.id, context)
313         self.parser_instance.localcontext['helper'] = helper
314         self.parser_instance.localcontext['css'] = css
315         self.parser_instance.localcontext['_'] = self.translate_call
316
317         # apply extender functions
318         additional = {}
319         if xml_id in _extender_functions:
320             for fct in _extender_functions[xml_id]:
321                 fct(pool, cr, uid, self.parser_instance.localcontext, context)
322
323         if report_xml.precise_mode:
324             ctx = dict(self.parser_instance.localcontext)
325             for obj in self.parser_instance.localcontext['objects']:
326                 ctx['objects'] = [obj]
327                 try :
328                     html = body_mako_tpl.render(dict(ctx))
329                     htmls.append(html)
330                 except Exception, e:
331                     msg = u"%s" % e
332                     _logger.error(msg)
333                     raise except_osv(_('Webkit render!'), msg)
334         else:
335             try :
336                 html = body_mako_tpl.render(dict(self.parser_instance.localcontext))
337                 htmls.append(html)
338             except Exception, e:
339                 msg = u"%s" % e
340                 _logger.error(msg)
341                 raise except_osv(_('Webkit render!'), msg)
342         head_mako_tpl = mako_template(header)
343         try :
344             head = head_mako_tpl.render(dict(self.parser_instance.localcontext, _debug=False))
345         except Exception, e:
346             raise except_osv(_('Webkit render!'), u"%s" % e)
347         foot = False
348         if footer :
349             foot_mako_tpl = mako_template(footer)
350             try :
351                 foot = foot_mako_tpl.render(dict({},
352                                             **self.parser_instance.localcontext))
353             except Exception, e:
354                 msg = u"%s" % e
355                 _logger.error(msg)
356                 raise except_osv(_('Webkit render!'), msg)
357         if report_xml.webkit_debug :
358             try :
359                 deb = head_mako_tpl.render(dict(self.parser_instance.localcontext, _debug=tools.ustr("\n".join(htmls))))
360             except Exception, e:
361                 msg = u"%s" % e
362                 _logger.error(msg)
363                 raise except_osv(_('Webkit render!'), msg)
364             return (deb, 'html')
365         bin = self.get_lib(cursor, uid)
366         pdf = self.generate_pdf(bin, report_xml, head, foot, htmls)
367         return (pdf, 'pdf')
368
369
370     def create(self, cursor, uid, ids, data, context=None):
371         """We override the create function in order to handle generator
372            Code taken from report openoffice. Thanks guys :) """
373         pool = openerp.registry(cursor.dbname)
374         ir_obj = pool['ir.actions.report.xml']
375         report_xml_ids = ir_obj.search(cursor, uid,
376                 [('report_name', '=', self.name[7:])], context=context)
377         if report_xml_ids:
378
379             report_xml = ir_obj.browse(cursor,
380                                        uid,
381                                        report_xml_ids[0],
382                                        context=context)
383             report_xml.report_rml = None
384             report_xml.report_rml_content = None
385             report_xml.report_sxw_content_data = None
386             report_xml.report_sxw_content = None
387             report_xml.report_sxw = None
388         else:
389             return super(WebKitParser, self).create(cursor, uid, ids, data, context)
390         if report_xml.report_type != 'webkit' :
391             return super(WebKitParser, self).create(cursor, uid, ids, data, context)
392         result = self.create_source_pdf(cursor, uid, ids, data, report_xml, context)
393         if not result:
394             return (False,False)
395         return result
396
397 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: