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