merge upstream
[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 req.session.proxy('common').version()['openerp']
749
750 class Proxy(openerpweb.Controller):
751     _cp_path = '/web/proxy'
752
753     @openerpweb.jsonrequest
754     def load(self, req, path):
755         """ Proxies an HTTP request through a JSON request.
756
757         It is strongly recommended to not request binary files through this,
758         as the result will be a binary data blob as well.
759
760         :param req: OpenERP request
761         :param path: actual request path
762         :return: file content
763         """
764         from werkzeug.test import Client
765         from werkzeug.wrappers import BaseResponse
766
767         return Client(req.httprequest.app, BaseResponse).get(path).data
768
769 class Database(openerpweb.Controller):
770     _cp_path = "/web/database"
771
772     @openerpweb.jsonrequest
773     def get_list(self, req):
774         dbs = db_list(req)
775         return {"db_list": dbs}
776
777     @openerpweb.jsonrequest
778     def create(self, req, fields):
779         params = dict(map(operator.itemgetter('name', 'value'), fields))
780         create_attrs = (
781             params['super_admin_pwd'],
782             params['db_name'],
783             bool(params.get('demo_data')),
784             params['db_lang'],
785             params['create_admin_pwd']
786         )
787
788         return req.session.proxy("db").create_database(*create_attrs)
789
790     @openerpweb.jsonrequest
791     def drop(self, req, fields):
792         password, db = operator.itemgetter(
793             'drop_pwd', 'drop_db')(
794                 dict(map(operator.itemgetter('name', 'value'), fields)))
795
796         try:
797             return req.session.proxy("db").drop(password, db)
798         except xmlrpclib.Fault, e:
799             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
800                 return {'error': e.faultCode, 'title': 'Drop Database'}
801         return {'error': 'Could not drop database !', 'title': 'Drop Database'}
802
803     @openerpweb.httprequest
804     def backup(self, req, backup_db, backup_pwd, token):
805         try:
806             db_dump = base64.b64decode(
807                 req.session.proxy("db").dump(backup_pwd, backup_db))
808             filename = "%(db)s_%(timestamp)s.dump" % {
809                 'db': backup_db,
810                 'timestamp': datetime.datetime.utcnow().strftime(
811                     "%Y-%m-%d_%H-%M-%SZ")
812             }
813             return req.make_response(db_dump,
814                [('Content-Type', 'application/octet-stream; charset=binary'),
815                ('Content-Disposition', 'attachment; filename="' + filename + '"')],
816                {'fileToken': int(token)}
817             )
818         except xmlrpclib.Fault, e:
819             return simplejson.dumps([[],[{'error': e.faultCode, 'title': 'backup Database'}]])
820
821     @openerpweb.httprequest
822     def restore(self, req, db_file, restore_pwd, new_db):
823         try:
824             data = base64.b64encode(db_file.read())
825             req.session.proxy("db").restore(restore_pwd, new_db, data)
826             return ''
827         except xmlrpclib.Fault, e:
828             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
829                 raise Exception("AccessDenied")
830
831     @openerpweb.jsonrequest
832     def change_password(self, req, fields):
833         old_password, new_password = operator.itemgetter(
834             'old_pwd', 'new_pwd')(
835                 dict(map(operator.itemgetter('name', 'value'), fields)))
836         try:
837             return req.session.proxy("db").change_admin_password(old_password, new_password)
838         except xmlrpclib.Fault, e:
839             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
840                 return {'error': e.faultCode, 'title': 'Change Password'}
841         return {'error': 'Error, password not changed !', 'title': 'Change Password'}
842
843 class Session(openerpweb.Controller):
844     _cp_path = "/web/session"
845
846     def session_info(self, req):
847         req.session.ensure_valid()
848         return {
849             "session_id": req.session_id,
850             "uid": req.session._uid,
851             "context": req.session.get_context() if req.session._uid else {},
852             "db": req.session._db,
853             "login": req.session._login,
854         }
855
856     @openerpweb.jsonrequest
857     def get_session_info(self, req):
858         return self.session_info(req)
859
860     @openerpweb.jsonrequest
861     def authenticate(self, req, db, login, password, base_location=None):
862         wsgienv = req.httprequest.environ
863         release = common.release
864         env = dict(
865             base_location=base_location,
866             HTTP_HOST=wsgienv['HTTP_HOST'],
867             REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
868             user_agent="%s / %s" % (release.name, release.version),
869         )
870         req.session.authenticate(db, login, password, env)
871
872         return self.session_info(req)
873
874     @openerpweb.jsonrequest
875     def change_password (self,req,fields):
876         old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
877                 dict(map(operator.itemgetter('name', 'value'), fields)))
878         if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
879             return {'error':'All passwords have to be filled.','title': 'Change Password'}
880         if new_password != confirm_password:
881             return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
882         try:
883             if req.session.model('res.users').change_password(
884                 old_password, new_password):
885                 return {'new_password':new_password}
886         except Exception:
887             return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
888         return {'error': 'Error, password not changed !', 'title': 'Change Password'}
889
890     @openerpweb.jsonrequest
891     def sc_list(self, req):
892         return req.session.model('ir.ui.view_sc').get_sc(
893             req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
894
895     @openerpweb.jsonrequest
896     def get_lang_list(self, req):
897         try:
898             return {
899                 'lang_list': (req.session.proxy("db").list_lang() or []),
900                 'error': ""
901             }
902         except Exception, e:
903             return {"error": e, "title": "Languages"}
904
905     @openerpweb.jsonrequest
906     def modules(self, req):
907         # return all installed modules. Web client is smart enough to not load a module twice
908         return module_installed(req)
909
910     @openerpweb.jsonrequest
911     def eval_domain_and_context(self, req, contexts, domains,
912                                 group_by_seq=None):
913         """ Evaluates sequences of domains and contexts, composing them into
914         a single context, domain or group_by sequence.
915
916         :param list contexts: list of contexts to merge together. Contexts are
917                               evaluated in sequence, all previous contexts
918                               are part of their own evaluation context
919                               (starting at the session context).
920         :param list domains: list of domains to merge together. Domains are
921                              evaluated in sequence and appended to one another
922                              (implicit AND), their evaluation domain is the
923                              result of merging all contexts.
924         :param list group_by_seq: list of domains (which may be in a different
925                                   order than the ``contexts`` parameter),
926                                   evaluated in sequence, their ``'group_by'``
927                                   key is extracted if they have one.
928         :returns:
929             a 3-dict of:
930
931             context (``dict``)
932                 the global context created by merging all of
933                 ``contexts``
934
935             domain (``list``)
936                 the concatenation of all domains
937
938             group_by (``list``)
939                 a list of fields to group by, potentially empty (in which case
940                 no group by should be performed)
941         """
942         context, domain = eval_context_and_domain(req.session,
943                                                   common.nonliterals.CompoundContext(*(contexts or [])),
944                                                   common.nonliterals.CompoundDomain(*(domains or [])))
945
946         group_by_sequence = []
947         for candidate in (group_by_seq or []):
948             ctx = req.session.eval_context(candidate, context)
949             group_by = ctx.get('group_by')
950             if not group_by:
951                 continue
952             elif isinstance(group_by, basestring):
953                 group_by_sequence.append(group_by)
954             else:
955                 group_by_sequence.extend(group_by)
956
957         return {
958             'context': context,
959             'domain': domain,
960             'group_by': group_by_sequence
961         }
962
963     @openerpweb.jsonrequest
964     def save_session_action(self, req, the_action):
965         """
966         This method store an action object in the session object and returns an integer
967         identifying that action. The method get_session_action() can be used to get
968         back the action.
969
970         :param the_action: The action to save in the session.
971         :type the_action: anything
972         :return: A key identifying the saved action.
973         :rtype: integer
974         """
975         saved_actions = req.httpsession.get('saved_actions')
976         if not saved_actions:
977             saved_actions = {"next":0, "actions":{}}
978             req.httpsession['saved_actions'] = saved_actions
979         # we don't allow more than 10 stored actions
980         if len(saved_actions["actions"]) >= 10:
981             del saved_actions["actions"][min(saved_actions["actions"])]
982         key = saved_actions["next"]
983         saved_actions["actions"][key] = the_action
984         saved_actions["next"] = key + 1
985         return key
986
987     @openerpweb.jsonrequest
988     def get_session_action(self, req, key):
989         """
990         Gets back a previously saved action. This method can return None if the action
991         was saved since too much time (this case should be handled in a smart way).
992
993         :param key: The key given by save_session_action()
994         :type key: integer
995         :return: The saved action or None.
996         :rtype: anything
997         """
998         saved_actions = req.httpsession.get('saved_actions')
999         if not saved_actions:
1000             return None
1001         return saved_actions["actions"].get(key)
1002
1003     @openerpweb.jsonrequest
1004     def check(self, req):
1005         req.session.assert_valid()
1006         return None
1007
1008     @openerpweb.jsonrequest
1009     def destroy(self, req):
1010         req.session._suicide = True
1011
1012 class Menu(openerpweb.Controller):
1013     _cp_path = "/web/menu"
1014
1015     @openerpweb.jsonrequest
1016     def load(self, req):
1017         return {'data': self.do_load(req)}
1018
1019     @openerpweb.jsonrequest
1020     def get_user_roots(self, req):
1021         return self.do_get_user_roots(req)
1022
1023     def do_get_user_roots(self, req):
1024         """ Return all root menu ids visible for the session user.
1025
1026         :param req: A request object, with an OpenERP session attribute
1027         :type req: < session -> OpenERPSession >
1028         :return: the root menu ids
1029         :rtype: list(int)
1030         """
1031         s = req.session
1032         context = s.eval_context(req.context)
1033         Menus = s.model('ir.ui.menu')
1034         # If a menu action is defined use its domain to get the root menu items
1035         user_menu_id = s.model('res.users').read([s._uid], ['menu_id'], context)[0]['menu_id']
1036
1037         menu_domain = [('parent_id', '=', False)]
1038         if user_menu_id:
1039             domain_string = s.model('ir.actions.act_window').read([user_menu_id[0]], ['domain'], context)[0]['domain']
1040             if domain_string:
1041                 menu_domain = ast.literal_eval(domain_string)
1042
1043         return Menus.search(menu_domain, 0, False, False, context)
1044
1045     def do_load(self, req):
1046         """ Loads all menu items (all applications and their sub-menus).
1047
1048         :param req: A request object, with an OpenERP session attribute
1049         :type req: < session -> OpenERPSession >
1050         :return: the menu root
1051         :rtype: dict('children': menu_nodes)
1052         """
1053         context = req.session.eval_context(req.context)
1054         Menus = req.session.model('ir.ui.menu')
1055
1056         menu_roots = Menus.read(self.do_get_user_roots(req), ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1057         menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children' : menu_roots}
1058
1059         # menus are loaded fully unlike a regular tree view, cause there are a
1060         # limited number of items (752 when all 6.1 addons are installed)
1061         menu_ids = Menus.search([], 0, False, False, context)
1062         menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1063         # adds roots at the end of the sequence, so that they will overwrite
1064         # equivalent menu items from full menu read when put into id:item
1065         # mapping, resulting in children being correctly set on the roots.
1066         menu_items.extend(menu_roots)
1067
1068         # make a tree using parent_id
1069         menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
1070         for menu_item in menu_items:
1071             if menu_item['parent_id']:
1072                 parent = menu_item['parent_id'][0]
1073             else:
1074                 parent = False
1075             if parent in menu_items_map:
1076                 menu_items_map[parent].setdefault(
1077                     'children', []).append(menu_item)
1078
1079         # sort by sequence a tree using parent_id
1080         for menu_item in menu_items:
1081             menu_item.setdefault('children', []).sort(
1082                 key=operator.itemgetter('sequence'))
1083
1084         return menu_root
1085
1086     @openerpweb.jsonrequest
1087     def action(self, req, menu_id):
1088         actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
1089                                              [('ir.ui.menu', menu_id)], False)
1090         return {"action": actions}
1091
1092 class DataSet(openerpweb.Controller):
1093     _cp_path = "/web/dataset"
1094
1095     @openerpweb.jsonrequest
1096     def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
1097         return self.do_search_read(req, model, fields, offset, limit, domain, sort)
1098     def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
1099                        , sort=None):
1100         """ Performs a search() followed by a read() (if needed) using the
1101         provided search criteria
1102
1103         :param req: a JSON-RPC request object
1104         :type req: openerpweb.JsonRequest
1105         :param str model: the name of the model to search on
1106         :param fields: a list of the fields to return in the result records
1107         :type fields: [str]
1108         :param int offset: from which index should the results start being returned
1109         :param int limit: the maximum number of records to return
1110         :param list domain: the search domain for the query
1111         :param list sort: sorting directives
1112         :returns: A structure (dict) with two keys: ids (all the ids matching
1113                   the (domain, context) pair) and records (paginated records
1114                   matching fields selection set)
1115         :rtype: list
1116         """
1117         Model = req.session.model(model)
1118
1119         context, domain = eval_context_and_domain(
1120             req.session, req.context, domain)
1121
1122         ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
1123         if limit and len(ids) == limit:
1124             length = Model.search_count(domain, context)
1125         else:
1126             length = len(ids) + (offset or 0)
1127         if fields and fields == ['id']:
1128             # shortcut read if we only want the ids
1129             return {
1130                 'length': length,
1131                 'records': [{'id': id} for id in ids]
1132             }
1133
1134         records = Model.read(ids, fields or False, context)
1135         records.sort(key=lambda obj: ids.index(obj['id']))
1136         return {
1137             'length': length,
1138             'records': records
1139         }
1140
1141     @openerpweb.jsonrequest
1142     def load(self, req, model, id, fields):
1143         m = req.session.model(model)
1144         value = {}
1145         r = m.read([id], False, req.session.eval_context(req.context))
1146         if r:
1147             value = r[0]
1148         return {'value': value}
1149
1150     def call_common(self, req, model, method, args, domain_id=None, context_id=None):
1151         has_domain = domain_id is not None and domain_id < len(args)
1152         has_context = context_id is not None and context_id < len(args)
1153
1154         domain = args[domain_id] if has_domain else []
1155         context = args[context_id] if has_context else {}
1156         c, d = eval_context_and_domain(req.session, context, domain)
1157         if has_domain:
1158             args[domain_id] = d
1159         if has_context:
1160             args[context_id] = c
1161
1162         return self._call_kw(req, model, method, args, {})
1163     
1164     def _call_kw(self, req, model, method, args, kwargs):
1165         for i in xrange(len(args)):
1166             if isinstance(args[i], common.nonliterals.BaseContext):
1167                 args[i] = req.session.eval_context(args[i])
1168             elif isinstance(args[i], common.nonliterals.BaseDomain):
1169                 args[i] = req.session.eval_domain(args[i])
1170         for k in kwargs.keys():
1171             if isinstance(kwargs[k], common.nonliterals.BaseContext):
1172                 kwargs[k] = req.session.eval_context(kwargs[k])
1173             elif isinstance(kwargs[k], common.nonliterals.BaseDomain):
1174                 kwargs[k] = req.session.eval_domain(kwargs[k])
1175
1176         # Temporary implements future display_name special field for model#read()
1177         if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
1178             if 'display_name' in args[1]:
1179                 names = req.session.model(model).name_get(args[0], **kwargs)
1180                 args[1].remove('display_name')
1181                 r = getattr(req.session.model(model), method)(*args, **kwargs)
1182                 for i in range(len(r)):
1183                     r[i]['display_name'] = names[i][1] or "%s#%d" % (model, names[i][0])
1184                 return r
1185
1186         return getattr(req.session.model(model), method)(*args, **kwargs)
1187
1188     @openerpweb.jsonrequest
1189     def onchange(self, req, model, method, args, context_id=None):
1190         """ Support method for handling onchange calls: behaves much like call
1191         with the following differences:
1192
1193         * Does not take a domain_id
1194         * Is aware of the return value's structure, and will parse the domains
1195           if needed in order to return either parsed literal domains (in JSON)
1196           or non-literal domain instances, allowing those domains to be used
1197           from JS
1198
1199         :param req:
1200         :type req: web.common.http.JsonRequest
1201         :param str model: object type on which to call the method
1202         :param str method: name of the onchange handler method
1203         :param list args: arguments to call the onchange handler with
1204         :param int context_id: index of the context object in the list of
1205                                arguments
1206         :return: result of the onchange call with all domains parsed
1207         """
1208         result = self.call_common(req, model, method, args, context_id=context_id)
1209         if not result or 'domain' not in result:
1210             return result
1211
1212         result['domain'] = dict(
1213             (k, parse_domain(v, req.session))
1214             for k, v in result['domain'].iteritems())
1215
1216         return result
1217
1218     @openerpweb.jsonrequest
1219     def call(self, req, model, method, args, domain_id=None, context_id=None):
1220         return self.call_common(req, model, method, args, domain_id, context_id)
1221     
1222     @openerpweb.jsonrequest
1223     def call_kw(self, req, model, method, args, kwargs):
1224         return self._call_kw(req, model, method, args, kwargs)
1225
1226     @openerpweb.jsonrequest
1227     def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1228         action = self.call_common(req, model, method, args, domain_id, context_id)
1229         if isinstance(action, dict) and action.get('type') != '':
1230             return clean_action(req, action)
1231         return False
1232
1233     @openerpweb.jsonrequest
1234     def exec_workflow(self, req, model, id, signal):
1235         return req.session.exec_workflow(model, id, signal)
1236
1237     @openerpweb.jsonrequest
1238     def resequence(self, req, model, ids, field='sequence', offset=0):
1239         """ Re-sequences a number of records in the model, by their ids
1240
1241         The re-sequencing starts at the first model of ``ids``, the sequence
1242         number is incremented by one after each record and starts at ``offset``
1243
1244         :param ids: identifiers of the records to resequence, in the new sequence order
1245         :type ids: list(id)
1246         :param str field: field used for sequence specification, defaults to
1247                           "sequence"
1248         :param int offset: sequence number for first record in ``ids``, allows
1249                            starting the resequencing from an arbitrary number,
1250                            defaults to ``0``
1251         """
1252         m = req.session.model(model)
1253         if not m.fields_get([field]):
1254             return False
1255         # python 2.6 has no start parameter
1256         for i, id in enumerate(ids):
1257             m.write(id, { field: i + offset })
1258         return True
1259
1260 class DataGroup(openerpweb.Controller):
1261     _cp_path = "/web/group"
1262     @openerpweb.jsonrequest
1263     def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
1264         Model = req.session.model(model)
1265         context, domain = eval_context_and_domain(req.session, req.context, domain)
1266
1267         return Model.read_group(
1268             domain or [], fields, group_by_fields, 0, False,
1269             dict(context, group_by=group_by_fields), sort or False)
1270
1271 class View(openerpweb.Controller):
1272     _cp_path = "/web/view"
1273
1274     def fields_view_get(self, req, model, view_id, view_type,
1275                         transform=True, toolbar=False, submenu=False):
1276         Model = req.session.model(model)
1277         context = req.session.eval_context(req.context)
1278         fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
1279         # todo fme?: check that we should pass the evaluated context here
1280         self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
1281         if toolbar and transform:
1282             self.process_toolbar(req, fvg['toolbar'])
1283         return fvg
1284
1285     def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
1286         # depending on how it feels, xmlrpclib.ServerProxy can translate
1287         # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
1288         # enjoy unicode strings which can not be trivially converted to
1289         # strings, and it blows up during parsing.
1290
1291         # So ensure we fix this retardation by converting view xml back to
1292         # bit strings.
1293         if isinstance(fvg['arch'], unicode):
1294             arch = fvg['arch'].encode('utf-8')
1295         else:
1296             arch = fvg['arch']
1297         fvg['arch_string'] = arch
1298
1299         if transform:
1300             evaluation_context = session.evaluation_context(context or {})
1301             xml = self.transform_view(arch, session, evaluation_context)
1302         else:
1303             xml = ElementTree.fromstring(arch)
1304         fvg['arch'] = common.xml2json.from_elementtree(xml, preserve_whitespaces)
1305
1306         if 'id' in fvg['fields']:
1307             # Special case for id's
1308             id_field = fvg['fields']['id']
1309             id_field['original_type'] = id_field['type']
1310             id_field['type'] = 'id'
1311
1312         for field in fvg['fields'].itervalues():
1313             if field.get('views'):
1314                 for view in field["views"].itervalues():
1315                     self.process_view(session, view, None, transform)
1316             if field.get('domain'):
1317                 field["domain"] = parse_domain(field["domain"], session)
1318             if field.get('context'):
1319                 field["context"] = parse_context(field["context"], session)
1320
1321     def process_toolbar(self, req, toolbar):
1322         """
1323         The toolbar is a mapping of section_key: [action_descriptor]
1324
1325         We need to clean all those actions in order to ensure correct
1326         round-tripping
1327         """
1328         for actions in toolbar.itervalues():
1329             for action in actions:
1330                 if 'context' in action:
1331                     action['context'] = parse_context(
1332                         action['context'], req.session)
1333                 if 'domain' in action:
1334                     action['domain'] = parse_domain(
1335                         action['domain'], req.session)
1336
1337     @openerpweb.jsonrequest
1338     def add_custom(self, req, view_id, arch):
1339         CustomView = req.session.model('ir.ui.view.custom')
1340         CustomView.create({
1341             'user_id': req.session._uid,
1342             'ref_id': view_id,
1343             'arch': arch
1344         }, req.session.eval_context(req.context))
1345         return {'result': True}
1346
1347     @openerpweb.jsonrequest
1348     def undo_custom(self, req, view_id, reset=False):
1349         CustomView = req.session.model('ir.ui.view.custom')
1350         context = req.session.eval_context(req.context)
1351         vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1352                                     0, False, False, context)
1353         if vcustom:
1354             if reset:
1355                 CustomView.unlink(vcustom, context)
1356             else:
1357                 CustomView.unlink([vcustom[0]], context)
1358             return {'result': True}
1359         return {'result': False}
1360
1361     def transform_view(self, view_string, session, context=None):
1362         # transform nodes on the fly via iterparse, instead of
1363         # doing it statically on the parsing result
1364         parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
1365         root = None
1366         for event, elem in parser:
1367             if event == "start":
1368                 if root is None:
1369                     root = elem
1370                 self.parse_domains_and_contexts(elem, session)
1371         return root
1372
1373     def parse_domains_and_contexts(self, elem, session):
1374         """ Converts domains and contexts from the view into Python objects,
1375         either literals if they can be parsed by literal_eval or a special
1376         placeholder object if the domain or context refers to free variables.
1377
1378         :param elem: the current node being parsed
1379         :type param: xml.etree.ElementTree.Element
1380         :param session: OpenERP session object, used to store and retrieve
1381                         non-literal objects
1382         :type session: openerpweb.openerpweb.OpenERPSession
1383         """
1384         for el in ['domain', 'filter_domain']:
1385             domain = elem.get(el, '').strip()
1386             if domain:
1387                 elem.set(el, parse_domain(domain, session))
1388                 elem.set(el + '_string', domain)
1389         for el in ['context', 'default_get']:
1390             context_string = elem.get(el, '').strip()
1391             if context_string:
1392                 elem.set(el, parse_context(context_string, session))
1393                 elem.set(el + '_string', context_string)
1394
1395     @openerpweb.jsonrequest
1396     def load(self, req, model, view_id, view_type, toolbar=False):
1397         return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1398
1399 class TreeView(View):
1400     _cp_path = "/web/treeview"
1401
1402     @openerpweb.jsonrequest
1403     def action(self, req, model, id):
1404         return load_actions_from_ir_values(
1405             req,'action', 'tree_but_open',[(model, id)],
1406             False)
1407
1408 class SearchView(View):
1409     _cp_path = "/web/searchview"
1410
1411     @openerpweb.jsonrequest
1412     def load(self, req, model, view_id):
1413         fields_view = self.fields_view_get(req, model, view_id, 'search')
1414         return {'fields_view': fields_view}
1415
1416     @openerpweb.jsonrequest
1417     def fields_get(self, req, model):
1418         Model = req.session.model(model)
1419         fields = Model.fields_get(False, req.session.eval_context(req.context))
1420         for field in fields.values():
1421             # shouldn't convert the views too?
1422             if field.get('domain'):
1423                 field["domain"] = parse_domain(field["domain"], req.session)
1424             if field.get('context'):
1425                 field["context"] = parse_context(field["context"], req.session)
1426         return {'fields': fields}
1427
1428     @openerpweb.jsonrequest
1429     def get_filters(self, req, model):
1430         logger = logging.getLogger(__name__ + '.SearchView.get_filters')
1431         Model = req.session.model("ir.filters")
1432         filters = Model.get_filters(model)
1433         for filter in filters:
1434             try:
1435                 parsed_context = parse_context(filter["context"], req.session)
1436                 filter["context"] = (parsed_context
1437                         if not isinstance(parsed_context, common.nonliterals.BaseContext)
1438                         else req.session.eval_context(parsed_context))
1439
1440                 parsed_domain = parse_domain(filter["domain"], req.session)
1441                 filter["domain"] = (parsed_domain
1442                         if not isinstance(parsed_domain, common.nonliterals.BaseDomain)
1443                         else req.session.eval_domain(parsed_domain))
1444             except Exception:
1445                 logger.exception("Failed to parse custom filter %s in %s",
1446                                  filter['name'], model)
1447                 filter['disabled'] = True
1448                 del filter['context']
1449                 del filter['domain']
1450         return filters
1451
1452 class Binary(openerpweb.Controller):
1453     _cp_path = "/web/binary"
1454
1455     @openerpweb.httprequest
1456     def image(self, req, model, id, field, **kw):
1457         last_update = '__last_update'
1458         Model = req.session.model(model)
1459         context = req.session.eval_context(req.context)
1460         headers = [('Content-Type', 'image/png')]
1461         etag = req.httprequest.headers.get('If-None-Match')
1462         hashed_session = hashlib.md5(req.session_id).hexdigest()
1463         id = None if not id else simplejson.loads(id)
1464         if type(id) is list:
1465             id = id[0] # m2o
1466         if etag:
1467             if not id and hashed_session == etag:
1468                 return werkzeug.wrappers.Response(status=304)
1469             else:
1470                 date = Model.read([id], [last_update], context)[0].get(last_update)
1471                 if hashlib.md5(date).hexdigest() == etag:
1472                     return werkzeug.wrappers.Response(status=304)
1473
1474         retag = hashed_session
1475         try:
1476             if not id:
1477                 res = Model.default_get([field], context).get(field)
1478                 image_data = base64.b64decode(res)
1479             else:
1480                 res = Model.read([id], [last_update, field], context)[0]
1481                 retag = hashlib.md5(res.get(last_update)).hexdigest()
1482                 image_data = base64.b64decode(res.get(field))
1483         except (TypeError, xmlrpclib.Fault):
1484             image_data = self.placeholder(req)
1485         headers.append(('ETag', retag))
1486         headers.append(('Content-Length', len(image_data)))
1487         try:
1488             ncache = int(kw.get('cache'))
1489             headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1490         except:
1491             pass
1492         return req.make_response(image_data, headers)
1493     def placeholder(self, req):
1494         addons_path = openerpweb.addons_manifest['web']['addons_path']
1495         return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1496     def content_disposition(self, filename, req):
1497         filename = filename.encode('utf8')
1498         escaped = urllib2.quote(filename)
1499         browser = req.httprequest.user_agent.browser
1500         version = int((req.httprequest.user_agent.version or '0').split('.')[0])
1501         if browser == 'msie' and version < 9:
1502             return "attachment; filename=%s" % escaped
1503         elif browser == 'safari':
1504             return "attachment; filename=%s" % filename
1505         else:
1506             return "attachment; filename*=UTF-8''%s" % escaped
1507
1508     @openerpweb.httprequest
1509     def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1510         """ Download link for files stored as binary fields.
1511
1512         If the ``id`` parameter is omitted, fetches the default value for the
1513         binary field (via ``default_get``), otherwise fetches the field for
1514         that precise record.
1515
1516         :param req: OpenERP request
1517         :type req: :class:`web.common.http.HttpRequest`
1518         :param str model: name of the model to fetch the binary from
1519         :param str field: binary field
1520         :param str id: id of the record from which to fetch the binary
1521         :param str filename_field: field holding the file's name, if any
1522         :returns: :class:`werkzeug.wrappers.Response`
1523         """
1524         Model = req.session.model(model)
1525         context = req.session.eval_context(req.context)
1526         fields = [field]
1527         if filename_field:
1528             fields.append(filename_field)
1529         if id:
1530             res = Model.read([int(id)], fields, context)[0]
1531         else:
1532             res = Model.default_get(fields, context)
1533         filecontent = base64.b64decode(res.get(field, ''))
1534         if not filecontent:
1535             return req.not_found()
1536         else:
1537             filename = '%s_%s' % (model.replace('.', '_'), id)
1538             if filename_field:
1539                 filename = res.get(filename_field, '') or filename
1540             return req.make_response(filecontent,
1541                 [('Content-Type', 'application/octet-stream'),
1542                  ('Content-Disposition', self.content_disposition(filename, req))])
1543
1544     @openerpweb.httprequest
1545     def saveas_ajax(self, req, data, token):
1546         jdata = simplejson.loads(data)
1547         model = jdata['model']
1548         field = jdata['field']
1549         id = jdata.get('id', None)
1550         filename_field = jdata.get('filename_field', None)
1551         context = jdata.get('context', dict())
1552
1553         context = req.session.eval_context(context)
1554         Model = req.session.model(model)
1555         fields = [field]
1556         if filename_field:
1557             fields.append(filename_field)
1558         if id:
1559             res = Model.read([int(id)], fields, context)[0]
1560         else:
1561             res = Model.default_get(fields, context)
1562         filecontent = base64.b64decode(res.get(field, ''))
1563         if not filecontent:
1564             raise ValueError("No content found for field '%s' on '%s:%s'" %
1565                 (field, model, id))
1566         else:
1567             filename = '%s_%s' % (model.replace('.', '_'), id)
1568             if filename_field:
1569                 filename = res.get(filename_field, '') or filename
1570             return req.make_response(filecontent,
1571                 headers=[('Content-Type', 'application/octet-stream'),
1572                         ('Content-Disposition', self.content_disposition(filename, req))],
1573                 cookies={'fileToken': int(token)})
1574
1575     @openerpweb.httprequest
1576     def upload(self, req, callback, ufile):
1577         # TODO: might be useful to have a configuration flag for max-length file uploads
1578         try:
1579             out = """<script language="javascript" type="text/javascript">
1580                         var win = window.top.window;
1581                         win.jQuery(win).trigger(%s, %s);
1582                     </script>"""
1583             data = ufile.read()
1584             args = [len(data), ufile.filename,
1585                     ufile.content_type, base64.b64encode(data)]
1586         except Exception, e:
1587             args = [False, e.message]
1588         return out % (simplejson.dumps(callback), simplejson.dumps(args))
1589
1590     @openerpweb.httprequest
1591     def upload_attachment(self, req, callback, model, id, ufile):
1592         context = req.session.eval_context(req.context)
1593         Model = req.session.model('ir.attachment')
1594         try:
1595             out = """<script language="javascript" type="text/javascript">
1596                         var win = window.top.window;
1597                         win.jQuery(win).trigger(%s, %s);
1598                     </script>"""
1599             attachment_id = Model.create({
1600                 'name': ufile.filename,
1601                 'datas': base64.encodestring(ufile.read()),
1602                 'datas_fname': ufile.filename,
1603                 'res_model': model,
1604                 'res_id': int(id)
1605             }, context)
1606             args = {
1607                 'filename': ufile.filename,
1608                 'id':  attachment_id
1609             }
1610         except Exception, e:
1611             args = { 'error': e.message }
1612         return out % (simplejson.dumps(callback), simplejson.dumps(args))
1613
1614 class Action(openerpweb.Controller):
1615     _cp_path = "/web/action"
1616
1617     @openerpweb.jsonrequest
1618     def load(self, req, action_id, do_not_eval=False):
1619         Actions = req.session.model('ir.actions.actions')
1620         value = False
1621         context = req.session.eval_context(req.context)
1622
1623         try:
1624             action_id = int(action_id)
1625         except ValueError:
1626             try:
1627                 module, xmlid = action_id.split('.', 1)
1628                 model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
1629                 assert model.startswith('ir.actions.')
1630             except Exception:
1631                 action_id = 0   # force failed read
1632
1633         base_action = Actions.read([action_id], ['type'], context)
1634         if base_action:
1635             ctx = {}
1636             action_type = base_action[0]['type']
1637             if action_type == 'ir.actions.report.xml':
1638                 ctx.update({'bin_size': True})
1639             ctx.update(context)
1640             action = req.session.model(action_type).read([action_id], False, ctx)
1641             if action:
1642                 value = clean_action(req, action[0], do_not_eval)
1643         return value
1644
1645     @openerpweb.jsonrequest
1646     def run(self, req, action_id):
1647         return_action = req.session.model('ir.actions.server').run(
1648             [action_id], req.session.eval_context(req.context))
1649         if return_action:
1650             return clean_action(req, return_action)
1651         else:
1652             return False
1653
1654 class Export(View):
1655     _cp_path = "/web/export"
1656
1657     @openerpweb.jsonrequest
1658     def formats(self, req):
1659         """ Returns all valid export formats
1660
1661         :returns: for each export format, a pair of identifier and printable name
1662         :rtype: [(str, str)]
1663         """
1664         return sorted([
1665             controller.fmt
1666             for path, controller in openerpweb.controllers_path.iteritems()
1667             if path.startswith(self._cp_path)
1668             if hasattr(controller, 'fmt')
1669         ], key=operator.itemgetter("label"))
1670
1671     def fields_get(self, req, model):
1672         Model = req.session.model(model)
1673         fields = Model.fields_get(False, req.session.eval_context(req.context))
1674         return fields
1675
1676     @openerpweb.jsonrequest
1677     def get_fields(self, req, model, prefix='', parent_name= '',
1678                    import_compat=True, parent_field_type=None,
1679                    exclude=None):
1680
1681         if import_compat and parent_field_type == "many2one":
1682             fields = {}
1683         else:
1684             fields = self.fields_get(req, model)
1685
1686         if import_compat:
1687             fields.pop('id', None)
1688         else:
1689             fields['.id'] = fields.pop('id', {'string': 'ID'})
1690
1691         fields_sequence = sorted(fields.iteritems(),
1692             key=lambda field: field[1].get('string', ''))
1693
1694         records = []
1695         for field_name, field in fields_sequence:
1696             if import_compat:
1697                 if exclude and field_name in exclude:
1698                     continue
1699                 if field.get('readonly'):
1700                     # If none of the field's states unsets readonly, skip the field
1701                     if all(dict(attrs).get('readonly', True)
1702                            for attrs in field.get('states', {}).values()):
1703                         continue
1704
1705             id = prefix + (prefix and '/'or '') + field_name
1706             name = parent_name + (parent_name and '/' or '') + field['string']
1707             record = {'id': id, 'string': name,
1708                       'value': id, 'children': False,
1709                       'field_type': field.get('type'),
1710                       'required': field.get('required'),
1711                       'relation_field': field.get('relation_field')}
1712             records.append(record)
1713
1714             if len(name.split('/')) < 3 and 'relation' in field:
1715                 ref = field.pop('relation')
1716                 record['value'] += '/id'
1717                 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1718
1719                 if not import_compat or field['type'] == 'one2many':
1720                     # m2m field in import_compat is childless
1721                     record['children'] = True
1722
1723         return records
1724
1725     @openerpweb.jsonrequest
1726     def namelist(self,req,  model, export_id):
1727         # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1728         export = req.session.model("ir.exports").read([export_id])[0]
1729         export_fields_list = req.session.model("ir.exports.line").read(
1730             export['export_fields'])
1731
1732         fields_data = self.fields_info(
1733             req, model, map(operator.itemgetter('name'), export_fields_list))
1734
1735         return [
1736             {'name': field['name'], 'label': fields_data[field['name']]}
1737             for field in export_fields_list
1738         ]
1739
1740     def fields_info(self, req, model, export_fields):
1741         info = {}
1742         fields = self.fields_get(req, model)
1743
1744         # To make fields retrieval more efficient, fetch all sub-fields of a
1745         # given field at the same time. Because the order in the export list is
1746         # arbitrary, this requires ordering all sub-fields of a given field
1747         # together so they can be fetched at the same time
1748         #
1749         # Works the following way:
1750         # * sort the list of fields to export, the default sorting order will
1751         #   put the field itself (if present, for xmlid) and all of its
1752         #   sub-fields right after it
1753         # * then, group on: the first field of the path (which is the same for
1754         #   a field and for its subfields and the length of splitting on the
1755         #   first '/', which basically means grouping the field on one side and
1756         #   all of the subfields on the other. This way, we have the field (for
1757         #   the xmlid) with length 1, and all of the subfields with the same
1758         #   base but a length "flag" of 2
1759         # * if we have a normal field (length 1), just add it to the info
1760         #   mapping (with its string) as-is
1761         # * otherwise, recursively call fields_info via graft_subfields.
1762         #   all graft_subfields does is take the result of fields_info (on the
1763         #   field's model) and prepend the current base (current field), which
1764         #   rebuilds the whole sub-tree for the field
1765         #
1766         # result: because we're not fetching the fields_get for half the
1767         # database models, fetching a namelist with a dozen fields (including
1768         # relational data) falls from ~6s to ~300ms (on the leads model).
1769         # export lists with no sub-fields (e.g. import_compatible lists with
1770         # no o2m) are even more efficient (from the same 6s to ~170ms, as
1771         # there's a single fields_get to execute)
1772         for (base, length), subfields in itertools.groupby(
1773                 sorted(export_fields),
1774                 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1775             subfields = list(subfields)
1776             if length == 2:
1777                 # subfields is a seq of $base/*rest, and not loaded yet
1778                 info.update(self.graft_subfields(
1779                     req, fields[base]['relation'], base, fields[base]['string'],
1780                     subfields
1781                 ))
1782             else:
1783                 info[base] = fields[base]['string']
1784
1785         return info
1786
1787     def graft_subfields(self, req, model, prefix, prefix_string, fields):
1788         export_fields = [field.split('/', 1)[1] for field in fields]
1789         return (
1790             (prefix + '/' + k, prefix_string + '/' + v)
1791             for k, v in self.fields_info(req, model, export_fields).iteritems())
1792
1793     #noinspection PyPropertyDefinition
1794     @property
1795     def content_type(self):
1796         """ Provides the format's content type """
1797         raise NotImplementedError()
1798
1799     def filename(self, base):
1800         """ Creates a valid filename for the format (with extension) from the
1801          provided base name (exension-less)
1802         """
1803         raise NotImplementedError()
1804
1805     def from_data(self, fields, rows):
1806         """ Conversion method from OpenERP's export data to whatever the
1807         current export class outputs
1808
1809         :params list fields: a list of fields to export
1810         :params list rows: a list of records to export
1811         :returns:
1812         :rtype: bytes
1813         """
1814         raise NotImplementedError()
1815
1816     @openerpweb.httprequest
1817     def index(self, req, data, token):
1818         model, fields, ids, domain, import_compat = \
1819             operator.itemgetter('model', 'fields', 'ids', 'domain',
1820                                 'import_compat')(
1821                 simplejson.loads(data))
1822
1823         context = req.session.eval_context(req.context)
1824         Model = req.session.model(model)
1825         ids = ids or Model.search(domain, 0, False, False, context)
1826
1827         field_names = map(operator.itemgetter('name'), fields)
1828         import_data = Model.export_data(ids, field_names, context).get('datas',[])
1829
1830         if import_compat:
1831             columns_headers = field_names
1832         else:
1833             columns_headers = [val['label'].strip() for val in fields]
1834
1835
1836         return req.make_response(self.from_data(columns_headers, import_data),
1837             headers=[('Content-Disposition', 'attachment; filename="%s"' % self.filename(model)),
1838                      ('Content-Type', self.content_type)],
1839             cookies={'fileToken': int(token)})
1840
1841 class CSVExport(Export):
1842     _cp_path = '/web/export/csv'
1843     fmt = {'tag': 'csv', 'label': 'CSV'}
1844
1845     @property
1846     def content_type(self):
1847         return 'text/csv;charset=utf8'
1848
1849     def filename(self, base):
1850         return base + '.csv'
1851
1852     def from_data(self, fields, rows):
1853         fp = StringIO()
1854         writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1855
1856         writer.writerow([name.encode('utf-8') for name in fields])
1857
1858         for data in rows:
1859             row = []
1860             for d in data:
1861                 if isinstance(d, basestring):
1862                     d = d.replace('\n',' ').replace('\t',' ')
1863                     try:
1864                         d = d.encode('utf-8')
1865                     except UnicodeError:
1866                         pass
1867                 if d is False: d = None
1868                 row.append(d)
1869             writer.writerow(row)
1870
1871         fp.seek(0)
1872         data = fp.read()
1873         fp.close()
1874         return data
1875
1876 class ExcelExport(Export):
1877     _cp_path = '/web/export/xls'
1878     fmt = {
1879         'tag': 'xls',
1880         'label': 'Excel',
1881         'error': None if xlwt else "XLWT required"
1882     }
1883
1884     @property
1885     def content_type(self):
1886         return 'application/vnd.ms-excel'
1887
1888     def filename(self, base):
1889         return base + '.xls'
1890
1891     def from_data(self, fields, rows):
1892         workbook = xlwt.Workbook()
1893         worksheet = workbook.add_sheet('Sheet 1')
1894
1895         for i, fieldname in enumerate(fields):
1896             worksheet.write(0, i, fieldname)
1897             worksheet.col(i).width = 8000 # around 220 pixels
1898
1899         style = xlwt.easyxf('align: wrap yes')
1900
1901         for row_index, row in enumerate(rows):
1902             for cell_index, cell_value in enumerate(row):
1903                 if isinstance(cell_value, basestring):
1904                     cell_value = re.sub("\r", " ", cell_value)
1905                 if cell_value is False: cell_value = None
1906                 worksheet.write(row_index + 1, cell_index, cell_value, style)
1907
1908         fp = StringIO()
1909         workbook.save(fp)
1910         fp.seek(0)
1911         data = fp.read()
1912         fp.close()
1913         return data
1914
1915 class Reports(View):
1916     _cp_path = "/web/report"
1917     POLLING_DELAY = 0.25
1918     TYPES_MAPPING = {
1919         'doc': 'application/vnd.ms-word',
1920         'html': 'text/html',
1921         'odt': 'application/vnd.oasis.opendocument.text',
1922         'pdf': 'application/pdf',
1923         'sxw': 'application/vnd.sun.xml.writer',
1924         'xls': 'application/vnd.ms-excel',
1925     }
1926
1927     @openerpweb.httprequest
1928     def index(self, req, action, token):
1929         action = simplejson.loads(action)
1930
1931         report_srv = req.session.proxy("report")
1932         context = req.session.eval_context(
1933             common.nonliterals.CompoundContext(
1934                 req.context or {}, action[ "context"]))
1935
1936         report_data = {}
1937         report_ids = context["active_ids"]
1938         if 'report_type' in action:
1939             report_data['report_type'] = action['report_type']
1940         if 'datas' in action:
1941             if 'ids' in action['datas']:
1942                 report_ids = action['datas'].pop('ids')
1943             report_data.update(action['datas'])
1944
1945         report_id = report_srv.report(
1946             req.session._db, req.session._uid, req.session._password,
1947             action["report_name"], report_ids,
1948             report_data, context)
1949
1950         report_struct = None
1951         while True:
1952             report_struct = report_srv.report_get(
1953                 req.session._db, req.session._uid, req.session._password, report_id)
1954             if report_struct["state"]:
1955                 break
1956
1957             time.sleep(self.POLLING_DELAY)
1958
1959         report = base64.b64decode(report_struct['result'])
1960         if report_struct.get('code') == 'zlib':
1961             report = zlib.decompress(report)
1962         report_mimetype = self.TYPES_MAPPING.get(
1963             report_struct['format'], 'octet-stream')
1964         file_name = action.get('name', 'report')
1965         if 'name' not in action:
1966             reports = req.session.model('ir.actions.report.xml')
1967             res_id = reports.search([('report_name', '=', action['report_name']),],
1968                                     0, False, False, context)
1969             if len(res_id) > 0:
1970                 file_name = reports.read(res_id[0], ['name'], context)['name']
1971             else:
1972                 file_name = action['report_name']
1973
1974         return req.make_response(report,
1975              headers=[
1976                  # maybe we should take of what characters can appear in a file name?
1977                  ('Content-Disposition', 'attachment; filename="%s.%s"' % (file_name, report_struct['format'])),
1978                  ('Content-Type', report_mimetype),
1979                  ('Content-Length', len(report))],
1980              cookies={'fileToken': int(token)})
1981
1982 class Import(View):
1983     _cp_path = "/web/import"
1984
1985     def fields_get(self, req, model):
1986         Model = req.session.model(model)
1987         fields = Model.fields_get(False, req.session.eval_context(req.context))
1988         return fields
1989
1990     @openerpweb.httprequest
1991     def detect_data(self, req, csvfile, csvsep=',', csvdel='"', csvcode='utf-8', jsonp='callback'):
1992         try:
1993             data = list(csv.reader(
1994                 csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
1995         except csv.Error, e:
1996             csvfile.seek(0)
1997             return '<script>window.top.%s(%s);</script>' % (
1998                 jsonp, simplejson.dumps({'error': {
1999                     'message': 'Error parsing CSV file: %s' % e,
2000                     # decodes each byte to a unicode character, which may or
2001                     # may not be printable, but decoding will succeed.
2002                     # Otherwise simplejson will try to decode the `str` using
2003                     # utf-8, which is very likely to blow up on characters out
2004                     # of the ascii range (in range [128, 256))
2005                     'preview': csvfile.read(200).decode('iso-8859-1')}}))
2006
2007         try:
2008             return '<script>window.top.%s(%s);</script>' % (
2009                 jsonp, simplejson.dumps(
2010                     {'records': data[:10]}, encoding=csvcode))
2011         except UnicodeDecodeError:
2012             return '<script>window.top.%s(%s);</script>' % (
2013                 jsonp, simplejson.dumps({
2014                     'message': u"Failed to decode CSV file using encoding %s, "
2015                                u"try switching to a different encoding" % csvcode
2016                 }))
2017
2018     @openerpweb.httprequest
2019     def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
2020                     meta):
2021         modle_obj = req.session.model(model)
2022         skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')(
2023             simplejson.loads(meta))
2024
2025         error = None
2026         if not (csvdel and len(csvdel) == 1):
2027             error = u"The CSV delimiter must be a single character"
2028
2029         if not indices and fields:
2030             error = u"You must select at least one field to import"
2031
2032         if error:
2033             return '<script>window.top.%s(%s);</script>' % (
2034                 jsonp, simplejson.dumps({'error': {'message': error}}))
2035
2036         # skip ignored records (@skip parameter)
2037         # then skip empty lines (not valid csv)
2038         # nb: should these operations be reverted?
2039         rows_to_import = itertools.ifilter(
2040             None,
2041             itertools.islice(
2042                 csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
2043                 skip, None))
2044
2045         # if only one index, itemgetter will return an atom rather than a tuple
2046         if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
2047         else: mapper = operator.itemgetter(*indices)
2048
2049         data = None
2050         error = None
2051         try:
2052             # decode each data row
2053             data = [
2054                 [record.decode(csvcode) for record in row]
2055                 for row in itertools.imap(mapper, rows_to_import)
2056                 # don't insert completely empty rows (can happen due to fields
2057                 # filtering in case of e.g. o2m content rows)
2058                 if any(row)
2059             ]
2060         except UnicodeDecodeError:
2061             error = u"Failed to decode CSV file using encoding %s" % csvcode
2062         except csv.Error, e:
2063             error = u"Could not process CSV file: %s" % e
2064
2065         # If the file contains nothing,
2066         if not data:
2067             error = u"File to import is empty"
2068         if error:
2069             return '<script>window.top.%s(%s);</script>' % (
2070                 jsonp, simplejson.dumps({'error': {'message': error}}))
2071
2072         try:
2073             (code, record, message, _nope) = modle_obj.import_data(
2074                 fields, data, 'init', '', False,
2075                 req.session.eval_context(req.context))
2076         except xmlrpclib.Fault, e:
2077             error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
2078             return '<script>window.top.%s(%s);</script>' % (
2079                 jsonp, simplejson.dumps({'error':error}))
2080
2081         if code != -1:
2082             return '<script>window.top.%s(%s);</script>' % (
2083                 jsonp, simplejson.dumps({'success':True}))
2084
2085         msg = u"Error during import: %s\n\nTrying to import record %r" % (
2086             message, record)
2087         return '<script>window.top.%s(%s);</script>' % (
2088             jsonp, simplejson.dumps({'error': {'message':msg}}))
2089
2090 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: