[MERGE] Sync with trunk.
[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 {
697             "version": openerp.release.version
698         }
699
700 class Proxy(openerpweb.Controller):
701     _cp_path = '/web/proxy'
702
703     @openerpweb.jsonrequest
704     def load(self, req, path):
705         """ Proxies an HTTP request through a JSON request.
706
707         It is strongly recommended to not request binary files through this,
708         as the result will be a binary data blob as well.
709
710         :param req: OpenERP request
711         :param path: actual request path
712         :return: file content
713         """
714         from werkzeug.test import Client
715         from werkzeug.wrappers import BaseResponse
716
717         return Client(req.httprequest.app, BaseResponse).get(path).data
718
719 class Database(openerpweb.Controller):
720     _cp_path = "/web/database"
721
722     @openerpweb.jsonrequest
723     def get_list(self, req):
724         return db_list(req)
725
726     @openerpweb.jsonrequest
727     def create(self, req, fields):
728         params = dict(map(operator.itemgetter('name', 'value'), fields))
729         return req.session.proxy("db").create_database(
730             params['super_admin_pwd'],
731             params['db_name'],
732             bool(params.get('demo_data')),
733             params['db_lang'],
734             params['create_admin_pwd'])
735
736     @openerpweb.jsonrequest
737     def duplicate(self, req, fields):
738         params = dict(map(operator.itemgetter('name', 'value'), fields))
739         return req.session.proxy("db").duplicate_database(
740             params['super_admin_pwd'],
741             params['db_original_name'],
742             params['db_name'])
743
744     @openerpweb.jsonrequest
745     def duplicate(self, req, fields):
746         params = dict(map(operator.itemgetter('name', 'value'), fields))
747         duplicate_attrs = (
748             params['super_admin_pwd'],
749             params['db_original_name'],
750             params['db_name'],
751         )
752
753         return req.session.proxy("db").duplicate_database(*duplicate_attrs)
754
755     @openerpweb.jsonrequest
756     def drop(self, req, fields):
757         password, db = operator.itemgetter(
758             'drop_pwd', 'drop_db')(
759                 dict(map(operator.itemgetter('name', 'value'), fields)))
760
761         try:
762             return req.session.proxy("db").drop(password, db)
763         except xmlrpclib.Fault, e:
764             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
765                 return {'error': e.faultCode, 'title': 'Drop Database'}
766         return {'error': _('Could not drop database !'), 'title': _('Drop Database')}
767
768     @openerpweb.httprequest
769     def backup(self, req, backup_db, backup_pwd, token):
770         try:
771             db_dump = base64.b64decode(
772                 req.session.proxy("db").dump(backup_pwd, backup_db))
773             filename = "%(db)s_%(timestamp)s.dump" % {
774                 'db': backup_db,
775                 'timestamp': datetime.datetime.utcnow().strftime(
776                     "%Y-%m-%d_%H-%M-%SZ")
777             }
778             return req.make_response(db_dump,
779                [('Content-Type', 'application/octet-stream; charset=binary'),
780                ('Content-Disposition', content_disposition(filename, req))],
781                {'fileToken': int(token)}
782             )
783         except xmlrpclib.Fault, e:
784             return simplejson.dumps([[],[{'error': e.faultCode, 'title': _('Backup Database')}]])
785
786     @openerpweb.httprequest
787     def restore(self, req, db_file, restore_pwd, new_db):
788         try:
789             data = base64.b64encode(db_file.read())
790             req.session.proxy("db").restore(restore_pwd, new_db, data)
791             return ''
792         except xmlrpclib.Fault, e:
793             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
794                 raise Exception("AccessDenied")
795
796     @openerpweb.jsonrequest
797     def change_password(self, req, fields):
798         old_password, new_password = operator.itemgetter(
799             'old_pwd', 'new_pwd')(
800                 dict(map(operator.itemgetter('name', 'value'), fields)))
801         try:
802             return req.session.proxy("db").change_admin_password(old_password, new_password)
803         except xmlrpclib.Fault, e:
804             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
805                 return {'error': e.faultCode, 'title': _('Change Password')}
806         return {'error': _('Error, password not changed !'), 'title': _('Change Password')}
807
808 class Session(openerpweb.Controller):
809     _cp_path = "/web/session"
810
811     def session_info(self, req):
812         req.session.ensure_valid()
813         return {
814             "session_id": req.session_id,
815             "uid": req.session._uid,
816             "context": req.session.get_context() if req.session._uid else {},
817             "db": req.session._db,
818             "login": req.session._login,
819         }
820
821     @openerpweb.jsonrequest
822     def get_session_info(self, req):
823         return self.session_info(req)
824
825     @openerpweb.jsonrequest
826     def authenticate(self, req, db, login, password, base_location=None):
827         wsgienv = req.httprequest.environ
828         env = dict(
829             base_location=base_location,
830             HTTP_HOST=wsgienv['HTTP_HOST'],
831             REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
832         )
833         req.session.authenticate(db, login, password, env)
834
835         return self.session_info(req)
836
837     @openerpweb.jsonrequest
838     def change_password (self,req,fields):
839         old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
840                 dict(map(operator.itemgetter('name', 'value'), fields)))
841         if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
842             return {'error':_('You cannot leave any password empty.'),'title': _('Change Password')}
843         if new_password != confirm_password:
844             return {'error': _('The new password and its confirmation must be identical.'),'title': _('Change Password')}
845         try:
846             if req.session.model('res.users').change_password(
847                 old_password, new_password):
848                 return {'new_password':new_password}
849         except Exception:
850             return {'error': _('The old password you provided is incorrect, your password was not changed.'), 'title': _('Change Password')}
851         return {'error': _('Error, password not changed !'), 'title': _('Change Password')}
852
853     @openerpweb.jsonrequest
854     def sc_list(self, req):
855         return req.session.model('ir.ui.view_sc').get_sc(
856             req.session._uid, "ir.ui.menu", req.context)
857
858     @openerpweb.jsonrequest
859     def get_lang_list(self, req):
860         try:
861             return req.session.proxy("db").list_lang() or []
862         except Exception, e:
863             return {"error": e, "title": _("Languages")}
864
865     @openerpweb.jsonrequest
866     def modules(self, req):
867         # return all installed modules. Web client is smart enough to not load a module twice
868         return module_installed(req)
869
870     @openerpweb.jsonrequest
871     def save_session_action(self, req, the_action):
872         """
873         This method store an action object in the session object and returns an integer
874         identifying that action. The method get_session_action() can be used to get
875         back the action.
876
877         :param the_action: The action to save in the session.
878         :type the_action: anything
879         :return: A key identifying the saved action.
880         :rtype: integer
881         """
882         saved_actions = req.httpsession.get('saved_actions')
883         if not saved_actions:
884             saved_actions = {"next":0, "actions":{}}
885             req.httpsession['saved_actions'] = saved_actions
886         # we don't allow more than 10 stored actions
887         if len(saved_actions["actions"]) >= 10:
888             del saved_actions["actions"][min(saved_actions["actions"])]
889         key = saved_actions["next"]
890         saved_actions["actions"][key] = the_action
891         saved_actions["next"] = key + 1
892         return key
893
894     @openerpweb.jsonrequest
895     def get_session_action(self, req, key):
896         """
897         Gets back a previously saved action. This method can return None if the action
898         was saved since too much time (this case should be handled in a smart way).
899
900         :param key: The key given by save_session_action()
901         :type key: integer
902         :return: The saved action or None.
903         :rtype: anything
904         """
905         saved_actions = req.httpsession.get('saved_actions')
906         if not saved_actions:
907             return None
908         return saved_actions["actions"].get(key)
909
910     @openerpweb.jsonrequest
911     def check(self, req):
912         req.session.assert_valid()
913         return None
914
915     @openerpweb.jsonrequest
916     def destroy(self, req):
917         req.session._suicide = True
918
919 class Menu(openerpweb.Controller):
920     _cp_path = "/web/menu"
921
922     @openerpweb.jsonrequest
923     def load(self, req):
924         return {'data': self.do_load(req)}
925
926     @openerpweb.jsonrequest
927     def load_needaction(self, req, menu_ids):
928         return {'data': self.do_load_needaction(req, menu_ids)}
929
930     @openerpweb.jsonrequest
931     def get_user_roots(self, req):
932         return self.do_get_user_roots(req)
933
934     def do_get_user_roots(self, req):
935         """ Return all root menu ids visible for the session user.
936
937         :param req: A request object, with an OpenERP session attribute
938         :type req: < session -> OpenERPSession >
939         :return: the root menu ids
940         :rtype: list(int)
941         """
942         s = req.session
943         Menus = s.model('ir.ui.menu')
944         # If a menu action is defined use its domain to get the root menu items
945         user_menu_id = s.model('res.users').read([s._uid], ['menu_id'],
946                                                  req.context)[0]['menu_id']
947
948         menu_domain = [('parent_id', '=', False)]
949         if user_menu_id:
950             domain_string = s.model('ir.actions.act_window').read(
951                 [user_menu_id[0]], ['domain'],req.context)[0]['domain']
952             if domain_string:
953                 menu_domain = ast.literal_eval(domain_string)
954
955         return Menus.search(menu_domain, 0, False, False, req.context)
956
957     def do_load(self, req):
958         """ Loads all menu items (all applications and their sub-menus).
959
960         :param req: A request object, with an OpenERP session attribute
961         :type req: < session -> OpenERPSession >
962         :return: the menu root
963         :rtype: dict('children': menu_nodes)
964         """
965         Menus = req.session.model('ir.ui.menu')
966
967         fields = ['name', 'sequence', 'parent_id', 'action',
968                   'needaction_enabled']
969         menu_roots = Menus.read(self.do_get_user_roots(req), fields, req.context)
970         menu_root = {
971             'id': False,
972             'name': 'root',
973             'parent_id': [-1, ''],
974             'children': menu_roots
975         }
976
977         # menus are loaded fully unlike a regular tree view, cause there are a
978         # limited number of items (752 when all 6.1 addons are installed)
979         menu_ids = Menus.search([], 0, False, False, req.context)
980         menu_items = Menus.read(menu_ids, fields, req.context)
981         # adds roots at the end of the sequence, so that they will overwrite
982         # equivalent menu items from full menu read when put into id:item
983         # mapping, resulting in children being correctly set on the roots.
984         menu_items.extend(menu_roots)
985
986         # make a tree using parent_id
987         menu_items_map = dict(
988             (menu_item["id"], menu_item) for menu_item in menu_items)
989         for menu_item in menu_items:
990             if menu_item['parent_id']:
991                 parent = menu_item['parent_id'][0]
992             else:
993                 parent = False
994             if parent in menu_items_map:
995                 menu_items_map[parent].setdefault(
996                     'children', []).append(menu_item)
997
998         # sort by sequence a tree using parent_id
999         for menu_item in menu_items:
1000             menu_item.setdefault('children', []).sort(
1001                 key=operator.itemgetter('sequence'))
1002
1003         return menu_root
1004
1005     def do_load_needaction(self, req, menu_ids=False):
1006         """ Loads needaction counters for all or some specific menu ids.
1007
1008             :param req: A request object, with an OpenERP session attribute
1009             :type req: < session -> OpenERPSession >
1010             :return: the menu root
1011             :rtype: dict('children': menu_nodes)
1012         """
1013         Menus = req.session.model('ir.ui.menu')
1014
1015         if menu_ids == False:
1016             menu_ids = Menus.search([('needaction_enabled', '=', True)], context=req.context)
1017
1018         menu_needaction_data = Menus.get_needaction_data(menu_ids, req.context)
1019         return menu_needaction_data
1020
1021     @openerpweb.jsonrequest
1022     def action(self, req, menu_id):
1023         actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
1024                                              [('ir.ui.menu', menu_id)], False)
1025         return {"action": actions}
1026
1027 class DataSet(openerpweb.Controller):
1028     _cp_path = "/web/dataset"
1029
1030     @openerpweb.jsonrequest
1031     def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
1032         return self.do_search_read(req, model, fields, offset, limit, domain, sort)
1033     def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
1034                        , sort=None):
1035         """ Performs a search() followed by a read() (if needed) using the
1036         provided search criteria
1037
1038         :param req: a JSON-RPC request object
1039         :type req: openerpweb.JsonRequest
1040         :param str model: the name of the model to search on
1041         :param fields: a list of the fields to return in the result records
1042         :type fields: [str]
1043         :param int offset: from which index should the results start being returned
1044         :param int limit: the maximum number of records to return
1045         :param list domain: the search domain for the query
1046         :param list sort: sorting directives
1047         :returns: A structure (dict) with two keys: ids (all the ids matching
1048                   the (domain, context) pair) and records (paginated records
1049                   matching fields selection set)
1050         :rtype: list
1051         """
1052         Model = req.session.model(model)
1053
1054         ids = Model.search(domain, offset or 0, limit or False, sort or False,
1055                            req.context)
1056         if limit and len(ids) == limit:
1057             length = Model.search_count(domain, req.context)
1058         else:
1059             length = len(ids) + (offset or 0)
1060         if fields and fields == ['id']:
1061             # shortcut read if we only want the ids
1062             return {
1063                 'length': length,
1064                 'records': [{'id': id} for id in ids]
1065             }
1066
1067         records = Model.read(ids, fields or False, req.context)
1068         records.sort(key=lambda obj: ids.index(obj['id']))
1069         return {
1070             'length': length,
1071             'records': records
1072         }
1073
1074     @openerpweb.jsonrequest
1075     def load(self, req, model, id, fields):
1076         m = req.session.model(model)
1077         value = {}
1078         r = m.read([id], False, req.context)
1079         if r:
1080             value = r[0]
1081         return {'value': value}
1082
1083     def call_common(self, req, model, method, args, domain_id=None, context_id=None):
1084         return self._call_kw(req, model, method, args, {})
1085
1086     def _call_kw(self, req, model, method, args, kwargs):
1087         # Temporary implements future display_name special field for model#read()
1088         if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
1089             if 'display_name' in args[1]:
1090                 names = req.session.model(model).name_get(args[0], **kwargs)
1091                 args[1].remove('display_name')
1092                 r = getattr(req.session.model(model), method)(*args, **kwargs)
1093                 for i in range(len(r)):
1094                     r[i]['display_name'] = names[i][1] or "%s#%d" % (model, names[i][0])
1095                 return r
1096
1097         return getattr(req.session.model(model), method)(*args, **kwargs)
1098
1099     @openerpweb.jsonrequest
1100     def call(self, req, model, method, args, domain_id=None, context_id=None):
1101         return self._call_kw(req, model, method, args, {})
1102     
1103     @openerpweb.jsonrequest
1104     def call_kw(self, req, model, method, args, kwargs):
1105         return self._call_kw(req, model, method, args, kwargs)
1106
1107     @openerpweb.jsonrequest
1108     def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1109         action = self._call_kw(req, model, method, args, {})
1110         if isinstance(action, dict) and action.get('type') != '':
1111             return clean_action(req, action)
1112         return False
1113
1114     @openerpweb.jsonrequest
1115     def exec_workflow(self, req, model, id, signal):
1116         return req.session.exec_workflow(model, id, signal)
1117
1118     @openerpweb.jsonrequest
1119     def resequence(self, req, model, ids, field='sequence', offset=0):
1120         """ Re-sequences a number of records in the model, by their ids
1121
1122         The re-sequencing starts at the first model of ``ids``, the sequence
1123         number is incremented by one after each record and starts at ``offset``
1124
1125         :param ids: identifiers of the records to resequence, in the new sequence order
1126         :type ids: list(id)
1127         :param str field: field used for sequence specification, defaults to
1128                           "sequence"
1129         :param int offset: sequence number for first record in ``ids``, allows
1130                            starting the resequencing from an arbitrary number,
1131                            defaults to ``0``
1132         """
1133         m = req.session.model(model)
1134         if not m.fields_get([field]):
1135             return False
1136         # python 2.6 has no start parameter
1137         for i, id in enumerate(ids):
1138             m.write(id, { field: i + offset })
1139         return True
1140
1141 class View(openerpweb.Controller):
1142     _cp_path = "/web/view"
1143
1144     @openerpweb.jsonrequest
1145     def add_custom(self, req, view_id, arch):
1146         CustomView = req.session.model('ir.ui.view.custom')
1147         CustomView.create({
1148             'user_id': req.session._uid,
1149             'ref_id': view_id,
1150             'arch': arch
1151         }, req.context)
1152         return {'result': True}
1153
1154     @openerpweb.jsonrequest
1155     def undo_custom(self, req, view_id, reset=False):
1156         CustomView = req.session.model('ir.ui.view.custom')
1157         vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1158                                     0, False, False, req.context)
1159         if vcustom:
1160             if reset:
1161                 CustomView.unlink(vcustom, req.context)
1162             else:
1163                 CustomView.unlink([vcustom[0]], req.context)
1164             return {'result': True}
1165         return {'result': False}
1166
1167 class TreeView(View):
1168     _cp_path = "/web/treeview"
1169
1170     @openerpweb.jsonrequest
1171     def action(self, req, model, id):
1172         return load_actions_from_ir_values(
1173             req,'action', 'tree_but_open',[(model, id)],
1174             False)
1175
1176 class Binary(openerpweb.Controller):
1177     _cp_path = "/web/binary"
1178
1179     @openerpweb.httprequest
1180     def image(self, req, model, id, field, **kw):
1181         last_update = '__last_update'
1182         Model = req.session.model(model)
1183         headers = [('Content-Type', 'image/png')]
1184         etag = req.httprequest.headers.get('If-None-Match')
1185         hashed_session = hashlib.md5(req.session_id).hexdigest()
1186         id = None if not id else simplejson.loads(id)
1187         if type(id) is list:
1188             id = id[0] # m2o
1189         if etag:
1190             if not id and hashed_session == etag:
1191                 return werkzeug.wrappers.Response(status=304)
1192             else:
1193                 date = Model.read([id], [last_update], req.context)[0].get(last_update)
1194                 if hashlib.md5(date).hexdigest() == etag:
1195                     return werkzeug.wrappers.Response(status=304)
1196
1197         retag = hashed_session
1198         try:
1199             if not id:
1200                 res = Model.default_get([field], req.context).get(field)
1201                 image_base64 = res
1202             else:
1203                 res = Model.read([id], [last_update, field], req.context)[0]
1204                 retag = hashlib.md5(res.get(last_update)).hexdigest()
1205                 image_base64 = res.get(field)
1206
1207             if kw.get('resize'):
1208                 resize = kw.get('resize').split(',')
1209                 if len(resize) == 2 and int(resize[0]) and int(resize[1]):
1210                     width = int(resize[0])
1211                     height = int(resize[1])
1212                     # resize maximum 500*500
1213                     if width > 500: width = 500
1214                     if height > 500: height = 500
1215                     image_base64 = openerp.tools.image_resize_image(base64_source=image_base64, size=(width, height), encoding='base64', filetype='PNG')
1216             
1217             image_data = base64.b64decode(image_base64)
1218
1219         except (TypeError, xmlrpclib.Fault):
1220             image_data = self.placeholder(req)
1221         headers.append(('ETag', retag))
1222         headers.append(('Content-Length', len(image_data)))
1223         try:
1224             ncache = int(kw.get('cache'))
1225             headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1226         except:
1227             pass
1228         return req.make_response(image_data, headers)
1229     def placeholder(self, req):
1230         addons_path = openerpweb.addons_manifest['web']['addons_path']
1231         return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1232
1233     @openerpweb.httprequest
1234     def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1235         """ Download link for files stored as binary fields.
1236
1237         If the ``id`` parameter is omitted, fetches the default value for the
1238         binary field (via ``default_get``), otherwise fetches the field for
1239         that precise record.
1240
1241         :param req: OpenERP request
1242         :type req: :class:`web.common.http.HttpRequest`
1243         :param str model: name of the model to fetch the binary from
1244         :param str field: binary field
1245         :param str id: id of the record from which to fetch the binary
1246         :param str filename_field: field holding the file's name, if any
1247         :returns: :class:`werkzeug.wrappers.Response`
1248         """
1249         Model = req.session.model(model)
1250         fields = [field]
1251         if filename_field:
1252             fields.append(filename_field)
1253         if id:
1254             res = Model.read([int(id)], fields, req.context)[0]
1255         else:
1256             res = Model.default_get(fields, req.context)
1257         filecontent = base64.b64decode(res.get(field, ''))
1258         if not filecontent:
1259             return req.not_found()
1260         else:
1261             filename = '%s_%s' % (model.replace('.', '_'), id)
1262             if filename_field:
1263                 filename = res.get(filename_field, '') or filename
1264             return req.make_response(filecontent,
1265                 [('Content-Type', 'application/octet-stream'),
1266                  ('Content-Disposition', content_disposition(filename, req))])
1267
1268     @openerpweb.httprequest
1269     def saveas_ajax(self, req, data, token):
1270         jdata = simplejson.loads(data)
1271         model = jdata['model']
1272         field = jdata['field']
1273         id = jdata.get('id', None)
1274         filename_field = jdata.get('filename_field', None)
1275         context = jdata.get('context', {})
1276
1277         Model = req.session.model(model)
1278         fields = [field]
1279         if filename_field:
1280             fields.append(filename_field)
1281         if id:
1282             res = Model.read([int(id)], fields, context)[0]
1283         else:
1284             res = Model.default_get(fields, context)
1285         filecontent = base64.b64decode(res.get(field, ''))
1286         if not filecontent:
1287             raise ValueError(_("No content found for field '%s' on '%s:%s'") %
1288                 (field, model, id))
1289         else:
1290             filename = '%s_%s' % (model.replace('.', '_'), id)
1291             if filename_field:
1292                 filename = res.get(filename_field, '') or filename
1293             return req.make_response(filecontent,
1294                 headers=[('Content-Type', 'application/octet-stream'),
1295                         ('Content-Disposition', content_disposition(filename, req))],
1296                 cookies={'fileToken': int(token)})
1297
1298     @openerpweb.httprequest
1299     def upload(self, req, callback, ufile):
1300         # TODO: might be useful to have a configuration flag for max-length file uploads
1301         try:
1302             out = """<script language="javascript" type="text/javascript">
1303                         var win = window.top.window;
1304                         win.jQuery(win).trigger(%s, %s);
1305                     </script>"""
1306             data = ufile.read()
1307             args = [len(data), ufile.filename,
1308                     ufile.content_type, base64.b64encode(data)]
1309         except Exception, e:
1310             args = [False, e.message]
1311         return out % (simplejson.dumps(callback), simplejson.dumps(args))
1312
1313     @openerpweb.httprequest
1314     def upload_attachment(self, req, callback, model, id, ufile):
1315         Model = req.session.model('ir.attachment')
1316         try:
1317             out = """<script language="javascript" type="text/javascript">
1318                         var win = window.top.window;
1319                         win.jQuery(win).trigger(%s, %s);
1320                     </script>"""
1321             attachment_id = Model.create({
1322                 'name': ufile.filename,
1323                 'datas': base64.encodestring(ufile.read()),
1324                 'datas_fname': ufile.filename,
1325                 'res_model': model,
1326                 'res_id': int(id)
1327             }, req.context)
1328             args = {
1329                 'filename': ufile.filename,
1330                 'id':  attachment_id
1331             }
1332         except Exception,e:
1333             args = {'erorr':e.faultCode.split('--')[1],'title':e.faultCode.split('--')[0]}
1334         return out % (simplejson.dumps(callback), simplejson.dumps(args))
1335
1336 class Action(openerpweb.Controller):
1337     _cp_path = "/web/action"
1338
1339     @openerpweb.jsonrequest
1340     def load(self, req, action_id, do_not_eval=False):
1341         Actions = req.session.model('ir.actions.actions')
1342         value = False
1343         try:
1344             action_id = int(action_id)
1345         except ValueError:
1346             try:
1347                 module, xmlid = action_id.split('.', 1)
1348                 model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
1349                 assert model.startswith('ir.actions.')
1350             except Exception:
1351                 action_id = 0   # force failed read
1352
1353         base_action = Actions.read([action_id], ['type'], req.context)
1354         if base_action:
1355             ctx = {}
1356             action_type = base_action[0]['type']
1357             if action_type == 'ir.actions.report.xml':
1358                 ctx.update({'bin_size': True})
1359             ctx.update(req.context)
1360             action = req.session.model(action_type).read([action_id], False, ctx)
1361             if action:
1362                 value = clean_action(req, action[0])
1363         return value
1364
1365     @openerpweb.jsonrequest
1366     def run(self, req, action_id):
1367         return_action = req.session.model('ir.actions.server').run(
1368             [action_id], req.context)
1369         if return_action:
1370             return clean_action(req, return_action)
1371         else:
1372             return False
1373
1374 class Export(View):
1375     _cp_path = "/web/export"
1376
1377     @openerpweb.jsonrequest
1378     def formats(self, req):
1379         """ Returns all valid export formats
1380
1381         :returns: for each export format, a pair of identifier and printable name
1382         :rtype: [(str, str)]
1383         """
1384         return sorted([
1385             controller.fmt
1386             for path, controller in openerpweb.controllers_path.iteritems()
1387             if path.startswith(self._cp_path)
1388             if hasattr(controller, 'fmt')
1389         ], key=operator.itemgetter("label"))
1390
1391     def fields_get(self, req, model):
1392         Model = req.session.model(model)
1393         fields = Model.fields_get(False, req.context)
1394         return fields
1395
1396     @openerpweb.jsonrequest
1397     def get_fields(self, req, model, prefix='', parent_name= '',
1398                    import_compat=True, parent_field_type=None,
1399                    exclude=None):
1400
1401         if import_compat and parent_field_type == "many2one":
1402             fields = {}
1403         else:
1404             fields = self.fields_get(req, model)
1405
1406         if import_compat:
1407             fields.pop('id', None)
1408         else:
1409             fields['.id'] = fields.pop('id', {'string': 'ID'})
1410
1411         fields_sequence = sorted(fields.iteritems(),
1412             key=lambda field: field[1].get('string', ''))
1413
1414         records = []
1415         for field_name, field in fields_sequence:
1416             if import_compat:
1417                 if exclude and field_name in exclude:
1418                     continue
1419                 if field.get('readonly'):
1420                     # If none of the field's states unsets readonly, skip the field
1421                     if all(dict(attrs).get('readonly', True)
1422                            for attrs in field.get('states', {}).values()):
1423                         continue
1424
1425             id = prefix + (prefix and '/'or '') + field_name
1426             name = parent_name + (parent_name and '/' or '') + field['string']
1427             record = {'id': id, 'string': name,
1428                       'value': id, 'children': False,
1429                       'field_type': field.get('type'),
1430                       'required': field.get('required'),
1431                       'relation_field': field.get('relation_field')}
1432             records.append(record)
1433
1434             if len(name.split('/')) < 3 and 'relation' in field:
1435                 ref = field.pop('relation')
1436                 record['value'] += '/id'
1437                 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1438
1439                 if not import_compat or field['type'] == 'one2many':
1440                     # m2m field in import_compat is childless
1441                     record['children'] = True
1442
1443         return records
1444
1445     @openerpweb.jsonrequest
1446     def namelist(self,req,  model, export_id):
1447         # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1448         export = req.session.model("ir.exports").read([export_id])[0]
1449         export_fields_list = req.session.model("ir.exports.line").read(
1450             export['export_fields'])
1451
1452         fields_data = self.fields_info(
1453             req, model, map(operator.itemgetter('name'), export_fields_list))
1454
1455         return [
1456             {'name': field['name'], 'label': fields_data[field['name']]}
1457             for field in export_fields_list
1458         ]
1459
1460     def fields_info(self, req, model, export_fields):
1461         info = {}
1462         fields = self.fields_get(req, model)
1463         if ".id" in export_fields:
1464             fields['.id'] = fields.pop('id', {'string': 'ID'})
1465             
1466         # To make fields retrieval more efficient, fetch all sub-fields of a
1467         # given field at the same time. Because the order in the export list is
1468         # arbitrary, this requires ordering all sub-fields of a given field
1469         # together so they can be fetched at the same time
1470         #
1471         # Works the following way:
1472         # * sort the list of fields to export, the default sorting order will
1473         #   put the field itself (if present, for xmlid) and all of its
1474         #   sub-fields right after it
1475         # * then, group on: the first field of the path (which is the same for
1476         #   a field and for its subfields and the length of splitting on the
1477         #   first '/', which basically means grouping the field on one side and
1478         #   all of the subfields on the other. This way, we have the field (for
1479         #   the xmlid) with length 1, and all of the subfields with the same
1480         #   base but a length "flag" of 2
1481         # * if we have a normal field (length 1), just add it to the info
1482         #   mapping (with its string) as-is
1483         # * otherwise, recursively call fields_info via graft_subfields.
1484         #   all graft_subfields does is take the result of fields_info (on the
1485         #   field's model) and prepend the current base (current field), which
1486         #   rebuilds the whole sub-tree for the field
1487         #
1488         # result: because we're not fetching the fields_get for half the
1489         # database models, fetching a namelist with a dozen fields (including
1490         # relational data) falls from ~6s to ~300ms (on the leads model).
1491         # export lists with no sub-fields (e.g. import_compatible lists with
1492         # no o2m) are even more efficient (from the same 6s to ~170ms, as
1493         # there's a single fields_get to execute)
1494         for (base, length), subfields in itertools.groupby(
1495                 sorted(export_fields),
1496                 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1497             subfields = list(subfields)
1498             if length == 2:
1499                 # subfields is a seq of $base/*rest, and not loaded yet
1500                 info.update(self.graft_subfields(
1501                     req, fields[base]['relation'], base, fields[base]['string'],
1502                     subfields
1503                 ))
1504             else:
1505                 info[base] = fields[base]['string']
1506
1507         return info
1508
1509     def graft_subfields(self, req, model, prefix, prefix_string, fields):
1510         export_fields = [field.split('/', 1)[1] for field in fields]
1511         return (
1512             (prefix + '/' + k, prefix_string + '/' + v)
1513             for k, v in self.fields_info(req, model, export_fields).iteritems())
1514
1515     #noinspection PyPropertyDefinition
1516     @property
1517     def content_type(self):
1518         """ Provides the format's content type """
1519         raise NotImplementedError()
1520
1521     def filename(self, base):
1522         """ Creates a valid filename for the format (with extension) from the
1523          provided base name (exension-less)
1524         """
1525         raise NotImplementedError()
1526
1527     def from_data(self, fields, rows):
1528         """ Conversion method from OpenERP's export data to whatever the
1529         current export class outputs
1530
1531         :params list fields: a list of fields to export
1532         :params list rows: a list of records to export
1533         :returns:
1534         :rtype: bytes
1535         """
1536         raise NotImplementedError()
1537
1538     @openerpweb.httprequest
1539     def index(self, req, data, token):
1540         model, fields, ids, domain, import_compat = \
1541             operator.itemgetter('model', 'fields', 'ids', 'domain',
1542                                 'import_compat')(
1543                 simplejson.loads(data))
1544
1545         Model = req.session.model(model)
1546         ids = ids or Model.search(domain, 0, False, False, req.context)
1547
1548         field_names = map(operator.itemgetter('name'), fields)
1549         import_data = Model.export_data(ids, field_names, req.context).get('datas',[])
1550
1551         if import_compat:
1552             columns_headers = field_names
1553         else:
1554             columns_headers = [val['label'].strip() for val in fields]
1555
1556
1557         return req.make_response(self.from_data(columns_headers, import_data),
1558             headers=[('Content-Disposition',
1559                             content_disposition(self.filename(model), req)),
1560                      ('Content-Type', self.content_type)],
1561             cookies={'fileToken': int(token)})
1562
1563 class CSVExport(Export):
1564     _cp_path = '/web/export/csv'
1565     fmt = {'tag': 'csv', 'label': 'CSV'}
1566
1567     @property
1568     def content_type(self):
1569         return 'text/csv;charset=utf8'
1570
1571     def filename(self, base):
1572         return base + '.csv'
1573
1574     def from_data(self, fields, rows):
1575         fp = StringIO()
1576         writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1577
1578         writer.writerow([name.encode('utf-8') for name in fields])
1579
1580         for data in rows:
1581             row = []
1582             for d in data:
1583                 if isinstance(d, basestring):
1584                     d = d.replace('\n',' ').replace('\t',' ')
1585                     try:
1586                         d = d.encode('utf-8')
1587                     except UnicodeError:
1588                         pass
1589                 if d is False: d = None
1590                 row.append(d)
1591             writer.writerow(row)
1592
1593         fp.seek(0)
1594         data = fp.read()
1595         fp.close()
1596         return data
1597
1598 class ExcelExport(Export):
1599     _cp_path = '/web/export/xls'
1600     fmt = {
1601         'tag': 'xls',
1602         'label': 'Excel',
1603         'error': None if xlwt else "XLWT required"
1604     }
1605
1606     @property
1607     def content_type(self):
1608         return 'application/vnd.ms-excel'
1609
1610     def filename(self, base):
1611         return base + '.xls'
1612
1613     def from_data(self, fields, rows):
1614         workbook = xlwt.Workbook()
1615         worksheet = workbook.add_sheet('Sheet 1')
1616
1617         for i, fieldname in enumerate(fields):
1618             worksheet.write(0, i, fieldname)
1619             worksheet.col(i).width = 8000 # around 220 pixels
1620
1621         style = xlwt.easyxf('align: wrap yes')
1622
1623         for row_index, row in enumerate(rows):
1624             for cell_index, cell_value in enumerate(row):
1625                 if isinstance(cell_value, basestring):
1626                     cell_value = re.sub("\r", " ", cell_value)
1627                 if cell_value is False: cell_value = None
1628                 worksheet.write(row_index + 1, cell_index, cell_value, style)
1629
1630         fp = StringIO()
1631         workbook.save(fp)
1632         fp.seek(0)
1633         data = fp.read()
1634         fp.close()
1635         return data
1636
1637 class Reports(View):
1638     _cp_path = "/web/report"
1639     POLLING_DELAY = 0.25
1640     TYPES_MAPPING = {
1641         'doc': 'application/vnd.ms-word',
1642         'html': 'text/html',
1643         'odt': 'application/vnd.oasis.opendocument.text',
1644         'pdf': 'application/pdf',
1645         'sxw': 'application/vnd.sun.xml.writer',
1646         'xls': 'application/vnd.ms-excel',
1647     }
1648
1649     @openerpweb.httprequest
1650     def index(self, req, action, token):
1651         action = simplejson.loads(action)
1652
1653         report_srv = req.session.proxy("report")
1654         context = dict(req.context)
1655         context.update(action["context"])
1656
1657         report_data = {}
1658         report_ids = context["active_ids"]
1659         if 'report_type' in action:
1660             report_data['report_type'] = action['report_type']
1661         if 'datas' in action:
1662             if 'ids' in action['datas']:
1663                 report_ids = action['datas'].pop('ids')
1664             report_data.update(action['datas'])
1665
1666         report_id = report_srv.report(
1667             req.session._db, req.session._uid, req.session._password,
1668             action["report_name"], report_ids,
1669             report_data, context)
1670
1671         report_struct = None
1672         while True:
1673             report_struct = report_srv.report_get(
1674                 req.session._db, req.session._uid, req.session._password, report_id)
1675             if report_struct["state"]:
1676                 break
1677
1678             time.sleep(self.POLLING_DELAY)
1679
1680         report = base64.b64decode(report_struct['result'])
1681         if report_struct.get('code') == 'zlib':
1682             report = zlib.decompress(report)
1683         report_mimetype = self.TYPES_MAPPING.get(
1684             report_struct['format'], 'octet-stream')
1685         file_name = action.get('name', 'report')
1686         if 'name' not in action:
1687             reports = req.session.model('ir.actions.report.xml')
1688             res_id = reports.search([('report_name', '=', action['report_name']),],
1689                                     0, False, False, context)
1690             if len(res_id) > 0:
1691                 file_name = reports.read(res_id[0], ['name'], context)['name']
1692             else:
1693                 file_name = action['report_name']
1694         file_name = '%s.%s' % (file_name, report_struct['format'])
1695
1696         return req.make_response(report,
1697              headers=[
1698                  ('Content-Disposition', content_disposition(file_name, req)),
1699                  ('Content-Type', report_mimetype),
1700                  ('Content-Length', len(report))],
1701              cookies={'fileToken': int(token)})
1702
1703 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: