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