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