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