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