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