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