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