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