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