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