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