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