[IMP] cleanup menu method mess
[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 from openerp.tools.translate import _
32
33 from .. import http
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 load_actions_from_ir_values(req, key, key2, models, meta):
365     Values = req.session.model('ir.values')
366     actions = Values.get(key, key2, models, meta, req.context)
367
368     return [(id, name, clean_action(req, action))
369             for id, name, action in actions]
370
371 def clean_action(req, action):
372     action.setdefault('flags', {})
373     action_type = action.setdefault('type', 'ir.actions.act_window_close')
374     if action_type == 'ir.actions.act_window':
375         return fix_view_modes(action)
376     return action
377
378 # I think generate_views,fix_view_modes should go into js ActionManager
379 def generate_views(action):
380     """
381     While the server generates a sequence called "views" computing dependencies
382     between a bunch of stuff for views coming directly from the database
383     (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
384     to return custom view dictionaries generated on the fly.
385
386     In that case, there is no ``views`` key available on the action.
387
388     Since the web client relies on ``action['views']``, generate it here from
389     ``view_mode`` and ``view_id``.
390
391     Currently handles two different cases:
392
393     * no view_id, multiple view_mode
394     * single view_id, single view_mode
395
396     :param dict action: action descriptor dictionary to generate a views key for
397     """
398     view_id = action.get('view_id') or False
399     if isinstance(view_id, (list, tuple)):
400         view_id = view_id[0]
401
402     # providing at least one view mode is a requirement, not an option
403     view_modes = action['view_mode'].split(',')
404
405     if len(view_modes) > 1:
406         if view_id:
407             raise ValueError('Non-db action dictionaries should provide '
408                              'either multiple view modes or a single view '
409                              'mode and an optional view id.\n\n Got view '
410                              'modes %r and view id %r for action %r' % (
411                 view_modes, view_id, action))
412         action['views'] = [(False, mode) for mode in view_modes]
413         return
414     action['views'] = [(view_id, view_modes[0])]
415
416 def fix_view_modes(action):
417     """ For historical reasons, OpenERP has weird dealings in relation to
418     view_mode and the view_type attribute (on window actions):
419
420     * one of the view modes is ``tree``, which stands for both list views
421       and tree views
422     * the choice is made by checking ``view_type``, which is either
423       ``form`` for a list view or ``tree`` for an actual tree view
424
425     This methods simply folds the view_type into view_mode by adding a
426     new view mode ``list`` which is the result of the ``tree`` view_mode
427     in conjunction with the ``form`` view_type.
428
429     TODO: this should go into the doc, some kind of "peculiarities" section
430
431     :param dict action: an action descriptor
432     :returns: nothing, the action is modified in place
433     """
434     if not action.get('views'):
435         generate_views(action)
436
437     if action.pop('view_type', 'form') != 'form':
438         return action
439
440     if 'view_mode' in action:
441         action['view_mode'] = ','.join(
442             mode if mode != 'tree' else 'list'
443             for mode in action['view_mode'].split(','))
444     action['views'] = [
445         [id, mode if mode != 'tree' else 'list']
446         for id, mode in action['views']
447     ]
448
449     return action
450
451 def _local_web_translations(trans_file):
452     messages = []
453     try:
454         with open(trans_file) as t_file:
455             po = babel.messages.pofile.read_po(t_file)
456     except Exception:
457         return
458     for x in po:
459         if x.id and x.string and "openerp-web" in x.auto_comments:
460             messages.append({'id': x.id, 'string': x.string})
461     return messages
462
463 def xml2json_from_elementtree(el, preserve_whitespaces=False):
464     """ xml2json-direct
465     Simple and straightforward XML-to-JSON converter in Python
466     New BSD Licensed
467     http://code.google.com/p/xml2json-direct/
468     """
469     res = {}
470     if el.tag[0] == "{":
471         ns, name = el.tag.rsplit("}", 1)
472         res["tag"] = name
473         res["namespace"] = ns[1:]
474     else:
475         res["tag"] = el.tag
476     res["attrs"] = {}
477     for k, v in el.items():
478         res["attrs"][k] = v
479     kids = []
480     if el.text and (preserve_whitespaces or el.text.strip() != ''):
481         kids.append(el.text)
482     for kid in el:
483         kids.append(xml2json_from_elementtree(kid, preserve_whitespaces))
484         if kid.tail and (preserve_whitespaces or kid.tail.strip() != ''):
485             kids.append(kid.tail)
486     res["children"] = kids
487     return res
488
489 def content_disposition(filename, req):
490     filename = filename.encode('utf8')
491     escaped = urllib2.quote(filename)
492     browser = req.httprequest.user_agent.browser
493     version = int((req.httprequest.user_agent.version or '0').split('.')[0])
494     if browser == 'msie' and version < 9:
495         return "attachment; filename=%s" % escaped
496     elif browser == 'safari':
497         return "attachment; filename=%s" % filename
498     else:
499         return "attachment; filename*=UTF-8''%s" % escaped
500
501
502 #----------------------------------------------------------
503 # OpenERP Web web Controllers
504 #----------------------------------------------------------
505
506 html_template = """<!DOCTYPE html>
507 <html style="height: 100%%">
508     <head>
509         <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
510         <meta http-equiv="content-type" content="text/html; charset=utf-8" />
511         <title>OpenERP</title>
512         <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
513         <link rel="stylesheet" href="/web/static/src/css/full.css" />
514         %(css)s
515         %(js)s
516         <script type="text/javascript">
517             $(function() {
518                 var s = new openerp.init(%(modules)s);
519                 %(init)s
520             });
521         </script>
522     </head>
523     <body>
524         <!--[if lte IE 8]>
525         <script src="http://ajax.googleapis.com/ajax/libs/chrome-frame/1/CFInstall.min.js"></script>
526         <script>CFInstall.check({mode: "overlay"});</script>
527         <![endif]-->
528     </body>
529 </html>
530 """
531
532 class Home(openerpweb.Controller):
533     _cp_path = '/'
534
535     @openerpweb.httprequest
536     def index(self, req, s_action=None, db=None, **kw):
537         js = "\n        ".join('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list(req, 'js', db=db))
538         css = "\n        ".join('<link rel="stylesheet" href="%s">' % i for i in manifest_list(req, 'css', db=db))
539
540         r = html_template % {
541             'js': js,
542             'css': css,
543             'modules': simplejson.dumps(module_boot(req, db=db)),
544             'init': 'var wc = new s.web.WebClient();wc.appendTo($(document.body));'
545         }
546         return r
547
548     @openerpweb.httprequest
549     def login(self, req, db, login, key):
550         return login_and_redirect(req, db, login, key)
551
552 class WebClient(openerpweb.Controller):
553     _cp_path = "/web/webclient"
554
555     @openerpweb.jsonrequest
556     def csslist(self, req, mods=None):
557         return manifest_list(req, 'css', mods=mods)
558
559     @openerpweb.jsonrequest
560     def jslist(self, req, mods=None):
561         return manifest_list(req, 'js', mods=mods)
562
563     @openerpweb.jsonrequest
564     def qweblist(self, req, mods=None):
565         return manifest_list(req, 'qweb', mods=mods)
566
567     @openerpweb.httprequest
568     def css(self, req, mods=None, db=None):
569         files = list(manifest_glob(req, 'css', addons=mods, db=db))
570         last_modified = get_last_modified(f[0] for f in files)
571         if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
572             return werkzeug.wrappers.Response(status=304)
573
574         file_map = dict(files)
575
576         rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
577         rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
578
579         def reader(f):
580             """read the a css file and absolutify all relative uris"""
581             with open(f, 'rb') as fp:
582                 data = fp.read().decode('utf-8')
583
584             path = file_map[f]
585             web_dir = os.path.dirname(path)
586
587             data = re.sub(
588                 rx_import,
589                 r"""@import \1%s/""" % (web_dir,),
590                 data,
591             )
592
593             data = re.sub(
594                 rx_url,
595                 r"""url(\1%s/""" % (web_dir,),
596                 data,
597             )
598             return data.encode('utf-8')
599
600         content, checksum = concat_files((f[0] for f in files), reader)
601
602         return make_conditional(
603             req, req.make_response(content, [('Content-Type', 'text/css')]),
604             last_modified, checksum)
605
606     @openerpweb.httprequest
607     def js(self, req, mods=None, db=None):
608         files = [f[0] for f in manifest_glob(req, 'js', addons=mods, db=db)]
609         last_modified = get_last_modified(files)
610         if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
611             return werkzeug.wrappers.Response(status=304)
612
613         content, checksum = concat_js(files)
614
615         return make_conditional(
616             req, req.make_response(content, [('Content-Type', 'application/javascript')]),
617             last_modified, checksum)
618
619     @openerpweb.httprequest
620     def qweb(self, req, mods=None, db=None):
621         files = [f[0] for f in manifest_glob(req, 'qweb', addons=mods, db=db)]
622         last_modified = get_last_modified(files)
623         if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
624             return werkzeug.wrappers.Response(status=304)
625
626         content, checksum = concat_xml(files)
627
628         return make_conditional(
629             req, req.make_response(content, [('Content-Type', 'text/xml')]),
630             last_modified, checksum)
631
632     @openerpweb.jsonrequest
633     def bootstrap_translations(self, req, mods):
634         """ Load local translations from *.po files, as a temporary solution
635             until we have established a valid session. This is meant only
636             for translating the login page and db management chrome, using
637             the browser's language. """
638         # For performance reasons we only load a single translation, so for
639         # sub-languages (that should only be partially translated) we load the
640         # main language PO instead - that should be enough for the login screen.
641         lang = req.lang.split('_')[0]
642
643         translations_per_module = {}
644         for addon_name in mods:
645             if openerpweb.addons_manifest[addon_name].get('bootstrap'):
646                 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
647                 f_name = os.path.join(addons_path, addon_name, "i18n", lang + ".po")
648                 if not os.path.exists(f_name):
649                     continue
650                 translations_per_module[addon_name] = {'messages': _local_web_translations(f_name)}
651
652         return {"modules": translations_per_module,
653                 "lang_parameters": None}
654
655     @openerpweb.jsonrequest
656     def translations(self, req, mods, lang):
657         res_lang = req.session.model('res.lang')
658         ids = res_lang.search([("code", "=", lang)])
659         lang_params = None
660         if ids:
661             lang_params = res_lang.read(ids[0], ["direction", "date_format", "time_format",
662                                                 "grouping", "decimal_point", "thousands_sep"])
663
664         # Regional languages (ll_CC) must inherit/override their parent lang (ll), but this is
665         # done server-side when the language is loaded, so we only need to load the user's lang.
666         ir_translation = req.session.model('ir.translation')
667         translations_per_module = {}
668         messages = ir_translation.search_read([('module','in',mods),('lang','=',lang),
669                                                ('comments','like','openerp-web'),('value','!=',False),
670                                                ('value','!=','')],
671                                               ['module','src','value','lang'], order='module') 
672         for mod, msg_group in itertools.groupby(messages, key=operator.itemgetter('module')):
673             translations_per_module.setdefault(mod,{'messages':[]})
674             translations_per_module[mod]['messages'].extend({'id': m['src'],
675                                                              'string': m['value']} \
676                                                                 for m in msg_group)
677         return {"modules": translations_per_module,
678                 "lang_parameters": lang_params}
679
680     @openerpweb.jsonrequest
681     def version_info(self, req):
682         return {
683             "version": openerp.release.version
684         }
685
686 class Proxy(openerpweb.Controller):
687     _cp_path = '/web/proxy'
688
689     @openerpweb.jsonrequest
690     def load(self, req, path):
691         """ Proxies an HTTP request through a JSON request.
692
693         It is strongly recommended to not request binary files through this,
694         as the result will be a binary data blob as well.
695
696         :param req: OpenERP request
697         :param path: actual request path
698         :return: file content
699         """
700         from werkzeug.test import Client
701         from werkzeug.wrappers import BaseResponse
702
703         return Client(req.httprequest.app, BaseResponse).get(path).data
704
705 class Database(openerpweb.Controller):
706     _cp_path = "/web/database"
707
708     @openerpweb.jsonrequest
709     def get_list(self, req):
710         return db_list(req)
711
712     @openerpweb.jsonrequest
713     def create(self, req, fields):
714         params = dict(map(operator.itemgetter('name', 'value'), fields))
715         return req.session.proxy("db").create_database(
716             params['super_admin_pwd'],
717             params['db_name'],
718             bool(params.get('demo_data')),
719             params['db_lang'],
720             params['create_admin_pwd'])
721
722     @openerpweb.jsonrequest
723     def duplicate(self, req, fields):
724         params = dict(map(operator.itemgetter('name', 'value'), fields))
725         return req.session.proxy("db").duplicate_database(
726             params['super_admin_pwd'],
727             params['db_original_name'],
728             params['db_name'])
729
730     @openerpweb.jsonrequest
731     def duplicate(self, req, fields):
732         params = dict(map(operator.itemgetter('name', 'value'), fields))
733         duplicate_attrs = (
734             params['super_admin_pwd'],
735             params['db_original_name'],
736             params['db_name'],
737         )
738
739         return req.session.proxy("db").duplicate_database(*duplicate_attrs)
740
741     @openerpweb.jsonrequest
742     def drop(self, req, fields):
743         password, db = operator.itemgetter(
744             'drop_pwd', 'drop_db')(
745                 dict(map(operator.itemgetter('name', 'value'), fields)))
746
747         try:
748             return req.session.proxy("db").drop(password, db)
749         except xmlrpclib.Fault, e:
750             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
751                 return {'error': e.faultCode, 'title': 'Drop Database'}
752         return {'error': _('Could not drop database !'), 'title': _('Drop Database')}
753
754     @openerpweb.httprequest
755     def backup(self, req, backup_db, backup_pwd, token):
756         try:
757             db_dump = base64.b64decode(
758                 req.session.proxy("db").dump(backup_pwd, backup_db))
759             filename = "%(db)s_%(timestamp)s.dump" % {
760                 'db': backup_db,
761                 'timestamp': datetime.datetime.utcnow().strftime(
762                     "%Y-%m-%d_%H-%M-%SZ")
763             }
764             return req.make_response(db_dump,
765                [('Content-Type', 'application/octet-stream; charset=binary'),
766                ('Content-Disposition', content_disposition(filename, req))],
767                {'fileToken': int(token)}
768             )
769         except xmlrpclib.Fault, e:
770             return simplejson.dumps([[],[{'error': e.faultCode, 'title': _('Backup Database')}]])
771
772     @openerpweb.httprequest
773     def restore(self, req, db_file, restore_pwd, new_db):
774         try:
775             data = base64.b64encode(db_file.read())
776             req.session.proxy("db").restore(restore_pwd, new_db, data)
777             return ''
778         except xmlrpclib.Fault, e:
779             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
780                 raise Exception("AccessDenied")
781
782     @openerpweb.jsonrequest
783     def change_password(self, req, fields):
784         old_password, new_password = operator.itemgetter(
785             'old_pwd', 'new_pwd')(
786                 dict(map(operator.itemgetter('name', 'value'), fields)))
787         try:
788             return req.session.proxy("db").change_admin_password(old_password, new_password)
789         except xmlrpclib.Fault, e:
790             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
791                 return {'error': e.faultCode, 'title': _('Change Password')}
792         return {'error': _('Error, password not changed !'), 'title': _('Change Password')}
793
794 class Session(openerpweb.Controller):
795     _cp_path = "/web/session"
796
797     def session_info(self, req):
798         req.session.ensure_valid()
799         return {
800             "session_id": req.session_id,
801             "uid": req.session._uid,
802             "context": req.session.get_context() if req.session._uid else {},
803             "db": req.session._db,
804             "login": req.session._login,
805         }
806
807     @openerpweb.jsonrequest
808     def get_session_info(self, req):
809         return self.session_info(req)
810
811     @openerpweb.jsonrequest
812     def authenticate(self, req, db, login, password, base_location=None):
813         wsgienv = req.httprequest.environ
814         env = dict(
815             base_location=base_location,
816             HTTP_HOST=wsgienv['HTTP_HOST'],
817             REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
818         )
819         req.session.authenticate(db, login, password, env)
820
821         return self.session_info(req)
822
823     @openerpweb.jsonrequest
824     def change_password (self,req,fields):
825         old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
826                 dict(map(operator.itemgetter('name', 'value'), fields)))
827         if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
828             return {'error':_('You cannot leave any password empty.'),'title': _('Change Password')}
829         if new_password != confirm_password:
830             return {'error': _('The new password and its confirmation must be identical.'),'title': _('Change Password')}
831         try:
832             if req.session.model('res.users').change_password(
833                 old_password, new_password):
834                 return {'new_password':new_password}
835         except Exception:
836             return {'error': _('The old password you provided is incorrect, your password was not changed.'), 'title': _('Change Password')}
837         return {'error': _('Error, password not changed !'), 'title': _('Change Password')}
838
839     @openerpweb.jsonrequest
840     def sc_list(self, req):
841         return req.session.model('ir.ui.view_sc').get_sc(
842             req.session._uid, "ir.ui.menu", req.context)
843
844     @openerpweb.jsonrequest
845     def get_lang_list(self, req):
846         try:
847             return req.session.proxy("db").list_lang() or []
848         except Exception, e:
849             return {"error": e, "title": _("Languages")}
850
851     @openerpweb.jsonrequest
852     def modules(self, req):
853         # return all installed modules. Web client is smart enough to not load a module twice
854         return module_installed(req)
855
856     @openerpweb.jsonrequest
857     def save_session_action(self, req, the_action):
858         """
859         This method store an action object in the session object and returns an integer
860         identifying that action. The method get_session_action() can be used to get
861         back the action.
862
863         :param the_action: The action to save in the session.
864         :type the_action: anything
865         :return: A key identifying the saved action.
866         :rtype: integer
867         """
868         saved_actions = req.httpsession.get('saved_actions')
869         if not saved_actions:
870             saved_actions = {"next":0, "actions":{}}
871             req.httpsession['saved_actions'] = saved_actions
872         # we don't allow more than 10 stored actions
873         if len(saved_actions["actions"]) >= 10:
874             del saved_actions["actions"][min(saved_actions["actions"])]
875         key = saved_actions["next"]
876         saved_actions["actions"][key] = the_action
877         saved_actions["next"] = key + 1
878         return key
879
880     @openerpweb.jsonrequest
881     def get_session_action(self, req, key):
882         """
883         Gets back a previously saved action. This method can return None if the action
884         was saved since too much time (this case should be handled in a smart way).
885
886         :param key: The key given by save_session_action()
887         :type key: integer
888         :return: The saved action or None.
889         :rtype: anything
890         """
891         saved_actions = req.httpsession.get('saved_actions')
892         if not saved_actions:
893             return None
894         return saved_actions["actions"].get(key)
895
896     @openerpweb.jsonrequest
897     def check(self, req):
898         req.session.assert_valid()
899         return None
900
901     @openerpweb.jsonrequest
902     def destroy(self, req):
903         req.session._suicide = True
904
905 class Menu(openerpweb.Controller):
906     _cp_path = "/web/menu"
907
908     @openerpweb.jsonrequest
909     def get_user_roots(self, req):
910         """ Return all root menu ids visible for the session user.
911
912         :param req: A request object, with an OpenERP session attribute
913         :type req: < session -> OpenERPSession >
914         :return: the root menu ids
915         :rtype: list(int)
916         """
917         s = req.session
918         Menus = s.model('ir.ui.menu')
919         # If a menu action is defined use its domain to get the root menu items
920         user_menu_id = s.model('res.users').read([s._uid], ['menu_id'],
921                                                  req.context)[0]['menu_id']
922
923         menu_domain = [('parent_id', '=', False)]
924         if user_menu_id:
925             domain_string = s.model('ir.actions.act_window').read(
926                 [user_menu_id[0]], ['domain'],req.context)[0]['domain']
927             if domain_string:
928                 menu_domain = ast.literal_eval(domain_string)
929
930         return Menus.search(menu_domain, 0, False, False, req.context)
931
932     @openerpweb.jsonrequest
933     def load(self, req):
934         """ Loads all menu items (all applications and their sub-menus).
935
936         :param req: A request object, with an OpenERP session attribute
937         :type req: < session -> OpenERPSession >
938         :return: the menu root
939         :rtype: dict('children': menu_nodes)
940         """
941         Menus = req.session.model('ir.ui.menu')
942
943         fields = ['name', 'sequence', 'parent_id', 'action',
944                   'needaction_enabled']
945         menu_roots = Menus.read(self.get_user_roots(req), fields, req.context)
946         menu_root = {
947             'id': False,
948             'name': 'root',
949             'parent_id': [-1, ''],
950             'children': menu_roots
951         }
952
953         # menus are loaded fully unlike a regular tree view, cause there are a
954         # limited number of items (752 when all 6.1 addons are installed)
955         menu_ids = Menus.search([], 0, False, False, req.context)
956         menu_items = Menus.read(menu_ids, fields, req.context)
957         # adds roots at the end of the sequence, so that they will overwrite
958         # equivalent menu items from full menu read when put into id:item
959         # mapping, resulting in children being correctly set on the roots.
960         menu_items.extend(menu_roots)
961
962         # make a tree using parent_id
963         menu_items_map = dict(
964             (menu_item["id"], menu_item) for menu_item in menu_items)
965         for menu_item in menu_items:
966             if menu_item['parent_id']:
967                 parent = menu_item['parent_id'][0]
968             else:
969                 parent = False
970             if parent in menu_items_map:
971                 menu_items_map[parent].setdefault(
972                     'children', []).append(menu_item)
973
974         # sort by sequence a tree using parent_id
975         for menu_item in menu_items:
976             menu_item.setdefault('children', []).sort(
977                 key=operator.itemgetter('sequence'))
978
979         return menu_root
980
981     @openerpweb.jsonrequest
982     def load_needaction(self, req, menu_ids=False):
983         """ Loads needaction counters for all or some specific menu ids.
984
985             :return: needaction data
986             :rtype: dict(menu_id: {'needaction_enabled': boolean, 'needaction_counter': int})
987         """
988         Menus = req.session.model('ir.ui.menu')
989
990         if menu_ids == False:
991             menu_ids = Menus.search([('needaction_enabled', '=', True)], context=req.context)
992
993         menu_needaction_data = Menus.get_needaction_data(menu_ids, req.context)
994         return menu_needaction_data
995
996     @openerpweb.jsonrequest
997     def action(self, req, menu_id):
998         # still used by web_shortcut
999         actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
1000                                              [('ir.ui.menu', menu_id)], False)
1001         return {"action": actions}
1002
1003 class DataSet(openerpweb.Controller):
1004     _cp_path = "/web/dataset"
1005
1006     @openerpweb.jsonrequest
1007     def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
1008         return self.do_search_read(req, model, fields, offset, limit, domain, sort)
1009     def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
1010                        , sort=None):
1011         """ Performs a search() followed by a read() (if needed) using the
1012         provided search criteria
1013
1014         :param req: a JSON-RPC request object
1015         :type req: openerpweb.JsonRequest
1016         :param str model: the name of the model to search on
1017         :param fields: a list of the fields to return in the result records
1018         :type fields: [str]
1019         :param int offset: from which index should the results start being returned
1020         :param int limit: the maximum number of records to return
1021         :param list domain: the search domain for the query
1022         :param list sort: sorting directives
1023         :returns: A structure (dict) with two keys: ids (all the ids matching
1024                   the (domain, context) pair) and records (paginated records
1025                   matching fields selection set)
1026         :rtype: list
1027         """
1028         Model = req.session.model(model)
1029
1030         ids = Model.search(domain, offset or 0, limit or False, sort or False,
1031                            req.context)
1032         if limit and len(ids) == limit:
1033             length = Model.search_count(domain, req.context)
1034         else:
1035             length = len(ids) + (offset or 0)
1036         if fields and fields == ['id']:
1037             # shortcut read if we only want the ids
1038             return {
1039                 'length': length,
1040                 'records': [{'id': id} for id in ids]
1041             }
1042
1043         records = Model.read(ids, fields or False, req.context)
1044         records.sort(key=lambda obj: ids.index(obj['id']))
1045         return {
1046             'length': length,
1047             'records': records
1048         }
1049
1050     @openerpweb.jsonrequest
1051     def load(self, req, model, id, fields):
1052         m = req.session.model(model)
1053         value = {}
1054         r = m.read([id], False, req.context)
1055         if r:
1056             value = r[0]
1057         return {'value': value}
1058
1059     def call_common(self, req, model, method, args, domain_id=None, context_id=None):
1060         return self._call_kw(req, model, method, args, {})
1061
1062     def _call_kw(self, req, model, method, args, kwargs):
1063         # Temporary implements future display_name special field for model#read()
1064         if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
1065             if 'display_name' in args[1]:
1066                 names = req.session.model(model).name_get(args[0], **kwargs)
1067                 args[1].remove('display_name')
1068                 r = getattr(req.session.model(model), method)(*args, **kwargs)
1069                 for i in range(len(r)):
1070                     r[i]['display_name'] = names[i][1] or "%s#%d" % (model, names[i][0])
1071                 return r
1072
1073         return getattr(req.session.model(model), method)(*args, **kwargs)
1074
1075     @openerpweb.jsonrequest
1076     def call(self, req, model, method, args, domain_id=None, context_id=None):
1077         return self._call_kw(req, model, method, args, {})
1078     
1079     @openerpweb.jsonrequest
1080     def call_kw(self, req, model, method, args, kwargs):
1081         return self._call_kw(req, model, method, args, kwargs)
1082
1083     @openerpweb.jsonrequest
1084     def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1085         action = self._call_kw(req, model, method, args, {})
1086         if isinstance(action, dict) and action.get('type') != '':
1087             return clean_action(req, action)
1088         return False
1089
1090     @openerpweb.jsonrequest
1091     def exec_workflow(self, req, model, id, signal):
1092         return req.session.exec_workflow(model, id, signal)
1093
1094     @openerpweb.jsonrequest
1095     def resequence(self, req, model, ids, field='sequence', offset=0):
1096         """ Re-sequences a number of records in the model, by their ids
1097
1098         The re-sequencing starts at the first model of ``ids``, the sequence
1099         number is incremented by one after each record and starts at ``offset``
1100
1101         :param ids: identifiers of the records to resequence, in the new sequence order
1102         :type ids: list(id)
1103         :param str field: field used for sequence specification, defaults to
1104                           "sequence"
1105         :param int offset: sequence number for first record in ``ids``, allows
1106                            starting the resequencing from an arbitrary number,
1107                            defaults to ``0``
1108         """
1109         m = req.session.model(model)
1110         if not m.fields_get([field]):
1111             return False
1112         # python 2.6 has no start parameter
1113         for i, id in enumerate(ids):
1114             m.write(id, { field: i + offset })
1115         return True
1116
1117 class View(openerpweb.Controller):
1118     _cp_path = "/web/view"
1119
1120     @openerpweb.jsonrequest
1121     def add_custom(self, req, view_id, arch):
1122         CustomView = req.session.model('ir.ui.view.custom')
1123         CustomView.create({
1124             'user_id': req.session._uid,
1125             'ref_id': view_id,
1126             'arch': arch
1127         }, req.context)
1128         return {'result': True}
1129
1130     @openerpweb.jsonrequest
1131     def undo_custom(self, req, view_id, reset=False):
1132         CustomView = req.session.model('ir.ui.view.custom')
1133         vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1134                                     0, False, False, req.context)
1135         if vcustom:
1136             if reset:
1137                 CustomView.unlink(vcustom, req.context)
1138             else:
1139                 CustomView.unlink([vcustom[0]], req.context)
1140             return {'result': True}
1141         return {'result': False}
1142
1143 class TreeView(View):
1144     _cp_path = "/web/treeview"
1145
1146     @openerpweb.jsonrequest
1147     def action(self, req, model, id):
1148         return load_actions_from_ir_values(
1149             req,'action', 'tree_but_open',[(model, id)],
1150             False)
1151
1152 class Binary(openerpweb.Controller):
1153     _cp_path = "/web/binary"
1154
1155     @openerpweb.httprequest
1156     def image(self, req, model, id, field, **kw):
1157         last_update = '__last_update'
1158         Model = req.session.model(model)
1159         headers = [('Content-Type', 'image/png')]
1160         etag = req.httprequest.headers.get('If-None-Match')
1161         hashed_session = hashlib.md5(req.session_id).hexdigest()
1162         id = None if not id else simplejson.loads(id)
1163         if type(id) is list:
1164             id = id[0] # m2o
1165         if etag:
1166             if not id and hashed_session == etag:
1167                 return werkzeug.wrappers.Response(status=304)
1168             else:
1169                 date = Model.read([id], [last_update], req.context)[0].get(last_update)
1170                 if hashlib.md5(date).hexdigest() == etag:
1171                     return werkzeug.wrappers.Response(status=304)
1172
1173         retag = hashed_session
1174         try:
1175             if not id:
1176                 res = Model.default_get([field], req.context).get(field)
1177                 image_base64 = res
1178             else:
1179                 res = Model.read([id], [last_update, field], req.context)[0]
1180                 retag = hashlib.md5(res.get(last_update)).hexdigest()
1181                 image_base64 = res.get(field)
1182
1183             if kw.get('resize'):
1184                 resize = kw.get('resize').split(',')
1185                 if len(resize) == 2 and int(resize[0]) and int(resize[1]):
1186                     width = int(resize[0])
1187                     height = int(resize[1])
1188                     # resize maximum 500*500
1189                     if width > 500: width = 500
1190                     if height > 500: height = 500
1191                     image_base64 = openerp.tools.image_resize_image(base64_source=image_base64, size=(width, height), encoding='base64', filetype='PNG')
1192             
1193             image_data = base64.b64decode(image_base64)
1194
1195         except (TypeError, xmlrpclib.Fault):
1196             image_data = self.placeholder(req)
1197         headers.append(('ETag', retag))
1198         headers.append(('Content-Length', len(image_data)))
1199         try:
1200             ncache = int(kw.get('cache'))
1201             headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1202         except:
1203             pass
1204         return req.make_response(image_data, headers)
1205     def placeholder(self, req):
1206         addons_path = openerpweb.addons_manifest['web']['addons_path']
1207         return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1208
1209     @openerpweb.httprequest
1210     def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1211         """ Download link for files stored as binary fields.
1212
1213         If the ``id`` parameter is omitted, fetches the default value for the
1214         binary field (via ``default_get``), otherwise fetches the field for
1215         that precise record.
1216
1217         :param req: OpenERP request
1218         :type req: :class:`web.common.http.HttpRequest`
1219         :param str model: name of the model to fetch the binary from
1220         :param str field: binary field
1221         :param str id: id of the record from which to fetch the binary
1222         :param str filename_field: field holding the file's name, if any
1223         :returns: :class:`werkzeug.wrappers.Response`
1224         """
1225         Model = req.session.model(model)
1226         fields = [field]
1227         if filename_field:
1228             fields.append(filename_field)
1229         if id:
1230             res = Model.read([int(id)], fields, req.context)[0]
1231         else:
1232             res = Model.default_get(fields, req.context)
1233         filecontent = base64.b64decode(res.get(field, ''))
1234         if not filecontent:
1235             return req.not_found()
1236         else:
1237             filename = '%s_%s' % (model.replace('.', '_'), id)
1238             if filename_field:
1239                 filename = res.get(filename_field, '') or filename
1240             return req.make_response(filecontent,
1241                 [('Content-Type', 'application/octet-stream'),
1242                  ('Content-Disposition', content_disposition(filename, req))])
1243
1244     @openerpweb.httprequest
1245     def saveas_ajax(self, req, data, token):
1246         jdata = simplejson.loads(data)
1247         model = jdata['model']
1248         field = jdata['field']
1249         id = jdata.get('id', None)
1250         filename_field = jdata.get('filename_field', None)
1251         context = jdata.get('context', {})
1252
1253         Model = req.session.model(model)
1254         fields = [field]
1255         if filename_field:
1256             fields.append(filename_field)
1257         if id:
1258             res = Model.read([int(id)], fields, context)[0]
1259         else:
1260             res = Model.default_get(fields, context)
1261         filecontent = base64.b64decode(res.get(field, ''))
1262         if not filecontent:
1263             raise ValueError(_("No content found for field '%s' on '%s:%s'") %
1264                 (field, model, id))
1265         else:
1266             filename = '%s_%s' % (model.replace('.', '_'), id)
1267             if filename_field:
1268                 filename = res.get(filename_field, '') or filename
1269             return req.make_response(filecontent,
1270                 headers=[('Content-Type', 'application/octet-stream'),
1271                         ('Content-Disposition', content_disposition(filename, req))],
1272                 cookies={'fileToken': int(token)})
1273
1274     @openerpweb.httprequest
1275     def upload(self, req, callback, ufile):
1276         # TODO: might be useful to have a configuration flag for max-length file uploads
1277         try:
1278             out = """<script language="javascript" type="text/javascript">
1279                         var win = window.top.window;
1280                         win.jQuery(win).trigger(%s, %s);
1281                     </script>"""
1282             data = ufile.read()
1283             args = [len(data), ufile.filename,
1284                     ufile.content_type, base64.b64encode(data)]
1285         except Exception, e:
1286             args = [False, e.message]
1287         return out % (simplejson.dumps(callback), simplejson.dumps(args))
1288
1289     @openerpweb.httprequest
1290     def upload_attachment(self, req, callback, model, id, ufile):
1291         Model = req.session.model('ir.attachment')
1292         try:
1293             out = """<script language="javascript" type="text/javascript">
1294                         var win = window.top.window;
1295                         win.jQuery(win).trigger(%s, %s);
1296                     </script>"""
1297             attachment_id = Model.create({
1298                 'name': ufile.filename,
1299                 'datas': base64.encodestring(ufile.read()),
1300                 'datas_fname': ufile.filename,
1301                 'res_model': model,
1302                 'res_id': int(id)
1303             }, req.context)
1304             args = {
1305                 'filename': ufile.filename,
1306                 'id':  attachment_id
1307             }
1308         except Exception,e:
1309             args = {'erorr':e.faultCode.split('--')[1],'title':e.faultCode.split('--')[0]}
1310         return out % (simplejson.dumps(callback), simplejson.dumps(args))
1311
1312 class Action(openerpweb.Controller):
1313     _cp_path = "/web/action"
1314
1315     @openerpweb.jsonrequest
1316     def load(self, req, action_id, do_not_eval=False):
1317         Actions = req.session.model('ir.actions.actions')
1318         value = False
1319         try:
1320             action_id = int(action_id)
1321         except ValueError:
1322             try:
1323                 module, xmlid = action_id.split('.', 1)
1324                 model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
1325                 assert model.startswith('ir.actions.')
1326             except Exception:
1327                 action_id = 0   # force failed read
1328
1329         base_action = Actions.read([action_id], ['type'], req.context)
1330         if base_action:
1331             ctx = {}
1332             action_type = base_action[0]['type']
1333             if action_type == 'ir.actions.report.xml':
1334                 ctx.update({'bin_size': True})
1335             ctx.update(req.context)
1336             action = req.session.model(action_type).read([action_id], False, ctx)
1337             if action:
1338                 value = clean_action(req, action[0])
1339         return value
1340
1341     @openerpweb.jsonrequest
1342     def run(self, req, action_id):
1343         return_action = req.session.model('ir.actions.server').run(
1344             [action_id], req.context)
1345         if return_action:
1346             return clean_action(req, return_action)
1347         else:
1348             return False
1349
1350 class Export(View):
1351     _cp_path = "/web/export"
1352
1353     @openerpweb.jsonrequest
1354     def formats(self, req):
1355         """ Returns all valid export formats
1356
1357         :returns: for each export format, a pair of identifier and printable name
1358         :rtype: [(str, str)]
1359         """
1360         return sorted([
1361             controller.fmt
1362             for path, controller in openerpweb.controllers_path.iteritems()
1363             if path.startswith(self._cp_path)
1364             if hasattr(controller, 'fmt')
1365         ], key=operator.itemgetter("label"))
1366
1367     def fields_get(self, req, model):
1368         Model = req.session.model(model)
1369         fields = Model.fields_get(False, req.context)
1370         return fields
1371
1372     @openerpweb.jsonrequest
1373     def get_fields(self, req, model, prefix='', parent_name= '',
1374                    import_compat=True, parent_field_type=None,
1375                    exclude=None):
1376
1377         if import_compat and parent_field_type == "many2one":
1378             fields = {}
1379         else:
1380             fields = self.fields_get(req, model)
1381
1382         if import_compat:
1383             fields.pop('id', None)
1384         else:
1385             fields['.id'] = fields.pop('id', {'string': 'ID'})
1386
1387         fields_sequence = sorted(fields.iteritems(),
1388             key=lambda field: field[1].get('string', ''))
1389
1390         records = []
1391         for field_name, field in fields_sequence:
1392             if import_compat:
1393                 if exclude and field_name in exclude:
1394                     continue
1395                 if field.get('readonly'):
1396                     # If none of the field's states unsets readonly, skip the field
1397                     if all(dict(attrs).get('readonly', True)
1398                            for attrs in field.get('states', {}).values()):
1399                         continue
1400
1401             id = prefix + (prefix and '/'or '') + field_name
1402             name = parent_name + (parent_name and '/' or '') + field['string']
1403             record = {'id': id, 'string': name,
1404                       'value': id, 'children': False,
1405                       'field_type': field.get('type'),
1406                       'required': field.get('required'),
1407                       'relation_field': field.get('relation_field')}
1408             records.append(record)
1409
1410             if len(name.split('/')) < 3 and 'relation' in field:
1411                 ref = field.pop('relation')
1412                 record['value'] += '/id'
1413                 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1414
1415                 if not import_compat or field['type'] == 'one2many':
1416                     # m2m field in import_compat is childless
1417                     record['children'] = True
1418
1419         return records
1420
1421     @openerpweb.jsonrequest
1422     def namelist(self,req,  model, export_id):
1423         # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1424         export = req.session.model("ir.exports").read([export_id])[0]
1425         export_fields_list = req.session.model("ir.exports.line").read(
1426             export['export_fields'])
1427
1428         fields_data = self.fields_info(
1429             req, model, map(operator.itemgetter('name'), export_fields_list))
1430
1431         return [
1432             {'name': field['name'], 'label': fields_data[field['name']]}
1433             for field in export_fields_list
1434         ]
1435
1436     def fields_info(self, req, model, export_fields):
1437         info = {}
1438         fields = self.fields_get(req, model)
1439         if ".id" in export_fields:
1440             fields['.id'] = fields.pop('id', {'string': 'ID'})
1441             
1442         # To make fields retrieval more efficient, fetch all sub-fields of a
1443         # given field at the same time. Because the order in the export list is
1444         # arbitrary, this requires ordering all sub-fields of a given field
1445         # together so they can be fetched at the same time
1446         #
1447         # Works the following way:
1448         # * sort the list of fields to export, the default sorting order will
1449         #   put the field itself (if present, for xmlid) and all of its
1450         #   sub-fields right after it
1451         # * then, group on: the first field of the path (which is the same for
1452         #   a field and for its subfields and the length of splitting on the
1453         #   first '/', which basically means grouping the field on one side and
1454         #   all of the subfields on the other. This way, we have the field (for
1455         #   the xmlid) with length 1, and all of the subfields with the same
1456         #   base but a length "flag" of 2
1457         # * if we have a normal field (length 1), just add it to the info
1458         #   mapping (with its string) as-is
1459         # * otherwise, recursively call fields_info via graft_subfields.
1460         #   all graft_subfields does is take the result of fields_info (on the
1461         #   field's model) and prepend the current base (current field), which
1462         #   rebuilds the whole sub-tree for the field
1463         #
1464         # result: because we're not fetching the fields_get for half the
1465         # database models, fetching a namelist with a dozen fields (including
1466         # relational data) falls from ~6s to ~300ms (on the leads model).
1467         # export lists with no sub-fields (e.g. import_compatible lists with
1468         # no o2m) are even more efficient (from the same 6s to ~170ms, as
1469         # there's a single fields_get to execute)
1470         for (base, length), subfields in itertools.groupby(
1471                 sorted(export_fields),
1472                 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1473             subfields = list(subfields)
1474             if length == 2:
1475                 # subfields is a seq of $base/*rest, and not loaded yet
1476                 info.update(self.graft_subfields(
1477                     req, fields[base]['relation'], base, fields[base]['string'],
1478                     subfields
1479                 ))
1480             else:
1481                 info[base] = fields[base]['string']
1482
1483         return info
1484
1485     def graft_subfields(self, req, model, prefix, prefix_string, fields):
1486         export_fields = [field.split('/', 1)[1] for field in fields]
1487         return (
1488             (prefix + '/' + k, prefix_string + '/' + v)
1489             for k, v in self.fields_info(req, model, export_fields).iteritems())
1490
1491     #noinspection PyPropertyDefinition
1492     @property
1493     def content_type(self):
1494         """ Provides the format's content type """
1495         raise NotImplementedError()
1496
1497     def filename(self, base):
1498         """ Creates a valid filename for the format (with extension) from the
1499          provided base name (exension-less)
1500         """
1501         raise NotImplementedError()
1502
1503     def from_data(self, fields, rows):
1504         """ Conversion method from OpenERP's export data to whatever the
1505         current export class outputs
1506
1507         :params list fields: a list of fields to export
1508         :params list rows: a list of records to export
1509         :returns:
1510         :rtype: bytes
1511         """
1512         raise NotImplementedError()
1513
1514     @openerpweb.httprequest
1515     def index(self, req, data, token):
1516         model, fields, ids, domain, import_compat = \
1517             operator.itemgetter('model', 'fields', 'ids', 'domain',
1518                                 'import_compat')(
1519                 simplejson.loads(data))
1520
1521         Model = req.session.model(model)
1522         ids = ids or Model.search(domain, 0, False, False, req.context)
1523
1524         field_names = map(operator.itemgetter('name'), fields)
1525         import_data = Model.export_data(ids, field_names, req.context).get('datas',[])
1526
1527         if import_compat:
1528             columns_headers = field_names
1529         else:
1530             columns_headers = [val['label'].strip() for val in fields]
1531
1532
1533         return req.make_response(self.from_data(columns_headers, import_data),
1534             headers=[('Content-Disposition',
1535                             content_disposition(self.filename(model), req)),
1536                      ('Content-Type', self.content_type)],
1537             cookies={'fileToken': int(token)})
1538
1539 class CSVExport(Export):
1540     _cp_path = '/web/export/csv'
1541     fmt = {'tag': 'csv', 'label': 'CSV'}
1542
1543     @property
1544     def content_type(self):
1545         return 'text/csv;charset=utf8'
1546
1547     def filename(self, base):
1548         return base + '.csv'
1549
1550     def from_data(self, fields, rows):
1551         fp = StringIO()
1552         writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1553
1554         writer.writerow([name.encode('utf-8') for name in fields])
1555
1556         for data in rows:
1557             row = []
1558             for d in data:
1559                 if isinstance(d, basestring):
1560                     d = d.replace('\n',' ').replace('\t',' ')
1561                     try:
1562                         d = d.encode('utf-8')
1563                     except UnicodeError:
1564                         pass
1565                 if d is False: d = None
1566                 row.append(d)
1567             writer.writerow(row)
1568
1569         fp.seek(0)
1570         data = fp.read()
1571         fp.close()
1572         return data
1573
1574 class ExcelExport(Export):
1575     _cp_path = '/web/export/xls'
1576     fmt = {
1577         'tag': 'xls',
1578         'label': 'Excel',
1579         'error': None if xlwt else "XLWT required"
1580     }
1581
1582     @property
1583     def content_type(self):
1584         return 'application/vnd.ms-excel'
1585
1586     def filename(self, base):
1587         return base + '.xls'
1588
1589     def from_data(self, fields, rows):
1590         workbook = xlwt.Workbook()
1591         worksheet = workbook.add_sheet('Sheet 1')
1592
1593         for i, fieldname in enumerate(fields):
1594             worksheet.write(0, i, fieldname)
1595             worksheet.col(i).width = 8000 # around 220 pixels
1596
1597         style = xlwt.easyxf('align: wrap yes')
1598
1599         for row_index, row in enumerate(rows):
1600             for cell_index, cell_value in enumerate(row):
1601                 if isinstance(cell_value, basestring):
1602                     cell_value = re.sub("\r", " ", cell_value)
1603                 if cell_value is False: cell_value = None
1604                 worksheet.write(row_index + 1, cell_index, cell_value, style)
1605
1606         fp = StringIO()
1607         workbook.save(fp)
1608         fp.seek(0)
1609         data = fp.read()
1610         fp.close()
1611         return data
1612
1613 class Reports(View):
1614     _cp_path = "/web/report"
1615     POLLING_DELAY = 0.25
1616     TYPES_MAPPING = {
1617         'doc': 'application/vnd.ms-word',
1618         'html': 'text/html',
1619         'odt': 'application/vnd.oasis.opendocument.text',
1620         'pdf': 'application/pdf',
1621         'sxw': 'application/vnd.sun.xml.writer',
1622         'xls': 'application/vnd.ms-excel',
1623     }
1624
1625     @openerpweb.httprequest
1626     def index(self, req, action, token):
1627         action = simplejson.loads(action)
1628
1629         report_srv = req.session.proxy("report")
1630         context = dict(req.context)
1631         context.update(action["context"])
1632
1633         report_data = {}
1634         report_ids = context["active_ids"]
1635         if 'report_type' in action:
1636             report_data['report_type'] = action['report_type']
1637         if 'datas' in action:
1638             if 'ids' in action['datas']:
1639                 report_ids = action['datas'].pop('ids')
1640             report_data.update(action['datas'])
1641
1642         report_id = report_srv.report(
1643             req.session._db, req.session._uid, req.session._password,
1644             action["report_name"], report_ids,
1645             report_data, context)
1646
1647         report_struct = None
1648         while True:
1649             report_struct = report_srv.report_get(
1650                 req.session._db, req.session._uid, req.session._password, report_id)
1651             if report_struct["state"]:
1652                 break
1653
1654             time.sleep(self.POLLING_DELAY)
1655
1656         report = base64.b64decode(report_struct['result'])
1657         if report_struct.get('code') == 'zlib':
1658             report = zlib.decompress(report)
1659         report_mimetype = self.TYPES_MAPPING.get(
1660             report_struct['format'], 'octet-stream')
1661         file_name = action.get('name', 'report')
1662         if 'name' not in action:
1663             reports = req.session.model('ir.actions.report.xml')
1664             res_id = reports.search([('report_name', '=', action['report_name']),],
1665                                     0, False, False, context)
1666             if len(res_id) > 0:
1667                 file_name = reports.read(res_id[0], ['name'], context)['name']
1668             else:
1669                 file_name = action['report_name']
1670         file_name = '%s.%s' % (file_name, report_struct['format'])
1671
1672         return req.make_response(report,
1673              headers=[
1674                  ('Content-Disposition', content_disposition(file_name, req)),
1675                  ('Content-Type', report_mimetype),
1676                  ('Content-Length', len(report))],
1677              cookies={'fileToken': int(token)})
1678
1679 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: