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