[IMP] [TMP] controller: search for all menus to have their needaction info, because...
[odoo/odoo.git] / addons / web / controllers / main.py
1 # -*- coding: utf-8 -*-
2
3 import ast
4 import base64
5 import csv
6 import glob
7 import itertools
8 import logging
9 import operator
10 import datetime
11 import hashlib
12 import os
13 import re
14 import simplejson
15 import time
16 import urllib2
17 import xmlrpclib
18 import zlib
19 from xml.etree import ElementTree
20 from cStringIO import StringIO
21
22 import babel.messages.pofile
23 import werkzeug.utils
24 import werkzeug.wrappers
25 try:
26     import xlwt
27 except ImportError:
28     xlwt = None
29
30 import openerp
31
32 from .. import http
33 from .. import nonliterals
34 openerpweb = http
35
36 #----------------------------------------------------------
37 # OpenERP Web helpers
38 #----------------------------------------------------------
39
40 def rjsmin(script):
41     """ Minify js with a clever regex.
42     Taken from http://opensource.perlig.de/rjsmin
43     Apache License, Version 2.0 """
44     def subber(match):
45         """ Substitution callback """
46         groups = match.groups()
47         return (
48             groups[0] or
49             groups[1] or
50             groups[2] or
51             groups[3] or
52             (groups[4] and '\n') or
53             (groups[5] and ' ') or
54             (groups[6] and ' ') or
55             (groups[7] and ' ') or
56             ''
57         )
58
59     result = re.sub(
60         r'([^\047"/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]|\r?'
61         r'\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|'
62         r'\r)[^"\\\r\n]*)*"))[^\047"/\000-\040]*)|(?:(?<=[(,=:\[!&|?{};\r\n]'
63         r')(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/'
64         r'))*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*'
65         r'(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*'
66         r'))|(?:(?<=[\000-#%-,./:-@\[-^`{-~-]return)(?:[\000-\011\013\014\01'
67         r'6-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*((?:/(?![\r\n/*])[^/'
68         r'\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]'
69         r'*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*))|(?<=[^\000-!#%&(*,./'
70         r':-@\[\\^`{|~])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/'
71         r'*][^*]*\*+)*/))*(?:((?:(?://[^\r\n]*)?[\r\n]))(?:[\000-\011\013\01'
72         r'4\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040"#'
73         r'%-\047)*,./:-@\\-^`|-~])|(?<=[^\000-#%-,./:-@\[-^`{-~-])((?:[\000-'
74         r'\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=[^'
75         r'\000-#%-,./:-@\[-^`{-~-])|(?<=\+)((?:[\000-\011\013\014\016-\040]|'
76         r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=\+)|(?<=-)((?:[\000-\011\0'
77         r'13\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=-)|(?:[\0'
78         r'00-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))+|(?:'
79         r'(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*'
80         r']*\*+(?:[^/*][^*]*\*+)*/))*)+', subber, '\n%s\n' % script
81     ).strip()
82     return result
83
84 def db_list(req):
85     proxy = req.session.proxy("db")
86     dbs = proxy.list()
87     h = req.httprequest.environ['HTTP_HOST'].split(':')[0]
88     d = h.split('.')[0]
89     r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
90     dbs = [i for i in dbs if re.match(r, i)]
91     return dbs
92
93 def db_monodb(req):
94     # if only one db exists, return it else return False
95     try:
96         dbs = db_list(req)
97         if len(dbs) == 1:
98             return dbs[0]
99     except xmlrpclib.Fault:
100         # ignore access denied
101         pass
102     return False
103
104 def module_topological_sort(modules):
105     """ Return a list of module names sorted so that their dependencies of the
106     modules are listed before the module itself
107
108     modules is a dict of {module_name: dependencies}
109
110     :param modules: modules to sort
111     :type modules: dict
112     :returns: list(str)
113     """
114
115     dependencies = set(itertools.chain.from_iterable(modules.itervalues()))
116     # incoming edge: dependency on other module (if a depends on b, a has an
117     # incoming edge from b, aka there's an edge from b to a)
118     # outgoing edge: other module depending on this one
119
120     # [Tarjan 1976], http://en.wikipedia.org/wiki/Topological_sorting#Algorithms
121     #L ← Empty list that will contain the sorted nodes
122     L = []
123     #S ← Set of all nodes with no outgoing edges (modules on which no other
124     #    module depends)
125     S = set(module for module in modules if module not in dependencies)
126
127     visited = set()
128     #function visit(node n)
129     def visit(n):
130         #if n has not been visited yet then
131         if n not in visited:
132             #mark n as visited
133             visited.add(n)
134             #change: n not web module, can not be resolved, ignore
135             if n not in modules: return
136             #for each node m with an edge from m to n do (dependencies of n)
137             for m in modules[n]:
138                 #visit(m)
139                 visit(m)
140             #add n to L
141             L.append(n)
142     #for each node n in S do
143     for n in S:
144         #visit(n)
145         visit(n)
146     return L
147
148 def module_installed(req):
149     # Candidates module the current heuristic is the /static dir
150     loadable = openerpweb.addons_manifest.keys()
151     modules = {}
152
153     # Retrieve database installed modules
154     # TODO The following code should move to ir.module.module.list_installed_modules()
155     Modules = req.session.model('ir.module.module')
156     domain = [('state','=','installed'), ('name','in', loadable)]
157     for module in Modules.search_read(domain, ['name', 'dependencies_id']):
158         modules[module['name']] = []
159         deps = module.get('dependencies_id')
160         if deps:
161             deps_read = req.session.model('ir.module.module.dependency').read(deps, ['name'])
162             dependencies = [i['name'] for i in deps_read]
163             modules[module['name']] = dependencies
164
165     sorted_modules = module_topological_sort(modules)
166     return sorted_modules
167
168 def module_installed_bypass_session(dbname):
169     loadable = openerpweb.addons_manifest.keys()
170     modules = {}
171     try:
172         import openerp.modules.registry
173         registry = openerp.modules.registry.RegistryManager.get(dbname)
174         with registry.cursor() as cr:
175             m = registry.get('ir.module.module')
176             # TODO The following code should move to ir.module.module.list_installed_modules()
177             domain = [('state','=','installed'), ('name','in', loadable)]
178             ids = m.search(cr, 1, [('state','=','installed'), ('name','in', loadable)])
179             for module in m.read(cr, 1, ids, ['name', 'dependencies_id']):
180                 modules[module['name']] = []
181                 deps = module.get('dependencies_id')
182                 if deps:
183                     deps_read = registry.get('ir.module.module.dependency').read(cr, 1, deps, ['name'])
184                     dependencies = [i['name'] for i in deps_read]
185                     modules[module['name']] = dependencies
186     except Exception,e:
187         pass
188     sorted_modules = module_topological_sort(modules)
189     return sorted_modules
190
191 def module_boot(req, db=None):
192     server_wide_modules = openerp.conf.server_wide_modules or ['web']
193     serverside = []
194     dbside = []
195     for i in server_wide_modules:
196         if i in openerpweb.addons_manifest:
197             serverside.append(i)
198     monodb = db or db_monodb(req)
199     if monodb:
200         dbside = module_installed_bypass_session(monodb)
201         dbside = [i for i in dbside if i not in serverside]
202     addons = serverside + dbside
203     return addons
204
205 def concat_xml(file_list):
206     """Concatenate xml files
207
208     :param list(str) file_list: list of files to check
209     :returns: (concatenation_result, checksum)
210     :rtype: (str, str)
211     """
212     checksum = hashlib.new('sha1')
213     if not file_list:
214         return '', checksum.hexdigest()
215
216     root = None
217     for fname in file_list:
218         with open(fname, 'rb') as fp:
219             contents = fp.read()
220             checksum.update(contents)
221             fp.seek(0)
222             xml = ElementTree.parse(fp).getroot()
223
224         if root is None:
225             root = ElementTree.Element(xml.tag)
226         #elif root.tag != xml.tag:
227         #    raise ValueError("Root tags missmatch: %r != %r" % (root.tag, xml.tag))
228
229         for child in xml.getchildren():
230             root.append(child)
231     return ElementTree.tostring(root, 'utf-8'), checksum.hexdigest()
232
233 def concat_files(file_list, reader=None, intersperse=""):
234     """ Concatenates contents of all provided files
235
236     :param list(str) file_list: list of files to check
237     :param function reader: reading procedure for each file
238     :param str intersperse: string to intersperse between file contents
239     :returns: (concatenation_result, checksum)
240     :rtype: (str, str)
241     """
242     checksum = hashlib.new('sha1')
243     if not file_list:
244         return '', checksum.hexdigest()
245
246     if reader is None:
247         def reader(f):
248             with open(f, 'rb') as fp:
249                 return fp.read()
250
251     files_content = []
252     for fname in file_list:
253         contents = reader(fname)
254         checksum.update(contents)
255         files_content.append(contents)
256
257     files_concat = intersperse.join(files_content)
258     return files_concat, checksum.hexdigest()
259
260 concat_js_cache = {}
261
262 def concat_js(file_list):
263     content, checksum = concat_files(file_list, intersperse=';')
264     if checksum in concat_js_cache:
265         content = concat_js_cache[checksum]
266     else:
267         content = rjsmin(content)
268         concat_js_cache[checksum] = content
269     return content, checksum
270
271 def fs2web(path):
272     """convert FS path into web path"""
273     return '/'.join(path.split(os.path.sep))
274
275 def manifest_glob(req, extension, addons=None, db=None):
276     if addons is None:
277         addons = module_boot(req, db=db)
278     else:
279         addons = addons.split(',')
280     r = []
281     for addon in addons:
282         manifest = openerpweb.addons_manifest.get(addon, None)
283         if not manifest:
284             continue
285         # ensure does not ends with /
286         addons_path = os.path.join(manifest['addons_path'], '')[:-1]
287         globlist = manifest.get(extension, [])
288         for pattern in globlist:
289             for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
290                 r.append((path, fs2web(path[len(addons_path):])))
291     return r
292
293 def manifest_list(req, extension, mods=None, db=None):
294     if not req.debug:
295         path = '/web/webclient/' + extension
296         if mods is not None:
297             path += '?mods=' + mods
298         elif db:
299             path += '?db=' + db
300         return [path]
301     files = manifest_glob(req, extension, addons=mods, db=db)
302     i_am_diabetic = req.httprequest.environ["QUERY_STRING"].count("no_sugar") >= 1 or \
303                     req.httprequest.environ.get('HTTP_REFERER', '').count("no_sugar") >= 1
304     if i_am_diabetic:
305         return [wp for _fp, wp in files]
306     else:
307         return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in files]
308
309 def get_last_modified(files):
310     """ Returns the modification time of the most recently modified
311     file provided
312
313     :param list(str) files: names of files to check
314     :return: most recent modification time amongst the fileset
315     :rtype: datetime.datetime
316     """
317     files = list(files)
318     if files:
319         return max(datetime.datetime.fromtimestamp(os.path.getmtime(f))
320                    for f in files)
321     return datetime.datetime(1970, 1, 1)
322
323 def make_conditional(req, response, last_modified=None, etag=None):
324     """ Makes the provided response conditional based upon the request,
325     and mandates revalidation from clients
326
327     Uses Werkzeug's own :meth:`ETagResponseMixin.make_conditional`, after
328     setting ``last_modified`` and ``etag`` correctly on the response object
329
330     :param req: OpenERP request
331     :type req: web.common.http.WebRequest
332     :param response: Werkzeug response
333     :type response: werkzeug.wrappers.Response
334     :param datetime.datetime last_modified: last modification date of the response content
335     :param str etag: some sort of checksum of the content (deep etag)
336     :return: the response object provided
337     :rtype: werkzeug.wrappers.Response
338     """
339     response.cache_control.must_revalidate = True
340     response.cache_control.max_age = 0
341     if last_modified:
342         response.last_modified = last_modified
343     if etag:
344         response.set_etag(etag)
345     return response.make_conditional(req.httprequest)
346
347 def login_and_redirect(req, db, login, key, redirect_url='/'):
348     wsgienv = req.httprequest.environ
349     env = dict(
350         base_location=req.httprequest.url_root.rstrip('/'),
351         HTTP_HOST=wsgienv['HTTP_HOST'],
352         REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
353     )
354     req.session.authenticate(db, login, key, env)
355     return set_cookie_and_redirect(req, redirect_url)
356
357 def set_cookie_and_redirect(req, redirect_url):
358     redirect = werkzeug.utils.redirect(redirect_url, 303)
359     redirect.autocorrect_location_header = False
360     cookie_val = urllib2.quote(simplejson.dumps(req.session_id))
361     redirect.set_cookie('instance0|session_id', cookie_val)
362     return redirect
363
364 def eval_context_and_domain(session, context, domain=None):
365     e_context = session.eval_context(context)
366     # should we give the evaluated context as an evaluation context to the domain?
367     e_domain = session.eval_domain(domain or [])
368
369     return e_context, e_domain
370
371 def load_actions_from_ir_values(req, key, key2, models, meta):
372     context = req.session.eval_context(req.context)
373     Values = req.session.model('ir.values')
374     actions = Values.get(key, key2, models, meta, context)
375
376     return [(id, name, clean_action(req, action, context))
377             for id, name, action in actions]
378
379 def clean_action(req, action, context, do_not_eval=False):
380     action.setdefault('flags', {})
381
382     context = context or {}
383     eval_ctx = req.session.evaluation_context(context)
384
385     if not do_not_eval:
386         # values come from the server, we can just eval them
387         if action.get('context') and isinstance(action.get('context'), basestring):
388             action['context'] = eval( action['context'], eval_ctx ) or {}
389
390         if action.get('domain') and isinstance(action.get('domain'), basestring):
391             action['domain'] = eval( action['domain'], eval_ctx ) or []
392     else:
393         if 'context' in action:
394             action['context'] = parse_context(action['context'], req.session)
395         if 'domain' in action:
396             action['domain'] = parse_domain(action['domain'], req.session)
397
398     action_type = action.setdefault('type', 'ir.actions.act_window_close')
399     if action_type == 'ir.actions.act_window':
400         return fix_view_modes(action)
401     return action
402
403 # I think generate_views,fix_view_modes should go into js ActionManager
404 def generate_views(action):
405     """
406     While the server generates a sequence called "views" computing dependencies
407     between a bunch of stuff for views coming directly from the database
408     (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
409     to return custom view dictionaries generated on the fly.
410
411     In that case, there is no ``views`` key available on the action.
412
413     Since the web client relies on ``action['views']``, generate it here from
414     ``view_mode`` and ``view_id``.
415
416     Currently handles two different cases:
417
418     * no view_id, multiple view_mode
419     * single view_id, single view_mode
420
421     :param dict action: action descriptor dictionary to generate a views key for
422     """
423     view_id = action.get('view_id') or False
424     if isinstance(view_id, (list, tuple)):
425         view_id = view_id[0]
426
427     # providing at least one view mode is a requirement, not an option
428     view_modes = action['view_mode'].split(',')
429
430     if len(view_modes) > 1:
431         if view_id:
432             raise ValueError('Non-db action dictionaries should provide '
433                              'either multiple view modes or a single view '
434                              'mode and an optional view id.\n\n Got view '
435                              'modes %r and view id %r for action %r' % (
436                 view_modes, view_id, action))
437         action['views'] = [(False, mode) for mode in view_modes]
438         return
439     action['views'] = [(view_id, view_modes[0])]
440
441 def fix_view_modes(action):
442     """ For historical reasons, OpenERP has weird dealings in relation to
443     view_mode and the view_type attribute (on window actions):
444
445     * one of the view modes is ``tree``, which stands for both list views
446       and tree views
447     * the choice is made by checking ``view_type``, which is either
448       ``form`` for a list view or ``tree`` for an actual tree view
449
450     This methods simply folds the view_type into view_mode by adding a
451     new view mode ``list`` which is the result of the ``tree`` view_mode
452     in conjunction with the ``form`` view_type.
453
454     TODO: this should go into the doc, some kind of "peculiarities" section
455
456     :param dict action: an action descriptor
457     :returns: nothing, the action is modified in place
458     """
459     if not action.get('views'):
460         generate_views(action)
461
462     if action.pop('view_type', 'form') != 'form':
463         return action
464
465     if 'view_mode' in action:
466         action['view_mode'] = ','.join(
467             mode if mode != 'tree' else 'list'
468             for mode in action['view_mode'].split(','))
469     action['views'] = [
470         [id, mode if mode != 'tree' else 'list']
471         for id, mode in action['views']
472     ]
473
474     return action
475
476 def parse_domain(domain, session):
477     """ Parses an arbitrary string containing a domain, transforms it
478     to either a literal domain or a :class:`nonliterals.Domain`
479
480     :param domain: the domain to parse, if the domain is not a string it
481                    is assumed to be a literal domain and is returned as-is
482     :param session: Current OpenERP session
483     :type session: openerpweb.OpenERPSession
484     """
485     if not isinstance(domain, basestring):
486         return domain
487     try:
488         return ast.literal_eval(domain)
489     except ValueError:
490         # not a literal
491         return nonliterals.Domain(session, domain)
492
493 def parse_context(context, session):
494     """ Parses an arbitrary string containing a context, transforms it
495     to either a literal context or a :class:`nonliterals.Context`
496
497     :param context: the context to parse, if the context is not a string it
498            is assumed to be a literal domain and is returned as-is
499     :param session: Current OpenERP session
500     :type session: openerpweb.OpenERPSession
501     """
502     if not isinstance(context, basestring):
503         return context
504     try:
505         return ast.literal_eval(context)
506     except ValueError:
507         return nonliterals.Context(session, context)
508
509 def _local_web_translations(trans_file):
510     messages = []
511     try:
512         with open(trans_file) as t_file:
513             po = babel.messages.pofile.read_po(t_file)
514     except Exception:
515         return
516     for x in po:
517         if x.id and x.string and "openerp-web" in x.auto_comments:
518             messages.append({'id': x.id, 'string': x.string})
519     return messages
520
521 def xml2json_from_elementtree(el, preserve_whitespaces=False):
522     """ xml2json-direct
523     Simple and straightforward XML-to-JSON converter in Python
524     New BSD Licensed
525     http://code.google.com/p/xml2json-direct/
526     """
527     res = {}
528     if el.tag[0] == "{":
529         ns, name = el.tag.rsplit("}", 1)
530         res["tag"] = name
531         res["namespace"] = ns[1:]
532     else:
533         res["tag"] = el.tag
534     res["attrs"] = {}
535     for k, v in el.items():
536         res["attrs"][k] = v
537     kids = []
538     if el.text and (preserve_whitespaces or el.text.strip() != ''):
539         kids.append(el.text)
540     for kid in el:
541         kids.append(xml2json_from_elementtree(kid, preserve_whitespaces))
542         if kid.tail and (preserve_whitespaces or kid.tail.strip() != ''):
543             kids.append(kid.tail)
544     res["children"] = kids
545     return res
546
547 def content_disposition(filename, req):
548     filename = filename.encode('utf8')
549     escaped = urllib2.quote(filename)
550     browser = req.httprequest.user_agent.browser
551     version = int((req.httprequest.user_agent.version or '0').split('.')[0])
552     if browser == 'msie' and version < 9:
553         return "attachment; filename=%s" % escaped
554     elif browser == 'safari':
555         return "attachment; filename=%s" % filename
556     else:
557         return "attachment; filename*=UTF-8''%s" % escaped
558
559
560 #----------------------------------------------------------
561 # OpenERP Web web Controllers
562 #----------------------------------------------------------
563
564 html_template = """<!DOCTYPE html>
565 <html style="height: 100%%">
566     <head>
567         <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
568         <meta http-equiv="content-type" content="text/html; charset=utf-8" />
569         <title>OpenERP</title>
570         <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
571         <link rel="stylesheet" href="/web/static/src/css/full.css" />
572         %(css)s
573         %(js)s
574         <script type="text/javascript">
575             $(function() {
576                 var s = new openerp.init(%(modules)s);
577                 %(init)s
578             });
579         </script>
580     </head>
581     <body>
582         <!--[if lte IE 8]>
583         <script src="http://ajax.googleapis.com/ajax/libs/chrome-frame/1/CFInstall.min.js"></script>
584         <script>
585             var test = function() {
586                 CFInstall.check({
587                     mode: "overlay"
588                 });
589             };
590             if (window.localStorage && false) {
591                 if (! localStorage.getItem("hasShownGFramePopup")) {
592                     test();
593                     localStorage.setItem("hasShownGFramePopup", true);
594                 }
595             } else {
596                 test();
597             }
598         </script>
599         <![endif]-->
600     </body>
601 </html>
602 """
603
604 class Home(openerpweb.Controller):
605     _cp_path = '/'
606
607     @openerpweb.httprequest
608     def index(self, req, s_action=None, db=None, **kw):
609         js = "\n        ".join('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list(req, 'js', db=db))
610         css = "\n        ".join('<link rel="stylesheet" href="%s">' % i for i in manifest_list(req, 'css', db=db))
611
612         r = html_template % {
613             'js': js,
614             'css': css,
615             'modules': simplejson.dumps(module_boot(req, db=db)),
616             'init': 'var wc = new s.web.WebClient();wc.appendTo($(document.body));'
617         }
618         return r
619
620     @openerpweb.httprequest
621     def login(self, req, db, login, key):
622         return login_and_redirect(req, db, login, key)
623
624 class WebClient(openerpweb.Controller):
625     _cp_path = "/web/webclient"
626
627     @openerpweb.jsonrequest
628     def csslist(self, req, mods=None):
629         return manifest_list(req, 'css', mods=mods)
630
631     @openerpweb.jsonrequest
632     def jslist(self, req, mods=None):
633         return manifest_list(req, 'js', mods=mods)
634
635     @openerpweb.jsonrequest
636     def qweblist(self, req, mods=None):
637         return manifest_list(req, 'qweb', mods=mods)
638
639     @openerpweb.httprequest
640     def css(self, req, mods=None, db=None):
641         files = list(manifest_glob(req, 'css', addons=mods, db=db))
642         last_modified = get_last_modified(f[0] for f in files)
643         if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
644             return werkzeug.wrappers.Response(status=304)
645
646         file_map = dict(files)
647
648         rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
649         rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
650
651         def reader(f):
652             """read the a css file and absolutify all relative uris"""
653             with open(f, 'rb') as fp:
654                 data = fp.read().decode('utf-8')
655
656             path = file_map[f]
657             web_dir = os.path.dirname(path)
658
659             data = re.sub(
660                 rx_import,
661                 r"""@import \1%s/""" % (web_dir,),
662                 data,
663             )
664
665             data = re.sub(
666                 rx_url,
667                 r"""url(\1%s/""" % (web_dir,),
668                 data,
669             )
670             return data.encode('utf-8')
671
672         content, checksum = concat_files((f[0] for f in files), reader)
673
674         return make_conditional(
675             req, req.make_response(content, [('Content-Type', 'text/css')]),
676             last_modified, checksum)
677
678     @openerpweb.httprequest
679     def js(self, req, mods=None, db=None):
680         files = [f[0] for f in manifest_glob(req, 'js', addons=mods, db=db)]
681         last_modified = get_last_modified(files)
682         if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
683             return werkzeug.wrappers.Response(status=304)
684
685         content, checksum = concat_js(files)
686
687         return make_conditional(
688             req, req.make_response(content, [('Content-Type', 'application/javascript')]),
689             last_modified, checksum)
690
691     @openerpweb.httprequest
692     def qweb(self, req, mods=None, db=None):
693         files = [f[0] for f in manifest_glob(req, 'qweb', addons=mods, db=db)]
694         last_modified = get_last_modified(files)
695         if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
696             return werkzeug.wrappers.Response(status=304)
697
698         content, checksum = concat_xml(files)
699
700         return make_conditional(
701             req, req.make_response(content, [('Content-Type', 'text/xml')]),
702             last_modified, checksum)
703
704     @openerpweb.jsonrequest
705     def bootstrap_translations(self, req, mods):
706         """ Load local translations from *.po files, as a temporary solution
707             until we have established a valid session. This is meant only
708             for translating the login page and db management chrome, using
709             the browser's language. """
710         # For performance reasons we only load a single translation, so for
711         # sub-languages (that should only be partially translated) we load the
712         # main language PO instead - that should be enough for the login screen.
713         lang = req.lang.split('_')[0]
714
715         translations_per_module = {}
716         for addon_name in mods:
717             if openerpweb.addons_manifest[addon_name].get('bootstrap'):
718                 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
719                 f_name = os.path.join(addons_path, addon_name, "i18n", lang + ".po")
720                 if not os.path.exists(f_name):
721                     continue
722                 translations_per_module[addon_name] = {'messages': _local_web_translations(f_name)}
723
724         return {"modules": translations_per_module,
725                 "lang_parameters": None}
726
727     @openerpweb.jsonrequest
728     def translations(self, req, mods, lang):
729         res_lang = req.session.model('res.lang')
730         ids = res_lang.search([("code", "=", lang)])
731         lang_params = None
732         if ids:
733             lang_params = res_lang.read(ids[0], ["direction", "date_format", "time_format",
734                                                 "grouping", "decimal_point", "thousands_sep"])
735
736         # Regional languages (ll_CC) must inherit/override their parent lang (ll), but this is
737         # done server-side when the language is loaded, so we only need to load the user's lang.
738         ir_translation = req.session.model('ir.translation')
739         translations_per_module = {}
740         messages = ir_translation.search_read([('module','in',mods),('lang','=',lang),
741                                                ('comments','like','openerp-web'),('value','!=',False),
742                                                ('value','!=','')],
743                                               ['module','src','value','lang'], order='module') 
744         for mod, msg_group in itertools.groupby(messages, key=operator.itemgetter('module')):
745             translations_per_module.setdefault(mod,{'messages':[]})
746             translations_per_module[mod]['messages'].extend({'id': m['src'],
747                                                              'string': m['value']} \
748                                                                 for m in msg_group)
749         return {"modules": translations_per_module,
750                 "lang_parameters": lang_params}
751
752     @openerpweb.jsonrequest
753     def version_info(self, req):
754         return {
755             "version": openerp.release.version
756         }
757
758 class Proxy(openerpweb.Controller):
759     _cp_path = '/web/proxy'
760
761     @openerpweb.jsonrequest
762     def load(self, req, path):
763         """ Proxies an HTTP request through a JSON request.
764
765         It is strongly recommended to not request binary files through this,
766         as the result will be a binary data blob as well.
767
768         :param req: OpenERP request
769         :param path: actual request path
770         :return: file content
771         """
772         from werkzeug.test import Client
773         from werkzeug.wrappers import BaseResponse
774
775         return Client(req.httprequest.app, BaseResponse).get(path).data
776
777 class Database(openerpweb.Controller):
778     _cp_path = "/web/database"
779
780     @openerpweb.jsonrequest
781     def get_list(self, req):
782         return db_list(req)
783
784     @openerpweb.jsonrequest
785     def create(self, req, fields):
786         params = dict(map(operator.itemgetter('name', 'value'), fields))
787         return req.session.proxy("db").create_database(
788             params['super_admin_pwd'],
789             params['db_name'],
790             bool(params.get('demo_data')),
791             params['db_lang'],
792             params['create_admin_pwd'])
793
794     @openerpweb.jsonrequest
795     def duplicate(self, req, fields):
796         params = dict(map(operator.itemgetter('name', 'value'), fields))
797         return req.session.proxy("db").duplicate_database(
798             params['super_admin_pwd'],
799             params['db_original_name'],
800             params['db_name'])
801
802     @openerpweb.jsonrequest
803     def duplicate(self, req, fields):
804         params = dict(map(operator.itemgetter('name', 'value'), fields))
805         duplicate_attrs = (
806             params['super_admin_pwd'],
807             params['db_original_name'],
808             params['db_name'],
809         )
810
811         return req.session.proxy("db").duplicate_database(*duplicate_attrs)
812
813     @openerpweb.jsonrequest
814     def drop(self, req, fields):
815         password, db = operator.itemgetter(
816             'drop_pwd', 'drop_db')(
817                 dict(map(operator.itemgetter('name', 'value'), fields)))
818
819         try:
820             return req.session.proxy("db").drop(password, db)
821         except xmlrpclib.Fault, e:
822             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
823                 return {'error': e.faultCode, 'title': 'Drop Database'}
824         return {'error': 'Could not drop database !', 'title': 'Drop Database'}
825
826     @openerpweb.httprequest
827     def backup(self, req, backup_db, backup_pwd, token):
828         try:
829             db_dump = base64.b64decode(
830                 req.session.proxy("db").dump(backup_pwd, backup_db))
831             filename = "%(db)s_%(timestamp)s.dump" % {
832                 'db': backup_db,
833                 'timestamp': datetime.datetime.utcnow().strftime(
834                     "%Y-%m-%d_%H-%M-%SZ")
835             }
836             return req.make_response(db_dump,
837                [('Content-Type', 'application/octet-stream; charset=binary'),
838                ('Content-Disposition', content_disposition(filename, req))],
839                {'fileToken': int(token)}
840             )
841         except xmlrpclib.Fault, e:
842             return simplejson.dumps([[],[{'error': e.faultCode, 'title': 'backup Database'}]])
843
844     @openerpweb.httprequest
845     def restore(self, req, db_file, restore_pwd, new_db):
846         try:
847             data = base64.b64encode(db_file.read())
848             req.session.proxy("db").restore(restore_pwd, new_db, data)
849             return ''
850         except xmlrpclib.Fault, e:
851             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
852                 raise Exception("AccessDenied")
853
854     @openerpweb.jsonrequest
855     def change_password(self, req, fields):
856         old_password, new_password = operator.itemgetter(
857             'old_pwd', 'new_pwd')(
858                 dict(map(operator.itemgetter('name', 'value'), fields)))
859         try:
860             return req.session.proxy("db").change_admin_password(old_password, new_password)
861         except xmlrpclib.Fault, e:
862             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
863                 return {'error': e.faultCode, 'title': 'Change Password'}
864         return {'error': 'Error, password not changed !', 'title': 'Change Password'}
865
866 class Session(openerpweb.Controller):
867     _cp_path = "/web/session"
868
869     def session_info(self, req):
870         req.session.ensure_valid()
871         return {
872             "session_id": req.session_id,
873             "uid": req.session._uid,
874             "context": req.session.get_context() if req.session._uid else {},
875             "db": req.session._db,
876             "login": req.session._login,
877         }
878
879     @openerpweb.jsonrequest
880     def get_session_info(self, req):
881         return self.session_info(req)
882
883     @openerpweb.jsonrequest
884     def authenticate(self, req, db, login, password, base_location=None):
885         wsgienv = req.httprequest.environ
886         env = dict(
887             base_location=base_location,
888             HTTP_HOST=wsgienv['HTTP_HOST'],
889             REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
890         )
891         req.session.authenticate(db, login, password, env)
892
893         return self.session_info(req)
894
895     @openerpweb.jsonrequest
896     def change_password (self,req,fields):
897         old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
898                 dict(map(operator.itemgetter('name', 'value'), fields)))
899         if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
900             return {'error':'You cannot leave any password empty.','title': 'Change Password'}
901         if new_password != confirm_password:
902             return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
903         try:
904             if req.session.model('res.users').change_password(
905                 old_password, new_password):
906                 return {'new_password':new_password}
907         except Exception:
908             return {'error': 'The old password you provided is incorrect, your password was not changed.', 'title': 'Change Password'}
909         return {'error': 'Error, password not changed !', 'title': 'Change Password'}
910
911     @openerpweb.jsonrequest
912     def sc_list(self, req):
913         return req.session.model('ir.ui.view_sc').get_sc(
914             req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
915
916     @openerpweb.jsonrequest
917     def get_lang_list(self, req):
918         try:
919             return req.session.proxy("db").list_lang() or []
920         except Exception, e:
921             return {"error": e, "title": "Languages"}
922
923     @openerpweb.jsonrequest
924     def modules(self, req):
925         # return all installed modules. Web client is smart enough to not load a module twice
926         return module_installed(req)
927
928     @openerpweb.jsonrequest
929     def eval_domain_and_context(self, req, contexts, domains,
930                                 group_by_seq=None):
931         """ Evaluates sequences of domains and contexts, composing them into
932         a single context, domain or group_by sequence.
933
934         :param list contexts: list of contexts to merge together. Contexts are
935                               evaluated in sequence, all previous contexts
936                               are part of their own evaluation context
937                               (starting at the session context).
938         :param list domains: list of domains to merge together. Domains are
939                              evaluated in sequence and appended to one another
940                              (implicit AND), their evaluation domain is the
941                              result of merging all contexts.
942         :param list group_by_seq: list of domains (which may be in a different
943                                   order than the ``contexts`` parameter),
944                                   evaluated in sequence, their ``'group_by'``
945                                   key is extracted if they have one.
946         :returns:
947             a 3-dict of:
948
949             context (``dict``)
950                 the global context created by merging all of
951                 ``contexts``
952
953             domain (``list``)
954                 the concatenation of all domains
955
956             group_by (``list``)
957                 a list of fields to group by, potentially empty (in which case
958                 no group by should be performed)
959         """
960         context, domain = eval_context_and_domain(req.session,
961                                                   nonliterals.CompoundContext(*(contexts or [])),
962                                                   nonliterals.CompoundDomain(*(domains or [])))
963
964         group_by_sequence = []
965         for candidate in (group_by_seq or []):
966             ctx = req.session.eval_context(candidate, context)
967             group_by = ctx.get('group_by')
968             if not group_by:
969                 continue
970             elif isinstance(group_by, basestring):
971                 group_by_sequence.append(group_by)
972             else:
973                 group_by_sequence.extend(group_by)
974
975         return {
976             'context': context,
977             'domain': domain,
978             'group_by': group_by_sequence
979         }
980
981     @openerpweb.jsonrequest
982     def save_session_action(self, req, the_action):
983         """
984         This method store an action object in the session object and returns an integer
985         identifying that action. The method get_session_action() can be used to get
986         back the action.
987
988         :param the_action: The action to save in the session.
989         :type the_action: anything
990         :return: A key identifying the saved action.
991         :rtype: integer
992         """
993         saved_actions = req.httpsession.get('saved_actions')
994         if not saved_actions:
995             saved_actions = {"next":0, "actions":{}}
996             req.httpsession['saved_actions'] = saved_actions
997         # we don't allow more than 10 stored actions
998         if len(saved_actions["actions"]) >= 10:
999             del saved_actions["actions"][min(saved_actions["actions"])]
1000         key = saved_actions["next"]
1001         saved_actions["actions"][key] = the_action
1002         saved_actions["next"] = key + 1
1003         return key
1004
1005     @openerpweb.jsonrequest
1006     def get_session_action(self, req, key):
1007         """
1008         Gets back a previously saved action. This method can return None if the action
1009         was saved since too much time (this case should be handled in a smart way).
1010
1011         :param key: The key given by save_session_action()
1012         :type key: integer
1013         :return: The saved action or None.
1014         :rtype: anything
1015         """
1016         saved_actions = req.httpsession.get('saved_actions')
1017         if not saved_actions:
1018             return None
1019         return saved_actions["actions"].get(key)
1020
1021     @openerpweb.jsonrequest
1022     def check(self, req):
1023         req.session.assert_valid()
1024         return None
1025
1026     @openerpweb.jsonrequest
1027     def destroy(self, req):
1028         req.session._suicide = True
1029
1030 class Menu(openerpweb.Controller):
1031     _cp_path = "/web/menu"
1032
1033     @openerpweb.jsonrequest
1034     def load(self, req):
1035         return {'data': self.do_load(req)}
1036
1037     @openerpweb.jsonrequest
1038     def load_needaction(self, req, menu_ids):
1039         return {'data': self.do_load_needaction(req, menu_ids)}
1040
1041     @openerpweb.jsonrequest
1042     def get_user_roots(self, req):
1043         return self.do_get_user_roots(req)
1044
1045     def do_get_user_roots(self, req):
1046         """ Return all root menu ids visible for the session user.
1047
1048         :param req: A request object, with an OpenERP session attribute
1049         :type req: < session -> OpenERPSession >
1050         :return: the root menu ids
1051         :rtype: list(int)
1052         """
1053         s = req.session
1054         context = s.eval_context(req.context)
1055         Menus = s.model('ir.ui.menu')
1056         # If a menu action is defined use its domain to get the root menu items
1057         user_menu_id = s.model('res.users').read([s._uid], ['menu_id'], context)[0]['menu_id']
1058
1059         menu_domain = [('parent_id', '=', False)]
1060         if user_menu_id:
1061             domain_string = s.model('ir.actions.act_window').read([user_menu_id[0]], ['domain'], context)[0]['domain']
1062             if domain_string:
1063                 menu_domain = ast.literal_eval(domain_string)
1064
1065         return Menus.search(menu_domain, 0, False, False, context)
1066
1067     def do_load(self, req):
1068         """ Loads all menu items (all applications and their sub-menus).
1069
1070         :param req: A request object, with an OpenERP session attribute
1071         :type req: < session -> OpenERPSession >
1072         :return: the menu root
1073         :rtype: dict('children': menu_nodes)
1074         """
1075         context = req.session.eval_context(req.context)
1076         Menus = req.session.model('ir.ui.menu')
1077
1078         menu_roots = Menus.read(self.do_get_user_roots(req), ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled'], context)
1079         menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children': menu_roots}
1080
1081         # menus are loaded fully unlike a regular tree view, cause there are a
1082         # limited number of items (752 when all 6.1 addons are installed)
1083         menu_ids = Menus.search([], 0, False, False, context)
1084         menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled'], context)
1085         # adds roots at the end of the sequence, so that they will overwrite
1086         # equivalent menu items from full menu read when put into id:item
1087         # mapping, resulting in children being correctly set on the roots.
1088         menu_items.extend(menu_roots)
1089
1090         # make a tree using parent_id
1091         menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
1092         for menu_item in menu_items:
1093             if menu_item['parent_id']:
1094                 parent = menu_item['parent_id'][0]
1095             else:
1096                 parent = False
1097             if parent in menu_items_map:
1098                 menu_items_map[parent].setdefault(
1099                     'children', []).append(menu_item)
1100
1101         # sort by sequence a tree using parent_id
1102         for menu_item in menu_items:
1103             menu_item.setdefault('children', []).sort(
1104                 key=operator.itemgetter('sequence'))
1105
1106         return menu_root
1107
1108     def do_load_needaction(self, req, menu_ids=False):
1109         """ Loads needaction counters for all or some specific menu ids.
1110
1111             :param req: A request object, with an OpenERP session attribute
1112             :type req: < session -> OpenERPSession >
1113             :return: the menu root
1114             :rtype: dict('children': menu_nodes)
1115         """
1116         context = req.session.eval_context(req.context)
1117         Menus = req.session.model('ir.ui.menu')
1118
1119         if menu_ids == False:
1120             menu_ids = Menus.search([], context=context)
1121             # TDE FIXME: set needaction_enabled column to store, to enable this more limited search
1122             # menu_ids = Menus.search([('needaction_enabled', '=', True)], context=context)
1123
1124         menu_needaction_data = Menus.get_needaction_data(menu_ids, context)
1125         return menu_needaction_data
1126
1127     @openerpweb.jsonrequest
1128     def action(self, req, menu_id):
1129         actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
1130                                              [('ir.ui.menu', menu_id)], False)
1131         return {"action": actions}
1132
1133 class DataSet(openerpweb.Controller):
1134     _cp_path = "/web/dataset"
1135
1136     @openerpweb.jsonrequest
1137     def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
1138         return self.do_search_read(req, model, fields, offset, limit, domain, sort)
1139     def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
1140                        , sort=None):
1141         """ Performs a search() followed by a read() (if needed) using the
1142         provided search criteria
1143
1144         :param req: a JSON-RPC request object
1145         :type req: openerpweb.JsonRequest
1146         :param str model: the name of the model to search on
1147         :param fields: a list of the fields to return in the result records
1148         :type fields: [str]
1149         :param int offset: from which index should the results start being returned
1150         :param int limit: the maximum number of records to return
1151         :param list domain: the search domain for the query
1152         :param list sort: sorting directives
1153         :returns: A structure (dict) with two keys: ids (all the ids matching
1154                   the (domain, context) pair) and records (paginated records
1155                   matching fields selection set)
1156         :rtype: list
1157         """
1158         Model = req.session.model(model)
1159
1160         context, domain = eval_context_and_domain(
1161             req.session, req.context, domain)
1162
1163         ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
1164         if limit and len(ids) == limit:
1165             length = Model.search_count(domain, context)
1166         else:
1167             length = len(ids) + (offset or 0)
1168         if fields and fields == ['id']:
1169             # shortcut read if we only want the ids
1170             return {
1171                 'length': length,
1172                 'records': [{'id': id} for id in ids]
1173             }
1174
1175         records = Model.read(ids, fields or False, context)
1176         records.sort(key=lambda obj: ids.index(obj['id']))
1177         return {
1178             'length': length,
1179             'records': records
1180         }
1181
1182     @openerpweb.jsonrequest
1183     def load(self, req, model, id, fields):
1184         m = req.session.model(model)
1185         value = {}
1186         r = m.read([id], False, req.session.eval_context(req.context))
1187         if r:
1188             value = r[0]
1189         return {'value': value}
1190
1191     def call_common(self, req, model, method, args, domain_id=None, context_id=None):
1192         has_domain = domain_id is not None and domain_id < len(args)
1193         has_context = context_id is not None and context_id < len(args)
1194
1195         domain = args[domain_id] if has_domain else []
1196         context = args[context_id] if has_context else {}
1197         c, d = eval_context_and_domain(req.session, context, domain)
1198         if has_domain:
1199             args[domain_id] = d
1200         if has_context:
1201             args[context_id] = c
1202
1203         return self._call_kw(req, model, method, args, {})
1204     
1205     def _call_kw(self, req, model, method, args, kwargs):
1206         for i in xrange(len(args)):
1207             if isinstance(args[i], nonliterals.BaseContext):
1208                 args[i] = req.session.eval_context(args[i])
1209             elif isinstance(args[i], nonliterals.BaseDomain):
1210                 args[i] = req.session.eval_domain(args[i])
1211         for k in kwargs.keys():
1212             if isinstance(kwargs[k], nonliterals.BaseContext):
1213                 kwargs[k] = req.session.eval_context(kwargs[k])
1214             elif isinstance(kwargs[k], nonliterals.BaseDomain):
1215                 kwargs[k] = req.session.eval_domain(kwargs[k])
1216
1217         # Temporary implements future display_name special field for model#read()
1218         if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
1219             if 'display_name' in args[1]:
1220                 names = req.session.model(model).name_get(args[0], **kwargs)
1221                 args[1].remove('display_name')
1222                 r = getattr(req.session.model(model), method)(*args, **kwargs)
1223                 for i in range(len(r)):
1224                     r[i]['display_name'] = names[i][1] or "%s#%d" % (model, names[i][0])
1225                 return r
1226
1227         return getattr(req.session.model(model), method)(*args, **kwargs)
1228
1229     @openerpweb.jsonrequest
1230     def onchange(self, req, model, method, args, context_id=None):
1231         """ Support method for handling onchange calls: behaves much like call
1232         with the following differences:
1233
1234         * Does not take a domain_id
1235         * Is aware of the return value's structure, and will parse the domains
1236           if needed in order to return either parsed literal domains (in JSON)
1237           or non-literal domain instances, allowing those domains to be used
1238           from JS
1239
1240         :param req:
1241         :type req: web.common.http.JsonRequest
1242         :param str model: object type on which to call the method
1243         :param str method: name of the onchange handler method
1244         :param list args: arguments to call the onchange handler with
1245         :param int context_id: index of the context object in the list of
1246                                arguments
1247         :return: result of the onchange call with all domains parsed
1248         """
1249         result = self.call_common(req, model, method, args, context_id=context_id)
1250         if not result or 'domain' not in result:
1251             return result
1252
1253         result['domain'] = dict(
1254             (k, parse_domain(v, req.session))
1255             for k, v in result['domain'].iteritems())
1256
1257         return result
1258
1259     @openerpweb.jsonrequest
1260     def call(self, req, model, method, args, domain_id=None, context_id=None):
1261         return self.call_common(req, model, method, args, domain_id, context_id)
1262     
1263     @openerpweb.jsonrequest
1264     def call_kw(self, req, model, method, args, kwargs):
1265         return self._call_kw(req, model, method, args, kwargs)
1266
1267     @openerpweb.jsonrequest
1268     def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1269         context = req.session.eval_context(req.context)
1270         action = self.call_common(req, model, method, args, domain_id, context_id)
1271         if isinstance(action, dict) and action.get('type') != '':
1272             return clean_action(req, action, context)
1273         return False
1274
1275     @openerpweb.jsonrequest
1276     def exec_workflow(self, req, model, id, signal):
1277         return req.session.exec_workflow(model, id, signal)
1278
1279     @openerpweb.jsonrequest
1280     def resequence(self, req, model, ids, field='sequence', offset=0):
1281         """ Re-sequences a number of records in the model, by their ids
1282
1283         The re-sequencing starts at the first model of ``ids``, the sequence
1284         number is incremented by one after each record and starts at ``offset``
1285
1286         :param ids: identifiers of the records to resequence, in the new sequence order
1287         :type ids: list(id)
1288         :param str field: field used for sequence specification, defaults to
1289                           "sequence"
1290         :param int offset: sequence number for first record in ``ids``, allows
1291                            starting the resequencing from an arbitrary number,
1292                            defaults to ``0``
1293         """
1294         m = req.session.model(model)
1295         if not m.fields_get([field]):
1296             return False
1297         # python 2.6 has no start parameter
1298         for i, id in enumerate(ids):
1299             m.write(id, { field: i + offset })
1300         return True
1301
1302 class View(openerpweb.Controller):
1303     _cp_path = "/web/view"
1304
1305     def fields_view_get(self, req, model, view_id, view_type,
1306                         transform=True, toolbar=False, submenu=False):
1307         Model = req.session.model(model)
1308         context = req.session.eval_context(req.context)
1309         fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
1310         # todo fme?: check that we should pass the evaluated context here
1311         self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
1312         if toolbar and transform:
1313             self.process_toolbar(req, fvg['toolbar'])
1314         return fvg
1315
1316     def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
1317         # depending on how it feels, xmlrpclib.ServerProxy can translate
1318         # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
1319         # enjoy unicode strings which can not be trivially converted to
1320         # strings, and it blows up during parsing.
1321
1322         # So ensure we fix this retardation by converting view xml back to
1323         # bit strings.
1324         if isinstance(fvg['arch'], unicode):
1325             arch = fvg['arch'].encode('utf-8')
1326         else:
1327             arch = fvg['arch']
1328         fvg['arch_string'] = arch
1329
1330         if transform:
1331             evaluation_context = session.evaluation_context(context or {})
1332             xml = self.transform_view(arch, session, evaluation_context)
1333         else:
1334             xml = ElementTree.fromstring(arch)
1335         fvg['arch'] = xml2json_from_elementtree(xml, preserve_whitespaces)
1336
1337         if 'id' in fvg['fields']:
1338             # Special case for id's
1339             id_field = fvg['fields']['id']
1340             id_field['original_type'] = id_field['type']
1341             id_field['type'] = 'id'
1342
1343         for field in fvg['fields'].itervalues():
1344             if field.get('views'):
1345                 for view in field["views"].itervalues():
1346                     self.process_view(session, view, None, transform)
1347             if field.get('domain'):
1348                 field["domain"] = parse_domain(field["domain"], session)
1349             if field.get('context'):
1350                 field["context"] = parse_context(field["context"], session)
1351
1352     def process_toolbar(self, req, toolbar):
1353         """
1354         The toolbar is a mapping of section_key: [action_descriptor]
1355
1356         We need to clean all those actions in order to ensure correct
1357         round-tripping
1358         """
1359         for actions in toolbar.itervalues():
1360             for action in actions:
1361                 if 'context' in action:
1362                     action['context'] = parse_context(
1363                         action['context'], req.session)
1364                 if 'domain' in action:
1365                     action['domain'] = parse_domain(
1366                         action['domain'], req.session)
1367
1368     @openerpweb.jsonrequest
1369     def add_custom(self, req, view_id, arch):
1370         CustomView = req.session.model('ir.ui.view.custom')
1371         CustomView.create({
1372             'user_id': req.session._uid,
1373             'ref_id': view_id,
1374             'arch': arch
1375         }, req.session.eval_context(req.context))
1376         return {'result': True}
1377
1378     @openerpweb.jsonrequest
1379     def undo_custom(self, req, view_id, reset=False):
1380         CustomView = req.session.model('ir.ui.view.custom')
1381         context = req.session.eval_context(req.context)
1382         vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1383                                     0, False, False, context)
1384         if vcustom:
1385             if reset:
1386                 CustomView.unlink(vcustom, context)
1387             else:
1388                 CustomView.unlink([vcustom[0]], context)
1389             return {'result': True}
1390         return {'result': False}
1391
1392     def transform_view(self, view_string, session, context=None):
1393         # transform nodes on the fly via iterparse, instead of
1394         # doing it statically on the parsing result
1395         parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
1396         root = None
1397         for event, elem in parser:
1398             if event == "start":
1399                 if root is None:
1400                     root = elem
1401                 self.parse_domains_and_contexts(elem, session)
1402         return root
1403
1404     def parse_domains_and_contexts(self, elem, session):
1405         """ Converts domains and contexts from the view into Python objects,
1406         either literals if they can be parsed by literal_eval or a special
1407         placeholder object if the domain or context refers to free variables.
1408
1409         :param elem: the current node being parsed
1410         :type param: xml.etree.ElementTree.Element
1411         :param session: OpenERP session object, used to store and retrieve
1412                         non-literal objects
1413         :type session: openerpweb.openerpweb.OpenERPSession
1414         """
1415         for el in ['domain', 'filter_domain']:
1416             domain = elem.get(el, '').strip()
1417             if domain:
1418                 elem.set(el, parse_domain(domain, session))
1419                 elem.set(el + '_string', domain)
1420         for el in ['context', 'default_get']:
1421             context_string = elem.get(el, '').strip()
1422             if context_string:
1423                 elem.set(el, parse_context(context_string, session))
1424                 elem.set(el + '_string', context_string)
1425
1426     @openerpweb.jsonrequest
1427     def load(self, req, model, view_id, view_type, toolbar=False):
1428         return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1429
1430 class TreeView(View):
1431     _cp_path = "/web/treeview"
1432
1433     @openerpweb.jsonrequest
1434     def action(self, req, model, id):
1435         return load_actions_from_ir_values(
1436             req,'action', 'tree_but_open',[(model, id)],
1437             False)
1438
1439 class SearchView(View):
1440     _cp_path = "/web/searchview"
1441
1442     @openerpweb.jsonrequest
1443     def load(self, req, model, view_id):
1444         fields_view = self.fields_view_get(req, model, view_id, 'search')
1445         return {'fields_view': fields_view}
1446
1447     @openerpweb.jsonrequest
1448     def fields_get(self, req, model):
1449         Model = req.session.model(model)
1450         fields = Model.fields_get(False, req.session.eval_context(req.context))
1451         for field in fields.values():
1452             # shouldn't convert the views too?
1453             if field.get('domain'):
1454                 field["domain"] = parse_domain(field["domain"], req.session)
1455             if field.get('context'):
1456                 field["context"] = parse_context(field["context"], req.session)
1457         return {'fields': fields}
1458
1459     @openerpweb.jsonrequest
1460     def get_filters(self, req, model):
1461         logger = logging.getLogger(__name__ + '.SearchView.get_filters')
1462         Model = req.session.model("ir.filters")
1463         filters = Model.get_filters(model)
1464         for filter in filters:
1465             try:
1466                 parsed_context = parse_context(filter["context"], req.session)
1467                 filter["context"] = (parsed_context
1468                         if not isinstance(parsed_context, nonliterals.BaseContext)
1469                         else req.session.eval_context(parsed_context))
1470
1471                 parsed_domain = parse_domain(filter["domain"], req.session)
1472                 filter["domain"] = (parsed_domain
1473                         if not isinstance(parsed_domain, nonliterals.BaseDomain)
1474                         else req.session.eval_domain(parsed_domain))
1475             except Exception:
1476                 logger.exception("Failed to parse custom filter %s in %s",
1477                                  filter['name'], model)
1478                 filter['disabled'] = True
1479                 del filter['context']
1480                 del filter['domain']
1481         return filters
1482
1483 class Binary(openerpweb.Controller):
1484     _cp_path = "/web/binary"
1485
1486     @openerpweb.httprequest
1487     def image(self, req, model, id, field, **kw):
1488         last_update = '__last_update'
1489         Model = req.session.model(model)
1490         context = req.session.eval_context(req.context)
1491         headers = [('Content-Type', 'image/png')]
1492         etag = req.httprequest.headers.get('If-None-Match')
1493         hashed_session = hashlib.md5(req.session_id).hexdigest()
1494         id = None if not id else simplejson.loads(id)
1495         if type(id) is list:
1496             id = id[0] # m2o
1497         if etag:
1498             if not id and hashed_session == etag:
1499                 return werkzeug.wrappers.Response(status=304)
1500             else:
1501                 date = Model.read([id], [last_update], context)[0].get(last_update)
1502                 if hashlib.md5(date).hexdigest() == etag:
1503                     return werkzeug.wrappers.Response(status=304)
1504
1505         retag = hashed_session
1506         try:
1507             if not id:
1508                 res = Model.default_get([field], context).get(field)
1509                 image_base64 = res
1510             else:
1511                 res = Model.read([id], [last_update, field], context)[0]
1512                 retag = hashlib.md5(res.get(last_update)).hexdigest()
1513                 image_base64 = res.get(field)
1514
1515             if kw.get('resize'):
1516                 resize = kw.get('resize').split(',');
1517                 if len(resize) == 2 and int(resize[0]) and int(resize[1]):
1518                     width = int(resize[0])
1519                     height = int(resize[1])
1520                     # resize maximum 500*500
1521                     if width > 500: width = 500
1522                     if height > 500: height = 500
1523                     image_base64 = openerp.tools.image_resize_image(base64_source=image_base64, size=(width, height), encoding='base64', filetype='PNG')
1524             
1525             image_data = base64.b64decode(image_base64)
1526
1527         except (TypeError, xmlrpclib.Fault):
1528             image_data = self.placeholder(req)
1529         headers.append(('ETag', retag))
1530         headers.append(('Content-Length', len(image_data)))
1531         try:
1532             ncache = int(kw.get('cache'))
1533             headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1534         except:
1535             pass
1536         return req.make_response(image_data, headers)
1537     def placeholder(self, req):
1538         addons_path = openerpweb.addons_manifest['web']['addons_path']
1539         return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1540
1541     @openerpweb.httprequest
1542     def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1543         """ Download link for files stored as binary fields.
1544
1545         If the ``id`` parameter is omitted, fetches the default value for the
1546         binary field (via ``default_get``), otherwise fetches the field for
1547         that precise record.
1548
1549         :param req: OpenERP request
1550         :type req: :class:`web.common.http.HttpRequest`
1551         :param str model: name of the model to fetch the binary from
1552         :param str field: binary field
1553         :param str id: id of the record from which to fetch the binary
1554         :param str filename_field: field holding the file's name, if any
1555         :returns: :class:`werkzeug.wrappers.Response`
1556         """
1557         Model = req.session.model(model)
1558         context = req.session.eval_context(req.context)
1559         fields = [field]
1560         if filename_field:
1561             fields.append(filename_field)
1562         if id:
1563             res = Model.read([int(id)], fields, context)[0]
1564         else:
1565             res = Model.default_get(fields, context)
1566         filecontent = base64.b64decode(res.get(field, ''))
1567         if not filecontent:
1568             return req.not_found()
1569         else:
1570             filename = '%s_%s' % (model.replace('.', '_'), id)
1571             if filename_field:
1572                 filename = res.get(filename_field, '') or filename
1573             return req.make_response(filecontent,
1574                 [('Content-Type', 'application/octet-stream'),
1575                  ('Content-Disposition', content_disposition(filename, req))])
1576
1577     @openerpweb.httprequest
1578     def saveas_ajax(self, req, data, token):
1579         jdata = simplejson.loads(data)
1580         model = jdata['model']
1581         field = jdata['field']
1582         id = jdata.get('id', None)
1583         filename_field = jdata.get('filename_field', None)
1584         context = jdata.get('context', dict())
1585
1586         context = req.session.eval_context(context)
1587         Model = req.session.model(model)
1588         fields = [field]
1589         if filename_field:
1590             fields.append(filename_field)
1591         if id:
1592             res = Model.read([int(id)], fields, context)[0]
1593         else:
1594             res = Model.default_get(fields, context)
1595         filecontent = base64.b64decode(res.get(field, ''))
1596         if not filecontent:
1597             raise ValueError("No content found for field '%s' on '%s:%s'" %
1598                 (field, model, id))
1599         else:
1600             filename = '%s_%s' % (model.replace('.', '_'), id)
1601             if filename_field:
1602                 filename = res.get(filename_field, '') or filename
1603             return req.make_response(filecontent,
1604                 headers=[('Content-Type', 'application/octet-stream'),
1605                         ('Content-Disposition', content_disposition(filename, req))],
1606                 cookies={'fileToken': int(token)})
1607
1608     @openerpweb.httprequest
1609     def upload(self, req, callback, ufile):
1610         # TODO: might be useful to have a configuration flag for max-length file uploads
1611         try:
1612             out = """<script language="javascript" type="text/javascript">
1613                         var win = window.top.window;
1614                         win.jQuery(win).trigger(%s, %s);
1615                     </script>"""
1616             data = ufile.read()
1617             args = [len(data), ufile.filename,
1618                     ufile.content_type, base64.b64encode(data)]
1619         except Exception, e:
1620             args = [False, e.message]
1621         return out % (simplejson.dumps(callback), simplejson.dumps(args))
1622
1623     @openerpweb.httprequest
1624     def upload_attachment(self, req, callback, model, id, ufile):
1625         context = req.session.eval_context(req.context)
1626         Model = req.session.model('ir.attachment')
1627         try:
1628             out = """<script language="javascript" type="text/javascript">
1629                         var win = window.top.window;
1630                         win.jQuery(win).trigger(%s, %s);
1631                     </script>"""
1632             attachment_id = Model.create({
1633                 'name': ufile.filename,
1634                 'datas': base64.encodestring(ufile.read()),
1635                 'datas_fname': ufile.filename,
1636                 'res_model': model,
1637                 'res_id': int(id)
1638             }, context)
1639             args = {
1640                 'filename': ufile.filename,
1641                 'id':  attachment_id
1642             }
1643         except Exception,e:
1644             args = {'erorr':e.faultCode.split('--')[1],'title':e.faultCode.split('--')[0]}
1645         return out % (simplejson.dumps(callback), simplejson.dumps(args))
1646
1647 class Action(openerpweb.Controller):
1648     _cp_path = "/web/action"
1649
1650     @openerpweb.jsonrequest
1651     def load(self, req, action_id, do_not_eval=False, eval_context=None):
1652         Actions = req.session.model('ir.actions.actions')
1653         value = False
1654         context = req.session.eval_context(req.context)
1655         eval_context = req.session.eval_context(nonliterals.CompoundContext(context, eval_context or {}))
1656
1657         try:
1658             action_id = int(action_id)
1659         except ValueError:
1660             try:
1661                 module, xmlid = action_id.split('.', 1)
1662                 model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
1663                 assert model.startswith('ir.actions.')
1664             except Exception:
1665                 action_id = 0   # force failed read
1666
1667         base_action = Actions.read([action_id], ['type'], context)
1668         if base_action:
1669             ctx = {}
1670             action_type = base_action[0]['type']
1671             if action_type == 'ir.actions.report.xml':
1672                 ctx.update({'bin_size': True})
1673             ctx.update(context)
1674             action = req.session.model(action_type).read([action_id], False, ctx)
1675             if action:
1676                 value = clean_action(req, action[0], eval_context, do_not_eval)
1677         return value
1678
1679     @openerpweb.jsonrequest
1680     def run(self, req, action_id):
1681         context = req.session.eval_context(req.context)
1682         return_action = req.session.model('ir.actions.server').run(
1683             [action_id], req.session.eval_context(req.context))
1684         if return_action:
1685             return clean_action(req, return_action, context)
1686         else:
1687             return False
1688
1689 class Export(View):
1690     _cp_path = "/web/export"
1691
1692     @openerpweb.jsonrequest
1693     def formats(self, req):
1694         """ Returns all valid export formats
1695
1696         :returns: for each export format, a pair of identifier and printable name
1697         :rtype: [(str, str)]
1698         """
1699         return sorted([
1700             controller.fmt
1701             for path, controller in openerpweb.controllers_path.iteritems()
1702             if path.startswith(self._cp_path)
1703             if hasattr(controller, 'fmt')
1704         ], key=operator.itemgetter("label"))
1705
1706     def fields_get(self, req, model):
1707         Model = req.session.model(model)
1708         fields = Model.fields_get(False, req.session.eval_context(req.context))
1709         return fields
1710
1711     @openerpweb.jsonrequest
1712     def get_fields(self, req, model, prefix='', parent_name= '',
1713                    import_compat=True, parent_field_type=None,
1714                    exclude=None):
1715
1716         if import_compat and parent_field_type == "many2one":
1717             fields = {}
1718         else:
1719             fields = self.fields_get(req, model)
1720
1721         if import_compat:
1722             fields.pop('id', None)
1723         else:
1724             fields['.id'] = fields.pop('id', {'string': 'ID'})
1725
1726         fields_sequence = sorted(fields.iteritems(),
1727             key=lambda field: field[1].get('string', ''))
1728
1729         records = []
1730         for field_name, field in fields_sequence:
1731             if import_compat:
1732                 if exclude and field_name in exclude:
1733                     continue
1734                 if field.get('readonly'):
1735                     # If none of the field's states unsets readonly, skip the field
1736                     if all(dict(attrs).get('readonly', True)
1737                            for attrs in field.get('states', {}).values()):
1738                         continue
1739
1740             id = prefix + (prefix and '/'or '') + field_name
1741             name = parent_name + (parent_name and '/' or '') + field['string']
1742             record = {'id': id, 'string': name,
1743                       'value': id, 'children': False,
1744                       'field_type': field.get('type'),
1745                       'required': field.get('required'),
1746                       'relation_field': field.get('relation_field')}
1747             records.append(record)
1748
1749             if len(name.split('/')) < 3 and 'relation' in field:
1750                 ref = field.pop('relation')
1751                 record['value'] += '/id'
1752                 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1753
1754                 if not import_compat or field['type'] == 'one2many':
1755                     # m2m field in import_compat is childless
1756                     record['children'] = True
1757
1758         return records
1759
1760     @openerpweb.jsonrequest
1761     def namelist(self,req,  model, export_id):
1762         # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1763         export = req.session.model("ir.exports").read([export_id])[0]
1764         export_fields_list = req.session.model("ir.exports.line").read(
1765             export['export_fields'])
1766
1767         fields_data = self.fields_info(
1768             req, model, map(operator.itemgetter('name'), export_fields_list))
1769
1770         return [
1771             {'name': field['name'], 'label': fields_data[field['name']]}
1772             for field in export_fields_list
1773         ]
1774
1775     def fields_info(self, req, model, export_fields):
1776         info = {}
1777         fields = self.fields_get(req, model)
1778         if ".id" in export_fields:
1779             fields['.id'] = fields.pop('id', {'string': 'ID'})
1780             
1781         # To make fields retrieval more efficient, fetch all sub-fields of a
1782         # given field at the same time. Because the order in the export list is
1783         # arbitrary, this requires ordering all sub-fields of a given field
1784         # together so they can be fetched at the same time
1785         #
1786         # Works the following way:
1787         # * sort the list of fields to export, the default sorting order will
1788         #   put the field itself (if present, for xmlid) and all of its
1789         #   sub-fields right after it
1790         # * then, group on: the first field of the path (which is the same for
1791         #   a field and for its subfields and the length of splitting on the
1792         #   first '/', which basically means grouping the field on one side and
1793         #   all of the subfields on the other. This way, we have the field (for
1794         #   the xmlid) with length 1, and all of the subfields with the same
1795         #   base but a length "flag" of 2
1796         # * if we have a normal field (length 1), just add it to the info
1797         #   mapping (with its string) as-is
1798         # * otherwise, recursively call fields_info via graft_subfields.
1799         #   all graft_subfields does is take the result of fields_info (on the
1800         #   field's model) and prepend the current base (current field), which
1801         #   rebuilds the whole sub-tree for the field
1802         #
1803         # result: because we're not fetching the fields_get for half the
1804         # database models, fetching a namelist with a dozen fields (including
1805         # relational data) falls from ~6s to ~300ms (on the leads model).
1806         # export lists with no sub-fields (e.g. import_compatible lists with
1807         # no o2m) are even more efficient (from the same 6s to ~170ms, as
1808         # there's a single fields_get to execute)
1809         for (base, length), subfields in itertools.groupby(
1810                 sorted(export_fields),
1811                 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1812             subfields = list(subfields)
1813             if length == 2:
1814                 # subfields is a seq of $base/*rest, and not loaded yet
1815                 info.update(self.graft_subfields(
1816                     req, fields[base]['relation'], base, fields[base]['string'],
1817                     subfields
1818                 ))
1819             else:
1820                 info[base] = fields[base]['string']
1821
1822         return info
1823
1824     def graft_subfields(self, req, model, prefix, prefix_string, fields):
1825         export_fields = [field.split('/', 1)[1] for field in fields]
1826         return (
1827             (prefix + '/' + k, prefix_string + '/' + v)
1828             for k, v in self.fields_info(req, model, export_fields).iteritems())
1829
1830     #noinspection PyPropertyDefinition
1831     @property
1832     def content_type(self):
1833         """ Provides the format's content type """
1834         raise NotImplementedError()
1835
1836     def filename(self, base):
1837         """ Creates a valid filename for the format (with extension) from the
1838          provided base name (exension-less)
1839         """
1840         raise NotImplementedError()
1841
1842     def from_data(self, fields, rows):
1843         """ Conversion method from OpenERP's export data to whatever the
1844         current export class outputs
1845
1846         :params list fields: a list of fields to export
1847         :params list rows: a list of records to export
1848         :returns:
1849         :rtype: bytes
1850         """
1851         raise NotImplementedError()
1852
1853     @openerpweb.httprequest
1854     def index(self, req, data, token):
1855         model, fields, ids, domain, import_compat = \
1856             operator.itemgetter('model', 'fields', 'ids', 'domain',
1857                                 'import_compat')(
1858                 simplejson.loads(data))
1859
1860         context = req.session.eval_context(req.context)
1861         Model = req.session.model(model)
1862         ids = ids or Model.search(domain, 0, False, False, context)
1863
1864         field_names = map(operator.itemgetter('name'), fields)
1865         import_data = Model.export_data(ids, field_names, context).get('datas',[])
1866
1867         if import_compat:
1868             columns_headers = field_names
1869         else:
1870             columns_headers = [val['label'].strip() for val in fields]
1871
1872
1873         return req.make_response(self.from_data(columns_headers, import_data),
1874             headers=[('Content-Disposition',
1875                             content_disposition(self.filename(model), req)),
1876                      ('Content-Type', self.content_type)],
1877             cookies={'fileToken': int(token)})
1878
1879 class CSVExport(Export):
1880     _cp_path = '/web/export/csv'
1881     fmt = {'tag': 'csv', 'label': 'CSV'}
1882
1883     @property
1884     def content_type(self):
1885         return 'text/csv;charset=utf8'
1886
1887     def filename(self, base):
1888         return base + '.csv'
1889
1890     def from_data(self, fields, rows):
1891         fp = StringIO()
1892         writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1893
1894         writer.writerow([name.encode('utf-8') for name in fields])
1895
1896         for data in rows:
1897             row = []
1898             for d in data:
1899                 if isinstance(d, basestring):
1900                     d = d.replace('\n',' ').replace('\t',' ')
1901                     try:
1902                         d = d.encode('utf-8')
1903                     except UnicodeError:
1904                         pass
1905                 if d is False: d = None
1906                 row.append(d)
1907             writer.writerow(row)
1908
1909         fp.seek(0)
1910         data = fp.read()
1911         fp.close()
1912         return data
1913
1914 class ExcelExport(Export):
1915     _cp_path = '/web/export/xls'
1916     fmt = {
1917         'tag': 'xls',
1918         'label': 'Excel',
1919         'error': None if xlwt else "XLWT required"
1920     }
1921
1922     @property
1923     def content_type(self):
1924         return 'application/vnd.ms-excel'
1925
1926     def filename(self, base):
1927         return base + '.xls'
1928
1929     def from_data(self, fields, rows):
1930         workbook = xlwt.Workbook()
1931         worksheet = workbook.add_sheet('Sheet 1')
1932
1933         for i, fieldname in enumerate(fields):
1934             worksheet.write(0, i, fieldname)
1935             worksheet.col(i).width = 8000 # around 220 pixels
1936
1937         style = xlwt.easyxf('align: wrap yes')
1938
1939         for row_index, row in enumerate(rows):
1940             for cell_index, cell_value in enumerate(row):
1941                 if isinstance(cell_value, basestring):
1942                     cell_value = re.sub("\r", " ", cell_value)
1943                 if cell_value is False: cell_value = None
1944                 worksheet.write(row_index + 1, cell_index, cell_value, style)
1945
1946         fp = StringIO()
1947         workbook.save(fp)
1948         fp.seek(0)
1949         data = fp.read()
1950         fp.close()
1951         return data
1952
1953 class Reports(View):
1954     _cp_path = "/web/report"
1955     POLLING_DELAY = 0.25
1956     TYPES_MAPPING = {
1957         'doc': 'application/vnd.ms-word',
1958         'html': 'text/html',
1959         'odt': 'application/vnd.oasis.opendocument.text',
1960         'pdf': 'application/pdf',
1961         'sxw': 'application/vnd.sun.xml.writer',
1962         'xls': 'application/vnd.ms-excel',
1963     }
1964
1965     @openerpweb.httprequest
1966     def index(self, req, action, token):
1967         action = simplejson.loads(action)
1968
1969         report_srv = req.session.proxy("report")
1970         context = req.session.eval_context(
1971             nonliterals.CompoundContext(
1972                 req.context or {}, action[ "context"]))
1973
1974         report_data = {}
1975         report_ids = context["active_ids"]
1976         if 'report_type' in action:
1977             report_data['report_type'] = action['report_type']
1978         if 'datas' in action:
1979             if 'ids' in action['datas']:
1980                 report_ids = action['datas'].pop('ids')
1981             report_data.update(action['datas'])
1982
1983         report_id = report_srv.report(
1984             req.session._db, req.session._uid, req.session._password,
1985             action["report_name"], report_ids,
1986             report_data, context)
1987
1988         report_struct = None
1989         while True:
1990             report_struct = report_srv.report_get(
1991                 req.session._db, req.session._uid, req.session._password, report_id)
1992             if report_struct["state"]:
1993                 break
1994
1995             time.sleep(self.POLLING_DELAY)
1996
1997         report = base64.b64decode(report_struct['result'])
1998         if report_struct.get('code') == 'zlib':
1999             report = zlib.decompress(report)
2000         report_mimetype = self.TYPES_MAPPING.get(
2001             report_struct['format'], 'octet-stream')
2002         file_name = action.get('name', 'report')
2003         if 'name' not in action:
2004             reports = req.session.model('ir.actions.report.xml')
2005             res_id = reports.search([('report_name', '=', action['report_name']),],
2006                                     0, False, False, context)
2007             if len(res_id) > 0:
2008                 file_name = reports.read(res_id[0], ['name'], context)['name']
2009             else:
2010                 file_name = action['report_name']
2011         file_name = '%s.%s' % (file_name, report_struct['format'])
2012
2013         return req.make_response(report,
2014              headers=[
2015                  ('Content-Disposition', content_disposition(file_name, req)),
2016                  ('Content-Type', report_mimetype),
2017                  ('Content-Length', len(report))],
2018              cookies={'fileToken': int(token)})
2019
2020 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: