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