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