[IMP] tools: mail: improved html_email_clean
[odoo/odoo.git] / openerp / tools / test_reports.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2010-2012 OpenERP s.a. (<http://openerp.com>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 """ Helper functions for reports testing.
23
24     Please /do not/ import this file by default, but only explicitly call it
25     through the code of yaml tests.
26 """
27
28 import openerp
29 import openerp.report
30 import openerp.tools as tools
31 import logging
32 from openerp.tools.safe_eval import safe_eval
33 from subprocess import Popen, PIPE
34 import os
35 import tempfile
36
37 _logger = logging.getLogger(__name__)
38 _test_logger = logging.getLogger('openerp.tests')
39
40
41 def try_report(cr, uid, rname, ids, data=None, context=None, our_module=None):
42     """ Try to render a report <rname> with contents of ids
43     
44         This function should also check for common pitfalls of reports.
45     """
46     if data is None:
47         data = {}
48     if context is None:
49         context = {}
50     if rname.startswith('report.'):
51         rname_s = rname[7:]
52     else:
53         rname_s = rname
54     _test_logger.info("  - Trying %s.create(%r)", rname, ids)
55     res = openerp.report.render_report(cr, uid, ids, rname_s, data, context)
56     if not isinstance(res, tuple):
57         raise RuntimeError("Result of %s.create() should be a (data,format) tuple, now it is a %s" % \
58                                 (rname, type(res)))
59     (res_data, res_format) = res
60
61     if not res_data:
62         raise ValueError("Report %s produced an empty result!" % rname)
63
64     if tools.config['test_report_directory']:
65         file(os.path.join(tools.config['test_report_directory'], rname+ '.'+res_format), 'wb+').write(res_data)
66
67     _logger.debug("Have a %s report for %s, will examine it", res_format, rname)
68     if res_format == 'pdf':
69         if res_data[:5] != '%PDF-':
70             raise ValueError("Report %s produced a non-pdf header, %r" % (rname, res_data[:10]))
71
72         res_text = False
73         try:
74             fd, rfname = tempfile.mkstemp(suffix=res_format)
75             os.write(fd, res_data)
76             os.close(fd)
77
78             proc = Popen(['pdftotext', '-enc', 'UTF-8', '-nopgbrk', rfname, '-'], shell=False, stdout=PIPE)
79             stdout, stderr = proc.communicate()
80             res_text = tools.ustr(stdout)
81             os.unlink(rfname)
82         except Exception:
83             _logger.debug("Unable to parse PDF report: install pdftotext to perform automated tests.")
84
85         if res_text is not False:
86             for line in res_text.split('\n'):
87                 if ('[[' in line) or ('[ [' in line):
88                     _logger.error("Report %s may have bad expression near: \"%s\".", rname, line[80:])
89             # TODO more checks, what else can be a sign of a faulty report?
90     elif res_format == 'foobar':
91         # TODO
92         pass
93     else:
94         _logger.warning("Report %s produced a \"%s\" chunk, cannot examine it", rname, res_format)
95         return False
96
97     _test_logger.info("  + Report %s produced correctly.", rname)
98     return True
99
100 def try_report_action(cr, uid, action_id, active_model=None, active_ids=None,
101                 wiz_data=None, wiz_buttons=None,
102                 context=None, our_module=None):
103     """Take an ir.action.act_window and follow it until a report is produced
104
105         :param action_id: the integer id of an action, or a reference to xml id
106                 of the act_window (can search [our_module.]+xml_id
107         :param active_model, active_ids: call the action as if it had been launched
108                 from that model+ids (tree/form view action)
109         :param wiz_data: a dictionary of values to use in the wizard, if needed.
110                 They will override (or complete) the default values of the
111                 wizard form.
112         :param wiz_buttons: a list of button names, or button icon strings, which
113                 should be preferred to press during the wizard.
114                 Eg. 'OK' or 'gtk-print'
115         :param our_module: the name of the calling module (string), like 'account'
116     """
117
118     if not our_module and isinstance(action_id, basestring):
119         if '.' in action_id:
120             our_module = action_id.split('.', 1)[0]
121
122     if context is None:
123         context = {}
124     else:
125         context = context.copy() # keep it local
126     # TODO context fill-up
127
128     registry = openerp.registry(cr.dbname)
129
130     def log_test(msg, *args):
131         _test_logger.info("  - " + msg, *args)
132
133     datas = {}
134     if active_model:
135         datas['model'] = active_model
136     if active_ids:
137         datas['ids'] = active_ids
138
139     if not wiz_buttons:
140         wiz_buttons = []
141
142     if isinstance(action_id, basestring):
143         if '.' in action_id:
144             act_module, act_xmlid = action_id.split('.', 1)
145         else:
146             if not our_module:
147                 raise ValueError('You cannot only specify action_id "%s" without a module name' % action_id)
148             act_module = our_module
149             act_xmlid = action_id
150         act_model, act_id = registry['ir.model.data'].get_object_reference(cr, uid, act_module, act_xmlid)
151     else:
152         assert isinstance(action_id, (long, int))
153         act_model = 'ir.action.act_window'     # assume that
154         act_id = action_id
155         act_xmlid = '<%s>' % act_id
156
157     def _exec_action(action, datas, context):
158         # taken from client/modules/action/main.py:84 _exec_action()
159         if isinstance(action, bool) or 'type' not in action:
160             return
161         # Updating the context : Adding the context of action in order to use it on Views called from buttons
162         if datas.get('id',False):
163             context.update( {'active_id': datas.get('id',False), 'active_ids': datas.get('ids',[]), 'active_model': datas.get('model',False)})
164         context.update(safe_eval(action.get('context','{}'), context.copy()))
165         if action['type'] in ['ir.actions.act_window', 'ir.actions.submenu']:
166             for key in ('res_id', 'res_model', 'view_type', 'view_mode',
167                     'limit', 'auto_refresh', 'search_view', 'auto_search', 'search_view_id'):
168                 datas[key] = action.get(key, datas.get(key, None))
169
170             view_id = False
171             if action.get('views', []):
172                 if isinstance(action['views'],list):
173                     view_id = action['views'][0][0]
174                     datas['view_mode']= action['views'][0][1]
175                 else:
176                     if action.get('view_id', False):
177                         view_id = action['view_id'][0]
178             elif action.get('view_id', False):
179                 view_id = action['view_id'][0]
180
181             assert datas['res_model'], "Cannot use the view without a model"
182             # Here, we have a view that we need to emulate
183             log_test("will emulate a %s view: %s#%s",
184                         action['view_type'], datas['res_model'], view_id or '?')
185
186             view_res = registry[datas['res_model']].fields_view_get(cr, uid, view_id, action['view_type'], context)
187             assert view_res and view_res.get('arch'), "Did not return any arch for the view"
188             view_data = {}
189             if view_res.get('fields',{}).keys():
190                 view_data = registry[datas['res_model']].default_get(cr, uid, view_res['fields'].keys(), context)
191             if datas.get('form'):
192                 view_data.update(datas.get('form'))
193             if wiz_data:
194                 view_data.update(wiz_data)
195             _logger.debug("View data is: %r", view_data)
196
197             for fk, field in view_res.get('fields',{}).items():
198                 # Default fields returns list of int, while at create()
199                 # we need to send a [(6,0,[int,..])]
200                 if field['type'] in ('one2many', 'many2many') \
201                         and view_data.get(fk, False) \
202                         and isinstance(view_data[fk], list) \
203                         and not isinstance(view_data[fk][0], tuple) :
204                     view_data[fk] = [(6, 0, view_data[fk])]
205
206             action_name = action.get('name')
207             try:
208                 from xml.dom import minidom
209                 cancel_found = False
210                 buttons = []
211                 dom_doc = minidom.parseString(view_res['arch'])
212                 if not action_name:
213                     action_name = dom_doc.documentElement.getAttribute('name')
214
215                 for button in dom_doc.getElementsByTagName('button'):
216                     button_weight = 0
217                     if button.getAttribute('special') == 'cancel':
218                         cancel_found = True
219                         continue
220                     if button.getAttribute('icon') == 'gtk-cancel':
221                         cancel_found = True
222                         continue
223                     if button.getAttribute('default_focus') == '1':
224                         button_weight += 20
225                     if button.getAttribute('string') in wiz_buttons:
226                         button_weight += 30
227                     elif button.getAttribute('icon') in wiz_buttons:
228                         button_weight += 10
229                     string = button.getAttribute('string') or '?%s' % len(buttons)
230
231                     buttons.append( { 'name': button.getAttribute('name'),
232                                 'string': string,
233                                 'type': button.getAttribute('type'),
234                                 'weight': button_weight,
235                                 })
236             except Exception, e:
237                 _logger.warning("Cannot resolve the view arch and locate the buttons!", exc_info=True)
238                 raise AssertionError(e.args[0])
239
240             if not datas['res_id']:
241                 # it is probably an orm_memory object, we need to create
242                 # an instance
243                 datas['res_id'] = registry[datas['res_model']].create(cr, uid, view_data, context)
244
245             if not buttons:
246                 raise AssertionError("view form doesn't have any buttons to press!")
247
248             buttons.sort(key=lambda b: b['weight'])
249             _logger.debug('Buttons are: %s', ', '.join([ '%s: %d' % (b['string'], b['weight']) for b in buttons]))
250
251             res = None
252             while buttons and not res:
253                 b = buttons.pop()
254                 log_test("in the \"%s\" form, I will press the \"%s\" button.", action_name, b['string'])
255                 if not b['type']:
256                     log_test("the \"%s\" button has no type, cannot use it", b['string'])
257                     continue
258                 if b['type'] == 'object':
259                     #there we are! press the button!
260                     fn =  getattr(registry[datas['res_model']], b['name'])
261                     if not fn:
262                         _logger.error("The %s model doesn't have a %s attribute!", datas['res_model'], b['name'])
263                         continue
264                     res = fn(cr, uid, [datas['res_id'],], context)
265                     break
266                 else:
267                     _logger.warning("in the \"%s\" form, the \"%s\" button has unknown type %s",
268                         action_name, b['string'], b['type'])
269             return res
270
271         elif action['type']=='ir.actions.report.xml':
272             if 'window' in datas:
273                 del datas['window']
274             if not datas:
275                 datas = action.get('datas',{})
276             datas = datas.copy()
277             ids = datas.get('ids')
278             if 'ids' in datas:
279                 del datas['ids']
280             res = try_report(cr, uid, 'report.'+action['report_name'], ids, datas, context, our_module=our_module)
281             return res
282         else:
283             raise Exception("Cannot handle action of type %s" % act_model)
284
285     log_test("will be using %s action %s #%d", act_model, act_xmlid, act_id)
286     action = registry[act_model].read(cr, uid, act_id, context=context)
287     assert action, "Could not read action %s[%s]" %(act_model, act_id)
288     loop = 0
289     while action:
290         loop += 1
291         # This part tries to emulate the loop of the Gtk client
292         if loop > 100:
293             _logger.error("Passed %d loops, giving up", loop)
294             raise Exception("Too many loops at action")
295         log_test("it is an %s action at loop #%d", action.get('type', 'unknown'), loop)
296         result = _exec_action(action, datas, context)
297         if not isinstance(result, dict):
298             break
299         datas = result.get('datas', {})
300         if datas:
301             del result['datas']
302         action = result
303
304     return True
305
306 #eof
307
308 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: