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