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