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