[IMP] Needaction: couters are not counter with the menu anymore.
[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([('needaction_enabled', '=', True)], context=context)
1121
1122         menu_needaction_data = Menus.get_needaction_data(menu_ids, context)
1123         return menu_needaction_data
1124
1125     @openerpweb.jsonrequest
1126     def action(self, req, menu_id):
1127         actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
1128                                              [('ir.ui.menu', menu_id)], False)
1129         return {"action": actions}
1130
1131 class DataSet(openerpweb.Controller):
1132     _cp_path = "/web/dataset"
1133
1134     @openerpweb.jsonrequest
1135     def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
1136         return self.do_search_read(req, model, fields, offset, limit, domain, sort)
1137     def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
1138                        , sort=None):
1139         """ Performs a search() followed by a read() (if needed) using the
1140         provided search criteria
1141
1142         :param req: a JSON-RPC request object
1143         :type req: openerpweb.JsonRequest
1144         :param str model: the name of the model to search on
1145         :param fields: a list of the fields to return in the result records
1146         :type fields: [str]
1147         :param int offset: from which index should the results start being returned
1148         :param int limit: the maximum number of records to return
1149         :param list domain: the search domain for the query
1150         :param list sort: sorting directives
1151         :returns: A structure (dict) with two keys: ids (all the ids matching
1152                   the (domain, context) pair) and records (paginated records
1153                   matching fields selection set)
1154         :rtype: list
1155         """
1156         Model = req.session.model(model)
1157
1158         context, domain = eval_context_and_domain(
1159             req.session, req.context, domain)
1160
1161         ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
1162         if limit and len(ids) == limit:
1163             length = Model.search_count(domain, context)
1164         else:
1165             length = len(ids) + (offset or 0)
1166         if fields and fields == ['id']:
1167             # shortcut read if we only want the ids
1168             return {
1169                 'length': length,
1170                 'records': [{'id': id} for id in ids]
1171             }
1172
1173         records = Model.read(ids, fields or False, context)
1174         records.sort(key=lambda obj: ids.index(obj['id']))
1175         return {
1176             'length': length,
1177             'records': records
1178         }
1179
1180     @openerpweb.jsonrequest
1181     def load(self, req, model, id, fields):
1182         m = req.session.model(model)
1183         value = {}
1184         r = m.read([id], False, req.session.eval_context(req.context))
1185         if r:
1186             value = r[0]
1187         return {'value': value}
1188
1189     def call_common(self, req, model, method, args, domain_id=None, context_id=None):
1190         has_domain = domain_id is not None and domain_id < len(args)
1191         has_context = context_id is not None and context_id < len(args)
1192
1193         domain = args[domain_id] if has_domain else []
1194         context = args[context_id] if has_context else {}
1195         c, d = eval_context_and_domain(req.session, context, domain)
1196         if has_domain:
1197             args[domain_id] = d
1198         if has_context:
1199             args[context_id] = c
1200
1201         return self._call_kw(req, model, method, args, {})
1202     
1203     def _call_kw(self, req, model, method, args, kwargs):
1204         for i in xrange(len(args)):
1205             if isinstance(args[i], nonliterals.BaseContext):
1206                 args[i] = req.session.eval_context(args[i])
1207             elif isinstance(args[i], nonliterals.BaseDomain):
1208                 args[i] = req.session.eval_domain(args[i])
1209         for k in kwargs.keys():
1210             if isinstance(kwargs[k], nonliterals.BaseContext):
1211                 kwargs[k] = req.session.eval_context(kwargs[k])
1212             elif isinstance(kwargs[k], nonliterals.BaseDomain):
1213                 kwargs[k] = req.session.eval_domain(kwargs[k])
1214
1215         # Temporary implements future display_name special field for model#read()
1216         if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
1217             if 'display_name' in args[1]:
1218                 names = req.session.model(model).name_get(args[0], **kwargs)
1219                 args[1].remove('display_name')
1220                 r = getattr(req.session.model(model), method)(*args, **kwargs)
1221                 for i in range(len(r)):
1222                     r[i]['display_name'] = names[i][1] or "%s#%d" % (model, names[i][0])
1223                 return r
1224
1225         return getattr(req.session.model(model), method)(*args, **kwargs)
1226
1227     @openerpweb.jsonrequest
1228     def onchange(self, req, model, method, args, context_id=None):
1229         """ Support method for handling onchange calls: behaves much like call
1230         with the following differences:
1231
1232         * Does not take a domain_id
1233         * Is aware of the return value's structure, and will parse the domains
1234           if needed in order to return either parsed literal domains (in JSON)
1235           or non-literal domain instances, allowing those domains to be used
1236           from JS
1237
1238         :param req:
1239         :type req: web.common.http.JsonRequest
1240         :param str model: object type on which to call the method
1241         :param str method: name of the onchange handler method
1242         :param list args: arguments to call the onchange handler with
1243         :param int context_id: index of the context object in the list of
1244                                arguments
1245         :return: result of the onchange call with all domains parsed
1246         """
1247         result = self.call_common(req, model, method, args, context_id=context_id)
1248         if not result or 'domain' not in result:
1249             return result
1250
1251         result['domain'] = dict(
1252             (k, parse_domain(v, req.session))
1253             for k, v in result['domain'].iteritems())
1254
1255         return result
1256
1257     @openerpweb.jsonrequest
1258     def call(self, req, model, method, args, domain_id=None, context_id=None):
1259         return self.call_common(req, model, method, args, domain_id, context_id)
1260     
1261     @openerpweb.jsonrequest
1262     def call_kw(self, req, model, method, args, kwargs):
1263         return self._call_kw(req, model, method, args, kwargs)
1264
1265     @openerpweb.jsonrequest
1266     def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1267         context = req.session.eval_context(req.context)
1268         action = self.call_common(req, model, method, args, domain_id, context_id)
1269         if isinstance(action, dict) and action.get('type') != '':
1270             return clean_action(req, action, context)
1271         return False
1272
1273     @openerpweb.jsonrequest
1274     def exec_workflow(self, req, model, id, signal):
1275         return req.session.exec_workflow(model, id, signal)
1276
1277     @openerpweb.jsonrequest
1278     def resequence(self, req, model, ids, field='sequence', offset=0):
1279         """ Re-sequences a number of records in the model, by their ids
1280
1281         The re-sequencing starts at the first model of ``ids``, the sequence
1282         number is incremented by one after each record and starts at ``offset``
1283
1284         :param ids: identifiers of the records to resequence, in the new sequence order
1285         :type ids: list(id)
1286         :param str field: field used for sequence specification, defaults to
1287                           "sequence"
1288         :param int offset: sequence number for first record in ``ids``, allows
1289                            starting the resequencing from an arbitrary number,
1290                            defaults to ``0``
1291         """
1292         m = req.session.model(model)
1293         if not m.fields_get([field]):
1294             return False
1295         # python 2.6 has no start parameter
1296         for i, id in enumerate(ids):
1297             m.write(id, { field: i + offset })
1298         return True
1299
1300 class View(openerpweb.Controller):
1301     _cp_path = "/web/view"
1302
1303     def fields_view_get(self, req, model, view_id, view_type,
1304                         transform=True, toolbar=False, submenu=False):
1305         Model = req.session.model(model)
1306         context = req.session.eval_context(req.context)
1307         fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
1308         # todo fme?: check that we should pass the evaluated context here
1309         self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
1310         if toolbar and transform:
1311             self.process_toolbar(req, fvg['toolbar'])
1312         return fvg
1313
1314     def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
1315         # depending on how it feels, xmlrpclib.ServerProxy can translate
1316         # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
1317         # enjoy unicode strings which can not be trivially converted to
1318         # strings, and it blows up during parsing.
1319
1320         # So ensure we fix this retardation by converting view xml back to
1321         # bit strings.
1322         if isinstance(fvg['arch'], unicode):
1323             arch = fvg['arch'].encode('utf-8')
1324         else:
1325             arch = fvg['arch']
1326         fvg['arch_string'] = arch
1327
1328         if transform:
1329             evaluation_context = session.evaluation_context(context or {})
1330             xml = self.transform_view(arch, session, evaluation_context)
1331         else:
1332             xml = ElementTree.fromstring(arch)
1333         fvg['arch'] = xml2json_from_elementtree(xml, preserve_whitespaces)
1334
1335         if 'id' in fvg['fields']:
1336             # Special case for id's
1337             id_field = fvg['fields']['id']
1338             id_field['original_type'] = id_field['type']
1339             id_field['type'] = 'id'
1340
1341         for field in fvg['fields'].itervalues():
1342             if field.get('views'):
1343                 for view in field["views"].itervalues():
1344                     self.process_view(session, view, None, transform)
1345             if field.get('domain'):
1346                 field["domain"] = parse_domain(field["domain"], session)
1347             if field.get('context'):
1348                 field["context"] = parse_context(field["context"], session)
1349
1350     def process_toolbar(self, req, toolbar):
1351         """
1352         The toolbar is a mapping of section_key: [action_descriptor]
1353
1354         We need to clean all those actions in order to ensure correct
1355         round-tripping
1356         """
1357         for actions in toolbar.itervalues():
1358             for action in actions:
1359                 if 'context' in action:
1360                     action['context'] = parse_context(
1361                         action['context'], req.session)
1362                 if 'domain' in action:
1363                     action['domain'] = parse_domain(
1364                         action['domain'], req.session)
1365
1366     @openerpweb.jsonrequest
1367     def add_custom(self, req, view_id, arch):
1368         CustomView = req.session.model('ir.ui.view.custom')
1369         CustomView.create({
1370             'user_id': req.session._uid,
1371             'ref_id': view_id,
1372             'arch': arch
1373         }, req.session.eval_context(req.context))
1374         return {'result': True}
1375
1376     @openerpweb.jsonrequest
1377     def undo_custom(self, req, view_id, reset=False):
1378         CustomView = req.session.model('ir.ui.view.custom')
1379         context = req.session.eval_context(req.context)
1380         vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1381                                     0, False, False, context)
1382         if vcustom:
1383             if reset:
1384                 CustomView.unlink(vcustom, context)
1385             else:
1386                 CustomView.unlink([vcustom[0]], context)
1387             return {'result': True}
1388         return {'result': False}
1389
1390     def transform_view(self, view_string, session, context=None):
1391         # transform nodes on the fly via iterparse, instead of
1392         # doing it statically on the parsing result
1393         parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
1394         root = None
1395         for event, elem in parser:
1396             if event == "start":
1397                 if root is None:
1398                     root = elem
1399                 self.parse_domains_and_contexts(elem, session)
1400         return root
1401
1402     def parse_domains_and_contexts(self, elem, session):
1403         """ Converts domains and contexts from the view into Python objects,
1404         either literals if they can be parsed by literal_eval or a special
1405         placeholder object if the domain or context refers to free variables.
1406
1407         :param elem: the current node being parsed
1408         :type param: xml.etree.ElementTree.Element
1409         :param session: OpenERP session object, used to store and retrieve
1410                         non-literal objects
1411         :type session: openerpweb.openerpweb.OpenERPSession
1412         """
1413         for el in ['domain', 'filter_domain']:
1414             domain = elem.get(el, '').strip()
1415             if domain:
1416                 elem.set(el, parse_domain(domain, session))
1417                 elem.set(el + '_string', domain)
1418         for el in ['context', 'default_get']:
1419             context_string = elem.get(el, '').strip()
1420             if context_string:
1421                 elem.set(el, parse_context(context_string, session))
1422                 elem.set(el + '_string', context_string)
1423
1424     @openerpweb.jsonrequest
1425     def load(self, req, model, view_id, view_type, toolbar=False):
1426         return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1427
1428 class TreeView(View):
1429     _cp_path = "/web/treeview"
1430
1431     @openerpweb.jsonrequest
1432     def action(self, req, model, id):
1433         return load_actions_from_ir_values(
1434             req,'action', 'tree_but_open',[(model, id)],
1435             False)
1436
1437 class SearchView(View):
1438     _cp_path = "/web/searchview"
1439
1440     @openerpweb.jsonrequest
1441     def load(self, req, model, view_id):
1442         fields_view = self.fields_view_get(req, model, view_id, 'search')
1443         return {'fields_view': fields_view}
1444
1445     @openerpweb.jsonrequest
1446     def fields_get(self, req, model):
1447         Model = req.session.model(model)
1448         fields = Model.fields_get(False, req.session.eval_context(req.context))
1449         for field in fields.values():
1450             # shouldn't convert the views too?
1451             if field.get('domain'):
1452                 field["domain"] = parse_domain(field["domain"], req.session)
1453             if field.get('context'):
1454                 field["context"] = parse_context(field["context"], req.session)
1455         return {'fields': fields}
1456
1457     @openerpweb.jsonrequest
1458     def get_filters(self, req, model):
1459         logger = logging.getLogger(__name__ + '.SearchView.get_filters')
1460         Model = req.session.model("ir.filters")
1461         filters = Model.get_filters(model)
1462         for filter in filters:
1463             try:
1464                 parsed_context = parse_context(filter["context"], req.session)
1465                 filter["context"] = (parsed_context
1466                         if not isinstance(parsed_context, nonliterals.BaseContext)
1467                         else req.session.eval_context(parsed_context))
1468
1469                 parsed_domain = parse_domain(filter["domain"], req.session)
1470                 filter["domain"] = (parsed_domain
1471                         if not isinstance(parsed_domain, nonliterals.BaseDomain)
1472                         else req.session.eval_domain(parsed_domain))
1473             except Exception:
1474                 logger.exception("Failed to parse custom filter %s in %s",
1475                                  filter['name'], model)
1476                 filter['disabled'] = True
1477                 del filter['context']
1478                 del filter['domain']
1479         return filters
1480
1481 class Binary(openerpweb.Controller):
1482     _cp_path = "/web/binary"
1483
1484     @openerpweb.httprequest
1485     def image(self, req, model, id, field, **kw):
1486         last_update = '__last_update'
1487         Model = req.session.model(model)
1488         context = req.session.eval_context(req.context)
1489         headers = [('Content-Type', 'image/png')]
1490         etag = req.httprequest.headers.get('If-None-Match')
1491         hashed_session = hashlib.md5(req.session_id).hexdigest()
1492         id = None if not id else simplejson.loads(id)
1493         if type(id) is list:
1494             id = id[0] # m2o
1495         if etag:
1496             if not id and hashed_session == etag:
1497                 return werkzeug.wrappers.Response(status=304)
1498             else:
1499                 date = Model.read([id], [last_update], context)[0].get(last_update)
1500                 if hashlib.md5(date).hexdigest() == etag:
1501                     return werkzeug.wrappers.Response(status=304)
1502
1503         retag = hashed_session
1504         try:
1505             if not id:
1506                 res = Model.default_get([field], context).get(field)
1507                 image_base64 = res
1508             else:
1509                 res = Model.read([id], [last_update, field], context)[0]
1510                 retag = hashlib.md5(res.get(last_update)).hexdigest()
1511                 image_base64 = res.get(field)
1512
1513             if kw.get('resize'):
1514                 resize = kw.get('resize').split(',');
1515                 if len(resize) == 2 and int(resize[0]) and int(resize[1]):
1516                     width = int(resize[0])
1517                     height = int(resize[1])
1518                     # resize maximum 500*500
1519                     if width > 500: width = 500
1520                     if height > 500: height = 500
1521                     image_base64 = openerp.tools.image_resize_image(base64_source=image_base64, size=(width, height), encoding='base64', filetype='PNG')
1522             
1523             image_data = base64.b64decode(image_base64)
1524
1525         except (TypeError, xmlrpclib.Fault):
1526             image_data = self.placeholder(req)
1527         headers.append(('ETag', retag))
1528         headers.append(('Content-Length', len(image_data)))
1529         try:
1530             ncache = int(kw.get('cache'))
1531             headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1532         except:
1533             pass
1534         return req.make_response(image_data, headers)
1535     def placeholder(self, req):
1536         addons_path = openerpweb.addons_manifest['web']['addons_path']
1537         return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1538
1539     @openerpweb.httprequest
1540     def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1541         """ Download link for files stored as binary fields.
1542
1543         If the ``id`` parameter is omitted, fetches the default value for the
1544         binary field (via ``default_get``), otherwise fetches the field for
1545         that precise record.
1546
1547         :param req: OpenERP request
1548         :type req: :class:`web.common.http.HttpRequest`
1549         :param str model: name of the model to fetch the binary from
1550         :param str field: binary field
1551         :param str id: id of the record from which to fetch the binary
1552         :param str filename_field: field holding the file's name, if any
1553         :returns: :class:`werkzeug.wrappers.Response`
1554         """
1555         Model = req.session.model(model)
1556         context = req.session.eval_context(req.context)
1557         fields = [field]
1558         if filename_field:
1559             fields.append(filename_field)
1560         if id:
1561             res = Model.read([int(id)], fields, context)[0]
1562         else:
1563             res = Model.default_get(fields, context)
1564         filecontent = base64.b64decode(res.get(field, ''))
1565         if not filecontent:
1566             return req.not_found()
1567         else:
1568             filename = '%s_%s' % (model.replace('.', '_'), id)
1569             if filename_field:
1570                 filename = res.get(filename_field, '') or filename
1571             return req.make_response(filecontent,
1572                 [('Content-Type', 'application/octet-stream'),
1573                  ('Content-Disposition', content_disposition(filename, req))])
1574
1575     @openerpweb.httprequest
1576     def saveas_ajax(self, req, data, token):
1577         jdata = simplejson.loads(data)
1578         model = jdata['model']
1579         field = jdata['field']
1580         id = jdata.get('id', None)
1581         filename_field = jdata.get('filename_field', None)
1582         context = jdata.get('context', dict())
1583
1584         context = req.session.eval_context(context)
1585         Model = req.session.model(model)
1586         fields = [field]
1587         if filename_field:
1588             fields.append(filename_field)
1589         if id:
1590             res = Model.read([int(id)], fields, context)[0]
1591         else:
1592             res = Model.default_get(fields, context)
1593         filecontent = base64.b64decode(res.get(field, ''))
1594         if not filecontent:
1595             raise ValueError("No content found for field '%s' on '%s:%s'" %
1596                 (field, model, id))
1597         else:
1598             filename = '%s_%s' % (model.replace('.', '_'), id)
1599             if filename_field:
1600                 filename = res.get(filename_field, '') or filename
1601             return req.make_response(filecontent,
1602                 headers=[('Content-Type', 'application/octet-stream'),
1603                         ('Content-Disposition', content_disposition(filename, req))],
1604                 cookies={'fileToken': int(token)})
1605
1606     @openerpweb.httprequest
1607     def upload(self, req, callback, ufile):
1608         # TODO: might be useful to have a configuration flag for max-length file uploads
1609         try:
1610             out = """<script language="javascript" type="text/javascript">
1611                         var win = window.top.window;
1612                         win.jQuery(win).trigger(%s, %s);
1613                     </script>"""
1614             data = ufile.read()
1615             args = [len(data), ufile.filename,
1616                     ufile.content_type, base64.b64encode(data)]
1617         except Exception, e:
1618             args = [False, e.message]
1619         return out % (simplejson.dumps(callback), simplejson.dumps(args))
1620
1621     @openerpweb.httprequest
1622     def upload_attachment(self, req, callback, model, id, ufile):
1623         context = req.session.eval_context(req.context)
1624         Model = req.session.model('ir.attachment')
1625         try:
1626             out = """<script language="javascript" type="text/javascript">
1627                         var win = window.top.window;
1628                         win.jQuery(win).trigger(%s, %s);
1629                     </script>"""
1630             attachment_id = Model.create({
1631                 'name': ufile.filename,
1632                 'datas': base64.encodestring(ufile.read()),
1633                 'datas_fname': ufile.filename,
1634                 'res_model': model,
1635                 'res_id': int(id)
1636             }, context)
1637             args = {
1638                 'filename': ufile.filename,
1639                 'id':  attachment_id
1640             }
1641         except Exception, e:
1642             args = { 'error': e.message }
1643         return out % (simplejson.dumps(callback), simplejson.dumps(args))
1644
1645 class Action(openerpweb.Controller):
1646     _cp_path = "/web/action"
1647
1648     @openerpweb.jsonrequest
1649     def load(self, req, action_id, do_not_eval=False, eval_context=None):
1650         Actions = req.session.model('ir.actions.actions')
1651         value = False
1652         context = req.session.eval_context(req.context)
1653         eval_context = req.session.eval_context(nonliterals.CompoundContext(context, eval_context or {}))
1654
1655         try:
1656             action_id = int(action_id)
1657         except ValueError:
1658             try:
1659                 module, xmlid = action_id.split('.', 1)
1660                 model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
1661                 assert model.startswith('ir.actions.')
1662             except Exception:
1663                 action_id = 0   # force failed read
1664
1665         base_action = Actions.read([action_id], ['type'], context)
1666         if base_action:
1667             ctx = {}
1668             action_type = base_action[0]['type']
1669             if action_type == 'ir.actions.report.xml':
1670                 ctx.update({'bin_size': True})
1671             ctx.update(context)
1672             action = req.session.model(action_type).read([action_id], False, ctx)
1673             if action:
1674                 value = clean_action(req, action[0], eval_context, do_not_eval)
1675         return value
1676
1677     @openerpweb.jsonrequest
1678     def run(self, req, action_id):
1679         context = req.session.eval_context(req.context)
1680         return_action = req.session.model('ir.actions.server').run(
1681             [action_id], req.session.eval_context(req.context))
1682         if return_action:
1683             return clean_action(req, return_action, context)
1684         else:
1685             return False
1686
1687 class Export(View):
1688     _cp_path = "/web/export"
1689
1690     @openerpweb.jsonrequest
1691     def formats(self, req):
1692         """ Returns all valid export formats
1693
1694         :returns: for each export format, a pair of identifier and printable name
1695         :rtype: [(str, str)]
1696         """
1697         return sorted([
1698             controller.fmt
1699             for path, controller in openerpweb.controllers_path.iteritems()
1700             if path.startswith(self._cp_path)
1701             if hasattr(controller, 'fmt')
1702         ], key=operator.itemgetter("label"))
1703
1704     def fields_get(self, req, model):
1705         Model = req.session.model(model)
1706         fields = Model.fields_get(False, req.session.eval_context(req.context))
1707         return fields
1708
1709     @openerpweb.jsonrequest
1710     def get_fields(self, req, model, prefix='', parent_name= '',
1711                    import_compat=True, parent_field_type=None,
1712                    exclude=None):
1713
1714         if import_compat and parent_field_type == "many2one":
1715             fields = {}
1716         else:
1717             fields = self.fields_get(req, model)
1718
1719         if import_compat:
1720             fields.pop('id', None)
1721         else:
1722             fields['.id'] = fields.pop('id', {'string': 'ID'})
1723
1724         fields_sequence = sorted(fields.iteritems(),
1725             key=lambda field: field[1].get('string', ''))
1726
1727         records = []
1728         for field_name, field in fields_sequence:
1729             if import_compat:
1730                 if exclude and field_name in exclude:
1731                     continue
1732                 if field.get('readonly'):
1733                     # If none of the field's states unsets readonly, skip the field
1734                     if all(dict(attrs).get('readonly', True)
1735                            for attrs in field.get('states', {}).values()):
1736                         continue
1737
1738             id = prefix + (prefix and '/'or '') + field_name
1739             name = parent_name + (parent_name and '/' or '') + field['string']
1740             record = {'id': id, 'string': name,
1741                       'value': id, 'children': False,
1742                       'field_type': field.get('type'),
1743                       'required': field.get('required'),
1744                       'relation_field': field.get('relation_field')}
1745             records.append(record)
1746
1747             if len(name.split('/')) < 3 and 'relation' in field:
1748                 ref = field.pop('relation')
1749                 record['value'] += '/id'
1750                 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1751
1752                 if not import_compat or field['type'] == 'one2many':
1753                     # m2m field in import_compat is childless
1754                     record['children'] = True
1755
1756         return records
1757
1758     @openerpweb.jsonrequest
1759     def namelist(self,req,  model, export_id):
1760         # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1761         export = req.session.model("ir.exports").read([export_id])[0]
1762         export_fields_list = req.session.model("ir.exports.line").read(
1763             export['export_fields'])
1764
1765         fields_data = self.fields_info(
1766             req, model, map(operator.itemgetter('name'), export_fields_list))
1767
1768         return [
1769             {'name': field['name'], 'label': fields_data[field['name']]}
1770             for field in export_fields_list
1771         ]
1772
1773     def fields_info(self, req, model, export_fields):
1774         info = {}
1775         fields = self.fields_get(req, model)
1776         if ".id" in export_fields:
1777             fields['.id'] = fields.pop('id', {'string': 'ID'})
1778             
1779         # To make fields retrieval more efficient, fetch all sub-fields of a
1780         # given field at the same time. Because the order in the export list is
1781         # arbitrary, this requires ordering all sub-fields of a given field
1782         # together so they can be fetched at the same time
1783         #
1784         # Works the following way:
1785         # * sort the list of fields to export, the default sorting order will
1786         #   put the field itself (if present, for xmlid) and all of its
1787         #   sub-fields right after it
1788         # * then, group on: the first field of the path (which is the same for
1789         #   a field and for its subfields and the length of splitting on the
1790         #   first '/', which basically means grouping the field on one side and
1791         #   all of the subfields on the other. This way, we have the field (for
1792         #   the xmlid) with length 1, and all of the subfields with the same
1793         #   base but a length "flag" of 2
1794         # * if we have a normal field (length 1), just add it to the info
1795         #   mapping (with its string) as-is
1796         # * otherwise, recursively call fields_info via graft_subfields.
1797         #   all graft_subfields does is take the result of fields_info (on the
1798         #   field's model) and prepend the current base (current field), which
1799         #   rebuilds the whole sub-tree for the field
1800         #
1801         # result: because we're not fetching the fields_get for half the
1802         # database models, fetching a namelist with a dozen fields (including
1803         # relational data) falls from ~6s to ~300ms (on the leads model).
1804         # export lists with no sub-fields (e.g. import_compatible lists with
1805         # no o2m) are even more efficient (from the same 6s to ~170ms, as
1806         # there's a single fields_get to execute)
1807         for (base, length), subfields in itertools.groupby(
1808                 sorted(export_fields),
1809                 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1810             subfields = list(subfields)
1811             if length == 2:
1812                 # subfields is a seq of $base/*rest, and not loaded yet
1813                 info.update(self.graft_subfields(
1814                     req, fields[base]['relation'], base, fields[base]['string'],
1815                     subfields
1816                 ))
1817             else:
1818                 info[base] = fields[base]['string']
1819
1820         return info
1821
1822     def graft_subfields(self, req, model, prefix, prefix_string, fields):
1823         export_fields = [field.split('/', 1)[1] for field in fields]
1824         return (
1825             (prefix + '/' + k, prefix_string + '/' + v)
1826             for k, v in self.fields_info(req, model, export_fields).iteritems())
1827
1828     #noinspection PyPropertyDefinition
1829     @property
1830     def content_type(self):
1831         """ Provides the format's content type """
1832         raise NotImplementedError()
1833
1834     def filename(self, base):
1835         """ Creates a valid filename for the format (with extension) from the
1836          provided base name (exension-less)
1837         """
1838         raise NotImplementedError()
1839
1840     def from_data(self, fields, rows):
1841         """ Conversion method from OpenERP's export data to whatever the
1842         current export class outputs
1843
1844         :params list fields: a list of fields to export
1845         :params list rows: a list of records to export
1846         :returns:
1847         :rtype: bytes
1848         """
1849         raise NotImplementedError()
1850
1851     @openerpweb.httprequest
1852     def index(self, req, data, token):
1853         model, fields, ids, domain, import_compat = \
1854             operator.itemgetter('model', 'fields', 'ids', 'domain',
1855                                 'import_compat')(
1856                 simplejson.loads(data))
1857
1858         context = req.session.eval_context(req.context)
1859         Model = req.session.model(model)
1860         ids = ids or Model.search(domain, 0, False, False, context)
1861
1862         field_names = map(operator.itemgetter('name'), fields)
1863         import_data = Model.export_data(ids, field_names, context).get('datas',[])
1864
1865         if import_compat:
1866             columns_headers = field_names
1867         else:
1868             columns_headers = [val['label'].strip() for val in fields]
1869
1870
1871         return req.make_response(self.from_data(columns_headers, import_data),
1872             headers=[('Content-Disposition',
1873                             content_disposition(self.filename(model), req)),
1874                      ('Content-Type', self.content_type)],
1875             cookies={'fileToken': int(token)})
1876
1877 class CSVExport(Export):
1878     _cp_path = '/web/export/csv'
1879     fmt = {'tag': 'csv', 'label': 'CSV'}
1880
1881     @property
1882     def content_type(self):
1883         return 'text/csv;charset=utf8'
1884
1885     def filename(self, base):
1886         return base + '.csv'
1887
1888     def from_data(self, fields, rows):
1889         fp = StringIO()
1890         writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1891
1892         writer.writerow([name.encode('utf-8') for name in fields])
1893
1894         for data in rows:
1895             row = []
1896             for d in data:
1897                 if isinstance(d, basestring):
1898                     d = d.replace('\n',' ').replace('\t',' ')
1899                     try:
1900                         d = d.encode('utf-8')
1901                     except UnicodeError:
1902                         pass
1903                 if d is False: d = None
1904                 row.append(d)
1905             writer.writerow(row)
1906
1907         fp.seek(0)
1908         data = fp.read()
1909         fp.close()
1910         return data
1911
1912 class ExcelExport(Export):
1913     _cp_path = '/web/export/xls'
1914     fmt = {
1915         'tag': 'xls',
1916         'label': 'Excel',
1917         'error': None if xlwt else "XLWT required"
1918     }
1919
1920     @property
1921     def content_type(self):
1922         return 'application/vnd.ms-excel'
1923
1924     def filename(self, base):
1925         return base + '.xls'
1926
1927     def from_data(self, fields, rows):
1928         workbook = xlwt.Workbook()
1929         worksheet = workbook.add_sheet('Sheet 1')
1930
1931         for i, fieldname in enumerate(fields):
1932             worksheet.write(0, i, fieldname)
1933             worksheet.col(i).width = 8000 # around 220 pixels
1934
1935         style = xlwt.easyxf('align: wrap yes')
1936
1937         for row_index, row in enumerate(rows):
1938             for cell_index, cell_value in enumerate(row):
1939                 if isinstance(cell_value, basestring):
1940                     cell_value = re.sub("\r", " ", cell_value)
1941                 if cell_value is False: cell_value = None
1942                 worksheet.write(row_index + 1, cell_index, cell_value, style)
1943
1944         fp = StringIO()
1945         workbook.save(fp)
1946         fp.seek(0)
1947         data = fp.read()
1948         fp.close()
1949         return data
1950
1951 class Reports(View):
1952     _cp_path = "/web/report"
1953     POLLING_DELAY = 0.25
1954     TYPES_MAPPING = {
1955         'doc': 'application/vnd.ms-word',
1956         'html': 'text/html',
1957         'odt': 'application/vnd.oasis.opendocument.text',
1958         'pdf': 'application/pdf',
1959         'sxw': 'application/vnd.sun.xml.writer',
1960         'xls': 'application/vnd.ms-excel',
1961     }
1962
1963     @openerpweb.httprequest
1964     def index(self, req, action, token):
1965         action = simplejson.loads(action)
1966
1967         report_srv = req.session.proxy("report")
1968         context = req.session.eval_context(
1969             nonliterals.CompoundContext(
1970                 req.context or {}, action[ "context"]))
1971
1972         report_data = {}
1973         report_ids = context["active_ids"]
1974         if 'report_type' in action:
1975             report_data['report_type'] = action['report_type']
1976         if 'datas' in action:
1977             if 'ids' in action['datas']:
1978                 report_ids = action['datas'].pop('ids')
1979             report_data.update(action['datas'])
1980
1981         report_id = report_srv.report(
1982             req.session._db, req.session._uid, req.session._password,
1983             action["report_name"], report_ids,
1984             report_data, context)
1985
1986         report_struct = None
1987         while True:
1988             report_struct = report_srv.report_get(
1989                 req.session._db, req.session._uid, req.session._password, report_id)
1990             if report_struct["state"]:
1991                 break
1992
1993             time.sleep(self.POLLING_DELAY)
1994
1995         report = base64.b64decode(report_struct['result'])
1996         if report_struct.get('code') == 'zlib':
1997             report = zlib.decompress(report)
1998         report_mimetype = self.TYPES_MAPPING.get(
1999             report_struct['format'], 'octet-stream')
2000         file_name = action.get('name', 'report')
2001         if 'name' not in action:
2002             reports = req.session.model('ir.actions.report.xml')
2003             res_id = reports.search([('report_name', '=', action['report_name']),],
2004                                     0, False, False, context)
2005             if len(res_id) > 0:
2006                 file_name = reports.read(res_id[0], ['name'], context)['name']
2007             else:
2008                 file_name = action['report_name']
2009         file_name = '%s.%s' % (file_name, report_struct['format'])
2010
2011         return req.make_response(report,
2012              headers=[
2013                  ('Content-Disposition', content_disposition(file_name, req)),
2014                  ('Content-Type', report_mimetype),
2015                  ('Content-Length', len(report))],
2016              cookies={'fileToken': int(token)})
2017
2018 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: