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