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