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