[FIX] /web/session/modules route return all installed modules. Web client is smart...
[odoo/odoo.git] / addons / web / controllers / main.py
1 # -*- coding: utf-8 -*-
2
3 import ast
4 import base64
5 import csv
6 import glob
7 import itertools
8 import logging
9 import operator
10 import datetime
11 import hashlib
12 import os
13 import re
14 import simplejson
15 import time
16 import urllib2
17 import xmlrpclib
18 import zlib
19 from xml.etree import ElementTree
20 from cStringIO import StringIO
21
22 import babel.messages.pofile
23 import werkzeug.utils
24 import werkzeug.wrappers
25 try:
26     import xlwt
27 except ImportError:
28     xlwt = None
29
30 from .. import common
31 openerpweb = common.http
32
33 #----------------------------------------------------------
34 # OpenERP Web helpers
35 #----------------------------------------------------------
36
37 def sass2scss(src):
38     # Validated by diff -u of sass2scss against:
39     # sass-convert -F sass -T scss openerp.sass openerp.scss
40     block = []
41     sass = ('', block)
42     reComment = re.compile(r'//.*$')
43     reIndent = re.compile(r'^\s+')
44     reIgnore = re.compile(r'^\s*(//.*)?$')
45     reFixes = { re.compile(r'\(\((.*)\)\)') : r'(\1)', }
46     lastLevel = 0
47     prevBlocks = {}
48     for l in src.split('\n'):
49         l = l.rstrip()
50         if reIgnore.search(l): continue
51         l = reComment.sub('', l)
52         l = l.rstrip()
53         indent = reIndent.match(l)
54         level = indent.end() if indent else 0
55         l = l[level:]
56         if level>lastLevel:
57             prevBlocks[lastLevel] = block
58             newBlock = []
59             block[-1] = (block[-1], newBlock)
60             block = newBlock
61         elif level<lastLevel:
62             block = prevBlocks[level]
63         lastLevel = level
64         if not l: continue
65         # Fixes
66         for ereg, repl in reFixes.items():
67             l = ereg.sub(repl if type(repl)==str else repl(), l)
68         block.append(l)
69
70     def write(sass, level=-1):
71         out = ""
72         indent = '  '*level
73         if type(sass)==tuple:
74             if level>=0:
75                 out += indent+sass[0]+" {\n"
76             for e in sass[1]:
77                 out += write(e, level+1)
78             if level>=0:
79                 out = out.rstrip(" \n")
80                 out += ' }\n'
81             if level==0:
82                 out += "\n"
83         else:
84             out += indent+sass+";\n"
85         return out
86     return write(sass)
87
88 def db_list(req):
89     dbs = []
90     proxy = req.session.proxy("db")
91     dbs = proxy.list()
92     h = req.httprequest.environ['HTTP_HOST'].split(':')[0]
93     d = h.split('.')[0]
94     r = req.config.dbfilter.replace('%h', h).replace('%d', d)
95     dbs = [i for i in dbs if re.match(r, i)]
96     return dbs
97
98 def module_topological_sort(modules):
99     """ Return a list of module names sorted so that their dependencies of the
100     modules are listed before the module itself
101
102     modules is a dict of {module_name: dependencies}
103
104     :param modules: modules to sort
105     :type modules: dict
106     :returns: list(str)
107     """
108
109     dependencies = set(itertools.chain.from_iterable(modules.itervalues()))
110     # incoming edge: dependency on other module (if a depends on b, a has an
111     # incoming edge from b, aka there's an edge from b to a)
112     # outgoing edge: other module depending on this one
113
114     # [Tarjan 1976], http://en.wikipedia.org/wiki/Topological_sorting#Algorithms
115     #L ← Empty list that will contain the sorted nodes
116     L = []
117     #S ← Set of all nodes with no outgoing edges (modules on which no other
118     #    module depends)
119     S = set(module for module in modules if module not in dependencies)
120
121     visited = set()
122     #function visit(node n)
123     def visit(n):
124         #if n has not been visited yet then
125         if n not in visited:
126             #mark n as visited
127             visited.add(n)
128             #change: n not web module, can not be resolved, ignore
129             if n not in modules: return
130             #for each node m with an edge from m to n do (dependencies of n)
131             for m in modules[n]:
132                 #visit(m)
133                 visit(m)
134             #add n to L
135             L.append(n)
136     #for each node n in S do
137     for n in S:
138         #visit(n)
139         visit(n)
140     return L
141
142 def module_installed(req):
143     # Candidates module the current heuristic is the /static dir
144     loadable = openerpweb.addons_manifest.keys()
145     modules = {}
146
147     # Retrieve database installed modules
148     # TODO The following code should move to ir.module.module.list_installed_modules()
149     Modules = req.session.model('ir.module.module')
150     domain = [('state','=','installed'), ('name','in', loadable)]
151     for module in Modules.search_read(domain, ['name', 'dependencies_id']):
152         modules[module['name']] = []
153         deps = module.get('dependencies_id')
154         if deps:
155             deps_read = req.session.model('ir.module.module.dependency').read(deps, ['name'])
156             dependencies = [i['name'] for i in deps_read]
157             modules[module['name']] = dependencies
158
159     sorted_modules = module_topological_sort(modules)
160     return sorted_modules
161
162 def module_installed_bypass_session(dbname):
163     loadable = openerpweb.addons_manifest.keys()
164     modules = {}
165     try:
166         import openerp.modules.registry
167         registry = openerp.modules.registry.RegistryManager.get(dbname)
168         with registry.cursor() as cr:
169             m = registry.get('ir.module.module')
170             # TODO The following code should move to ir.module.module.list_installed_modules()
171             domain = [('state','=','installed'), ('name','in', loadable)]
172             ids = m.search(cr, 1, [('state','=','installed'), ('name','in', loadable)])
173             for module in m.read(cr, 1, ids, ['name', 'dependencies_id']):
174                 modules[module['name']] = []
175                 deps = module.get('dependencies_id')
176                 if deps:
177                     deps_read = registry.get('ir.module.module.dependency').read(cr, 1, deps, ['name'])
178                     dependencies = [i['name'] for i in deps_read]
179                     modules[module['name']] = dependencies
180     except Exception,e:
181         pass
182     sorted_modules = module_topological_sort(modules)
183     return sorted_modules
184
185 def module_boot(req):
186     serverside = []
187     dbside = []
188     for i in req.config.server_wide_modules:
189         if i in openerpweb.addons_manifest:
190             serverside.append(i)
191     # if only one db load every module at boot
192     dbs = []
193     try:
194         dbs = db_list(req)
195     except xmlrpclib.Fault:
196         # ignore access denied
197         pass
198     if len(dbs) == 1:
199         dbside = module_installed_bypass_session(dbs[0])
200         dbside = [i for i in dbside if i not in serverside]
201     addons = serverside + dbside
202     return addons
203
204 def concat_xml(file_list):
205     """Concatenate xml files
206
207     :param list(str) file_list: list of files to check
208     :returns: (concatenation_result, checksum)
209     :rtype: (str, str)
210     """
211     checksum = hashlib.new('sha1')
212     if not file_list:
213         return '', checksum.hexdigest()
214
215     root = None
216     for fname in file_list:
217         with open(fname, 'rb') as fp:
218             contents = fp.read()
219             checksum.update(contents)
220             fp.seek(0)
221             xml = ElementTree.parse(fp).getroot()
222
223         if root is None:
224             root = ElementTree.Element(xml.tag)
225         #elif root.tag != xml.tag:
226         #    raise ValueError("Root tags missmatch: %r != %r" % (root.tag, xml.tag))
227
228         for child in xml.getchildren():
229             root.append(child)
230     return ElementTree.tostring(root, 'utf-8'), checksum.hexdigest()
231
232 def concat_files(file_list, reader=None, intersperse=""):
233     """ Concatenates contents of all provided files
234
235     :param list(str) file_list: list of files to check
236     :param function reader: reading procedure for each file
237     :param str intersperse: string to intersperse between file contents
238     :returns: (concatenation_result, checksum)
239     :rtype: (str, str)
240     """
241     checksum = hashlib.new('sha1')
242     if not file_list:
243         return '', checksum.hexdigest()
244
245     if reader is None:
246         def reader(f):
247             with open(f, 'rb') as fp:
248                 return fp.read()
249
250     files_content = []
251     for fname in file_list:
252         contents = reader(fname)
253         checksum.update(contents)
254         files_content.append(contents)
255
256     files_concat = intersperse.join(files_content)
257     return files_concat, checksum.hexdigest()
258
259 def manifest_glob(req, addons, key):
260     if addons is None:
261         addons = module_boot(req)
262     else:
263         addons = addons.split(',')
264     r = []
265     for addon in addons:
266         manifest = openerpweb.addons_manifest.get(addon, None)
267         if not manifest:
268             continue
269         # ensure does not ends with /
270         addons_path = os.path.join(manifest['addons_path'], '')[:-1]
271         globlist = manifest.get(key, [])
272         for pattern in globlist:
273             for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
274                 r.append((path, path[len(addons_path):]))
275     return r
276
277 def manifest_list(req, mods, extension):
278     if not req.debug:
279         path = '/web/webclient/' + extension
280         if mods is not None:
281             path += '?mods=' + mods
282         return [path]
283     files = manifest_glob(req, mods, extension)
284     i_am_diabetic = req.httprequest.environ["QUERY_STRING"].count("no_sugar") >= 1 or \
285                     req.httprequest.environ.get('HTTP_REFERER', '').count("no_sugar") >= 1
286     if i_am_diabetic:
287         return [wp for _fp, wp in files]
288     else:
289         return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in files]
290
291 def get_last_modified(files):
292     """ Returns the modification time of the most recently modified
293     file provided
294
295     :param list(str) files: names of files to check
296     :return: most recent modification time amongst the fileset
297     :rtype: datetime.datetime
298     """
299     files = list(files)
300     if files:
301         return max(datetime.datetime.fromtimestamp(os.path.getmtime(f))
302                    for f in files)
303     return datetime.datetime(1970, 1, 1)
304
305 def make_conditional(req, response, last_modified=None, etag=None):
306     """ Makes the provided response conditional based upon the request,
307     and mandates revalidation from clients
308
309     Uses Werkzeug's own :meth:`ETagResponseMixin.make_conditional`, after
310     setting ``last_modified`` and ``etag`` correctly on the response object
311
312     :param req: OpenERP request
313     :type req: web.common.http.WebRequest
314     :param response: Werkzeug response
315     :type response: werkzeug.wrappers.Response
316     :param datetime.datetime last_modified: last modification date of the response content
317     :param str etag: some sort of checksum of the content (deep etag)
318     :return: the response object provided
319     :rtype: werkzeug.wrappers.Response
320     """
321     response.cache_control.must_revalidate = True
322     response.cache_control.max_age = 0
323     if last_modified:
324         response.last_modified = last_modified
325     if etag:
326         response.set_etag(etag)
327     return response.make_conditional(req.httprequest)
328
329 def login_and_redirect(req, db, login, key, redirect_url='/'):
330     req.session.authenticate(db, login, key, {})
331     return set_cookie_and_redirect(req, redirect_url)
332
333 def set_cookie_and_redirect(req, redirect_url):
334     redirect = werkzeug.utils.redirect(redirect_url, 303)
335     redirect.autocorrect_location_header = False
336     cookie_val = urllib2.quote(simplejson.dumps(req.session_id))
337     redirect.set_cookie('instance0|session_id', cookie_val)
338     return redirect
339
340 def eval_context_and_domain(session, context, domain=None):
341     e_context = session.eval_context(context)
342     # should we give the evaluated context as an evaluation context to the domain?
343     e_domain = session.eval_domain(domain or [])
344
345     return e_context, e_domain
346
347 def load_actions_from_ir_values(req, key, key2, models, meta):
348     context = req.session.eval_context(req.context)
349     Values = req.session.model('ir.values')
350     actions = Values.get(key, key2, models, meta, context)
351
352     return [(id, name, clean_action(req, action))
353             for id, name, action in actions]
354
355 def clean_action(req, action, do_not_eval=False):
356     action.setdefault('flags', {})
357
358     context = req.session.eval_context(req.context)
359     eval_ctx = req.session.evaluation_context(context)
360
361     if not do_not_eval:
362         # values come from the server, we can just eval them
363         if action.get('context') and isinstance(action.get('context'), basestring):
364             action['context'] = eval( action['context'], eval_ctx ) or {}
365
366         if action.get('domain') and isinstance(action.get('domain'), basestring):
367             action['domain'] = eval( action['domain'], eval_ctx ) or []
368     else:
369         if 'context' in action:
370             action['context'] = parse_context(action['context'], req.session)
371         if 'domain' in action:
372             action['domain'] = parse_domain(action['domain'], req.session)
373
374     action_type = action.setdefault('type', 'ir.actions.act_window_close')
375     if action_type == 'ir.actions.act_window':
376         return fix_view_modes(action)
377     return action
378
379 # I think generate_views,fix_view_modes should go into js ActionManager
380 def generate_views(action):
381     """
382     While the server generates a sequence called "views" computing dependencies
383     between a bunch of stuff for views coming directly from the database
384     (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
385     to return custom view dictionaries generated on the fly.
386
387     In that case, there is no ``views`` key available on the action.
388
389     Since the web client relies on ``action['views']``, generate it here from
390     ``view_mode`` and ``view_id``.
391
392     Currently handles two different cases:
393
394     * no view_id, multiple view_mode
395     * single view_id, single view_mode
396
397     :param dict action: action descriptor dictionary to generate a views key for
398     """
399     view_id = action.get('view_id') or False
400     if isinstance(view_id, (list, tuple)):
401         view_id = view_id[0]
402
403     # providing at least one view mode is a requirement, not an option
404     view_modes = action['view_mode'].split(',')
405
406     if len(view_modes) > 1:
407         if view_id:
408             raise ValueError('Non-db action dictionaries should provide '
409                              'either multiple view modes or a single view '
410                              'mode and an optional view id.\n\n Got view '
411                              'modes %r and view id %r for action %r' % (
412                 view_modes, view_id, action))
413         action['views'] = [(False, mode) for mode in view_modes]
414         return
415     action['views'] = [(view_id, view_modes[0])]
416
417 def fix_view_modes(action):
418     """ For historical reasons, OpenERP has weird dealings in relation to
419     view_mode and the view_type attribute (on window actions):
420
421     * one of the view modes is ``tree``, which stands for both list views
422       and tree views
423     * the choice is made by checking ``view_type``, which is either
424       ``form`` for a list view or ``tree`` for an actual tree view
425
426     This methods simply folds the view_type into view_mode by adding a
427     new view mode ``list`` which is the result of the ``tree`` view_mode
428     in conjunction with the ``form`` view_type.
429
430     TODO: this should go into the doc, some kind of "peculiarities" section
431
432     :param dict action: an action descriptor
433     :returns: nothing, the action is modified in place
434     """
435     if not action.get('views'):
436         generate_views(action)
437
438     id_form = None
439     for index, (id, mode) in enumerate(action['views']):
440         if mode == 'form':
441             id_form = id
442             break
443
444     if action.pop('view_type', 'form') != 'form':
445         return action
446
447     action['views'] = [
448         [id, mode if mode != 'tree' else 'list']
449         for id, mode in action['views']
450     ]
451
452     return action
453
454 def parse_domain(domain, session):
455     """ Parses an arbitrary string containing a domain, transforms it
456     to either a literal domain or a :class:`common.nonliterals.Domain`
457
458     :param domain: the domain to parse, if the domain is not a string it
459                    is assumed to be a literal domain and is returned as-is
460     :param session: Current OpenERP session
461     :type session: openerpweb.openerpweb.OpenERPSession
462     """
463     if not isinstance(domain, basestring):
464         return domain
465     try:
466         return ast.literal_eval(domain)
467     except ValueError:
468         # not a literal
469         return common.nonliterals.Domain(session, domain)
470
471 def parse_context(context, session):
472     """ Parses an arbitrary string containing a context, transforms it
473     to either a literal context or a :class:`common.nonliterals.Context`
474
475     :param context: the context to parse, if the context is not a string it
476            is assumed to be a literal domain and is returned as-is
477     :param session: Current OpenERP session
478     :type session: openerpweb.openerpweb.OpenERPSession
479     """
480     if not isinstance(context, basestring):
481         return context
482     try:
483         return ast.literal_eval(context)
484     except ValueError:
485         return common.nonliterals.Context(session, context)
486
487 #----------------------------------------------------------
488 # OpenERP Web web Controllers
489 #----------------------------------------------------------
490
491 html_template = """<!DOCTYPE html>
492 <html style="height: 100%%">
493     <head>
494         <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
495         <meta http-equiv="content-type" content="text/html; charset=utf-8" />
496         <title>OpenERP</title>
497         <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
498         <link rel="stylesheet" href="/web/static/src/css/full.css" />
499         %(css)s
500         %(js)s
501         <script type="text/javascript">
502             $(function() {
503                 var s = new openerp.init(%(modules)s);
504                 %(init)s
505             });
506         </script>
507     </head>
508     <body></body>
509 </html>
510 """
511
512 class Home(openerpweb.Controller):
513     _cp_path = '/'
514
515     @openerpweb.httprequest
516     def index(self, req, s_action=None, **kw):
517         js = "\n        ".join('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list(req, None, 'js'))
518         css = "\n        ".join('<link rel="stylesheet" href="%s">' % i for i in manifest_list(req, None, 'css'))
519
520         r = html_template % {
521             'js': js,
522             'css': css,
523             'modules': simplejson.dumps(module_boot(req)),
524             'init': 'var wc = new s.web.WebClient();wc.appendTo($(document.body));'
525         }
526         return r
527
528     @openerpweb.httprequest
529     def login(self, req, db, login, key):
530         return login_and_redirect(req, db, login, key)
531
532 class WebClient(openerpweb.Controller):
533     _cp_path = "/web/webclient"
534
535     @openerpweb.jsonrequest
536     def csslist(self, req, mods=None):
537         return manifest_list(req, mods, 'css')
538
539     @openerpweb.jsonrequest
540     def jslist(self, req, mods=None):
541         return manifest_list(req, mods, 'js')
542
543     @openerpweb.jsonrequest
544     def qweblist(self, req, mods=None):
545         return manifest_list(req, mods, 'qweb')
546
547     @openerpweb.httprequest
548     def css(self, req, mods=None):
549         files = list(manifest_glob(req, mods, 'css'))
550         last_modified = get_last_modified(f[0] for f in files)
551         if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
552             return werkzeug.wrappers.Response(status=304)
553
554         file_map = dict(files)
555
556         rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
557         rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
558
559         def reader(f):
560             """read the a css file and absolutify all relative uris"""
561             with open(f, 'rb') as fp:
562                 data = fp.read().decode('utf-8')
563
564             path = file_map[f]
565             # convert FS path into web path
566             web_dir = '/'.join(os.path.dirname(path).split(os.path.sep))
567
568             data = re.sub(
569                 rx_import,
570                 r"""@import \1%s/""" % (web_dir,),
571                 data,
572             )
573
574             data = re.sub(
575                 rx_url,
576                 r"""url(\1%s/""" % (web_dir,),
577                 data,
578             )
579             return data.encode('utf-8')
580
581         content, checksum = concat_files((f[0] for f in files), reader)
582
583         return make_conditional(
584             req, req.make_response(content, [('Content-Type', 'text/css')]),
585             last_modified, checksum)
586
587     @openerpweb.httprequest
588     def js(self, req, mods=None):
589         files = [f[0] for f in manifest_glob(req, mods, 'js')]
590         last_modified = get_last_modified(files)
591         if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
592             return werkzeug.wrappers.Response(status=304)
593
594         content, checksum = concat_files(files, intersperse=';')
595
596         return make_conditional(
597             req, req.make_response(content, [('Content-Type', 'application/javascript')]),
598             last_modified, checksum)
599
600     @openerpweb.httprequest
601     def qweb(self, req, mods=None):
602         files = [f[0] for f in manifest_glob(req, mods, 'qweb')]
603         last_modified = get_last_modified(files)
604         if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
605             return werkzeug.wrappers.Response(status=304)
606
607         content, checksum = concat_xml(files)
608
609         return make_conditional(
610             req, req.make_response(content, [('Content-Type', 'text/xml')]),
611             last_modified, checksum)
612
613     @openerpweb.jsonrequest
614     def translations(self, req, mods, lang):
615         lang_model = req.session.model('res.lang')
616         ids = lang_model.search([("code", "=", lang)])
617         if ids:
618             lang_obj = lang_model.read(ids[0], ["direction", "date_format", "time_format",
619                                                 "grouping", "decimal_point", "thousands_sep"])
620         else:
621             lang_obj = None
622
623         if "_" in lang:
624             separator = "_"
625         else:
626             separator = "@"
627         langs = lang.split(separator)
628         langs = [separator.join(langs[:x]) for x in range(1, len(langs) + 1)]
629
630         transs = {}
631         for addon_name in mods:
632             transl = {"messages":[]}
633             transs[addon_name] = transl
634             addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
635             for l in langs:
636                 f_name = os.path.join(addons_path, addon_name, "i18n", l + ".po")
637                 if not os.path.exists(f_name):
638                     continue
639                 try:
640                     with open(f_name) as t_file:
641                         po = babel.messages.pofile.read_po(t_file)
642                 except Exception:
643                     continue
644                 for x in po:
645                     if x.id and x.string and "openerp-web" in x.auto_comments:
646                         transl["messages"].append({'id': x.id, 'string': x.string})
647         return {"modules": transs,
648                 "lang_parameters": lang_obj}
649
650     @openerpweb.jsonrequest
651     def version_info(self, req):
652         return {
653             "version": common.release.version
654         }
655
656 class Proxy(openerpweb.Controller):
657     _cp_path = '/web/proxy'
658
659     @openerpweb.jsonrequest
660     def load(self, req, path):
661         """ Proxies an HTTP request through a JSON request.
662
663         It is strongly recommended to not request binary files through this,
664         as the result will be a binary data blob as well.
665
666         :param req: OpenERP request
667         :param path: actual request path
668         :return: file content
669         """
670         from werkzeug.test import Client
671         from werkzeug.wrappers import BaseResponse
672
673         return Client(req.httprequest.app, BaseResponse).get(path).data
674
675 class Database(openerpweb.Controller):
676     _cp_path = "/web/database"
677
678     @openerpweb.jsonrequest
679     def get_list(self, req):
680         dbs = db_list(req)
681         return {"db_list": dbs}
682
683     @openerpweb.jsonrequest
684     def create(self, req, fields):
685         params = dict(map(operator.itemgetter('name', 'value'), fields))
686         create_attrs = (
687             params['super_admin_pwd'],
688             params['db_name'],
689             bool(params.get('demo_data')),
690             params['db_lang'],
691             params['create_admin_pwd']
692         )
693
694         return req.session.proxy("db").create_database(*create_attrs)
695
696     @openerpweb.jsonrequest
697     def drop(self, req, fields):
698         password, db = operator.itemgetter(
699             'drop_pwd', 'drop_db')(
700                 dict(map(operator.itemgetter('name', 'value'), fields)))
701
702         try:
703             return req.session.proxy("db").drop(password, db)
704         except xmlrpclib.Fault, e:
705             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
706                 return {'error': e.faultCode, 'title': 'Drop Database'}
707         return {'error': 'Could not drop database !', 'title': 'Drop Database'}
708
709     @openerpweb.httprequest
710     def backup(self, req, backup_db, backup_pwd, token):
711         try:
712             db_dump = base64.b64decode(
713                 req.session.proxy("db").dump(backup_pwd, backup_db))
714             filename = "%(db)s_%(timestamp)s.dump" % {
715                 'db': backup_db,
716                 'timestamp': datetime.datetime.utcnow().strftime(
717                     "%Y-%m-%d_%H-%M-%SZ")
718             }
719             return req.make_response(db_dump,
720                [('Content-Type', 'application/octet-stream; charset=binary'),
721                ('Content-Disposition', 'attachment; filename="' + filename + '"')],
722                {'fileToken': int(token)}
723             )
724         except xmlrpclib.Fault, e:
725              return simplejson.dumps([[],[{'error': e.faultCode, 'title': 'backup Database'}]])
726
727     @openerpweb.httprequest
728     def restore(self, req, db_file, restore_pwd, new_db):
729         try:
730             data = base64.b64encode(db_file.read())
731             req.session.proxy("db").restore(restore_pwd, new_db, data)
732             return ''
733         except xmlrpclib.Fault, e:
734             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
735                 raise Exception("AccessDenied")
736
737     @openerpweb.jsonrequest
738     def change_password(self, req, fields):
739         old_password, new_password = operator.itemgetter(
740             'old_pwd', 'new_pwd')(
741                 dict(map(operator.itemgetter('name', 'value'), fields)))
742         try:
743             return req.session.proxy("db").change_admin_password(old_password, new_password)
744         except xmlrpclib.Fault, e:
745             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
746                 return {'error': e.faultCode, 'title': 'Change Password'}
747         return {'error': 'Error, password not changed !', 'title': 'Change Password'}
748
749 class Session(openerpweb.Controller):
750     _cp_path = "/web/session"
751
752     def session_info(self, req):
753         req.session.ensure_valid()
754         return {
755             "session_id": req.session_id,
756             "uid": req.session._uid,
757             "context": req.session.get_context() if req.session._uid else {},
758             "db": req.session._db,
759             "login": req.session._login,
760             "openerp_entreprise": req.session.openerp_entreprise(),
761         }
762
763     @openerpweb.jsonrequest
764     def get_session_info(self, req):
765         return self.session_info(req)
766
767     @openerpweb.jsonrequest
768     def authenticate(self, req, db, login, password, base_location=None):
769         wsgienv = req.httprequest.environ
770         release = common.release
771         env = dict(
772             base_location=base_location,
773             HTTP_HOST=wsgienv['HTTP_HOST'],
774             REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
775             user_agent="%s / %s" % (release.name, release.version),
776         )
777         req.session.authenticate(db, login, password, env)
778
779         return self.session_info(req)
780
781     @openerpweb.jsonrequest
782     def change_password (self,req,fields):
783         old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
784                 dict(map(operator.itemgetter('name', 'value'), fields)))
785         if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
786             return {'error':'All passwords have to be filled.','title': 'Change Password'}
787         if new_password != confirm_password:
788             return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
789         try:
790             if req.session.model('res.users').change_password(
791                 old_password, new_password):
792                 return {'new_password':new_password}
793         except Exception:
794             return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
795         return {'error': 'Error, password not changed !', 'title': 'Change Password'}
796
797     @openerpweb.jsonrequest
798     def sc_list(self, req):
799         return req.session.model('ir.ui.view_sc').get_sc(
800             req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
801
802     @openerpweb.jsonrequest
803     def get_lang_list(self, req):
804         try:
805             return {
806                 'lang_list': (req.session.proxy("db").list_lang() or []),
807                 'error': ""
808             }
809         except Exception, e:
810             return {"error": e, "title": "Languages"}
811
812     @openerpweb.jsonrequest
813     def modules(self, req):
814         # return all installed modules. Web client is smart enough to not load a module twice
815         return module_installed(req)
816
817     @openerpweb.jsonrequest
818     def eval_domain_and_context(self, req, contexts, domains,
819                                 group_by_seq=None):
820         """ Evaluates sequences of domains and contexts, composing them into
821         a single context, domain or group_by sequence.
822
823         :param list contexts: list of contexts to merge together. Contexts are
824                               evaluated in sequence, all previous contexts
825                               are part of their own evaluation context
826                               (starting at the session context).
827         :param list domains: list of domains to merge together. Domains are
828                              evaluated in sequence and appended to one another
829                              (implicit AND), their evaluation domain is the
830                              result of merging all contexts.
831         :param list group_by_seq: list of domains (which may be in a different
832                                   order than the ``contexts`` parameter),
833                                   evaluated in sequence, their ``'group_by'``
834                                   key is extracted if they have one.
835         :returns:
836             a 3-dict of:
837
838             context (``dict``)
839                 the global context created by merging all of
840                 ``contexts``
841
842             domain (``list``)
843                 the concatenation of all domains
844
845             group_by (``list``)
846                 a list of fields to group by, potentially empty (in which case
847                 no group by should be performed)
848         """
849         context, domain = eval_context_and_domain(req.session,
850                                                   common.nonliterals.CompoundContext(*(contexts or [])),
851                                                   common.nonliterals.CompoundDomain(*(domains or [])))
852
853         group_by_sequence = []
854         for candidate in (group_by_seq or []):
855             ctx = req.session.eval_context(candidate, context)
856             group_by = ctx.get('group_by')
857             if not group_by:
858                 continue
859             elif isinstance(group_by, basestring):
860                 group_by_sequence.append(group_by)
861             else:
862                 group_by_sequence.extend(group_by)
863
864         return {
865             'context': context,
866             'domain': domain,
867             'group_by': group_by_sequence
868         }
869
870     @openerpweb.jsonrequest
871     def save_session_action(self, req, the_action):
872         """
873         This method store an action object in the session object and returns an integer
874         identifying that action. The method get_session_action() can be used to get
875         back the action.
876
877         :param the_action: The action to save in the session.
878         :type the_action: anything
879         :return: A key identifying the saved action.
880         :rtype: integer
881         """
882         saved_actions = req.httpsession.get('saved_actions')
883         if not saved_actions:
884             saved_actions = {"next":0, "actions":{}}
885             req.httpsession['saved_actions'] = saved_actions
886         # we don't allow more than 10 stored actions
887         if len(saved_actions["actions"]) >= 10:
888             del saved_actions["actions"][min(saved_actions["actions"])]
889         key = saved_actions["next"]
890         saved_actions["actions"][key] = the_action
891         saved_actions["next"] = key + 1
892         return key
893
894     @openerpweb.jsonrequest
895     def get_session_action(self, req, key):
896         """
897         Gets back a previously saved action. This method can return None if the action
898         was saved since too much time (this case should be handled in a smart way).
899
900         :param key: The key given by save_session_action()
901         :type key: integer
902         :return: The saved action or None.
903         :rtype: anything
904         """
905         saved_actions = req.httpsession.get('saved_actions')
906         if not saved_actions:
907             return None
908         return saved_actions["actions"].get(key)
909
910     @openerpweb.jsonrequest
911     def check(self, req):
912         req.session.assert_valid()
913         return None
914
915     @openerpweb.jsonrequest
916     def destroy(self, req):
917         req.session._suicide = True
918
919 class Menu(openerpweb.Controller):
920     _cp_path = "/web/menu"
921
922     @openerpweb.jsonrequest
923     def load(self, req):
924         return {'data': self.do_load(req)}
925
926     @openerpweb.jsonrequest
927     def get_user_roots(self, req):
928         return self.do_get_user_roots(req)
929
930     def do_get_user_roots(self, req):
931         """ Return all root menu ids visible for the session user.
932
933         :param req: A request object, with an OpenERP session attribute
934         :type req: < session -> OpenERPSession >
935         :return: the root menu ids
936         :rtype: list(int)
937         """
938         s = req.session
939         context = s.eval_context(req.context)
940         Menus = s.model('ir.ui.menu')
941         # If a menu action is defined use its domain to get the root menu items
942         user_menu_id = s.model('res.users').read([s._uid], ['menu_id'], context)[0]['menu_id']
943
944         menu_domain = [('parent_id', '=', False)]
945         if user_menu_id:
946             domain_string = s.model('ir.actions.act_window').read([user_menu_id[0]], ['domain'], context)[0]['domain']
947             if domain_string:
948                 menu_domain = ast.literal_eval(domain_string)
949
950         return Menus.search(menu_domain, 0, False, False, context)
951
952     def do_load(self, req):
953         """ Loads all menu items (all applications and their sub-menus).
954
955         :param req: A request object, with an OpenERP session attribute
956         :type req: < session -> OpenERPSession >
957         :return: the menu root
958         :rtype: dict('children': menu_nodes)
959         """
960         context = req.session.eval_context(req.context)
961         Menus = req.session.model('ir.ui.menu')
962
963         menu_roots = Menus.read(self.do_get_user_roots(req), ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
964         menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children' : menu_roots}
965
966         # menus are loaded fully unlike a regular tree view, cause there are a
967         # limited number of items (752 when all 6.1 addons are installed)
968         menu_ids = Menus.search([], 0, False, False, context)
969         menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
970         # adds roots at the end of the sequence, so that they will overwrite
971         # equivalent menu items from full menu read when put into id:item
972         # mapping, resulting in children being correctly set on the roots.
973         menu_items.extend(menu_roots)
974
975         # make a tree using parent_id
976         menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
977         for menu_item in menu_items:
978             if menu_item['parent_id']:
979                 parent = menu_item['parent_id'][0]
980             else:
981                 parent = False
982             if parent in menu_items_map:
983                 menu_items_map[parent].setdefault(
984                     'children', []).append(menu_item)
985
986         # sort by sequence a tree using parent_id
987         for menu_item in menu_items:
988             menu_item.setdefault('children', []).sort(
989                 key=operator.itemgetter('sequence'))
990
991         return menu_root
992
993     @openerpweb.jsonrequest
994     def action(self, req, menu_id):
995         actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
996                                              [('ir.ui.menu', menu_id)], False)
997         return {"action": actions}
998
999 class DataSet(openerpweb.Controller):
1000     _cp_path = "/web/dataset"
1001
1002     @openerpweb.jsonrequest
1003     def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
1004         return self.do_search_read(req, model, fields, offset, limit, domain, sort)
1005     def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
1006                        , sort=None):
1007         """ Performs a search() followed by a read() (if needed) using the
1008         provided search criteria
1009
1010         :param req: a JSON-RPC request object
1011         :type req: openerpweb.JsonRequest
1012         :param str model: the name of the model to search on
1013         :param fields: a list of the fields to return in the result records
1014         :type fields: [str]
1015         :param int offset: from which index should the results start being returned
1016         :param int limit: the maximum number of records to return
1017         :param list domain: the search domain for the query
1018         :param list sort: sorting directives
1019         :returns: A structure (dict) with two keys: ids (all the ids matching
1020                   the (domain, context) pair) and records (paginated records
1021                   matching fields selection set)
1022         :rtype: list
1023         """
1024         Model = req.session.model(model)
1025
1026         context, domain = eval_context_and_domain(
1027             req.session, req.context, domain)
1028
1029         ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
1030         if limit and len(ids) == limit:
1031             length = Model.search_count(domain, context)
1032         else:
1033             length = len(ids) + (offset or 0)
1034         if fields and fields == ['id']:
1035             # shortcut read if we only want the ids
1036             return {
1037                 'length': length,
1038                 'records': [{'id': id} for id in ids]
1039             }
1040
1041         records = Model.read(ids, fields or False, context)
1042         records.sort(key=lambda obj: ids.index(obj['id']))
1043         return {
1044             'length': length,
1045             'records': records
1046         }
1047
1048     @openerpweb.jsonrequest
1049     def load(self, req, model, id, fields):
1050         m = req.session.model(model)
1051         value = {}
1052         r = m.read([id], False, req.session.eval_context(req.context))
1053         if r:
1054             value = r[0]
1055         return {'value': value}
1056
1057     def call_common(self, req, model, method, args, domain_id=None, context_id=None):
1058         has_domain = domain_id is not None and domain_id < len(args)
1059         has_context = context_id is not None and context_id < len(args)
1060
1061         domain = args[domain_id] if has_domain else []
1062         context = args[context_id] if has_context else {}
1063         c, d = eval_context_and_domain(req.session, context, domain)
1064         if has_domain:
1065             args[domain_id] = d
1066         if has_context:
1067             args[context_id] = c
1068
1069         return self._call_kw(req, model, method, args, {})
1070     
1071     def _call_kw(self, req, model, method, args, kwargs):
1072         for i in xrange(len(args)):
1073             if isinstance(args[i], common.nonliterals.BaseContext):
1074                 args[i] = req.session.eval_context(args[i])
1075             elif isinstance(args[i], common.nonliterals.BaseDomain):
1076                 args[i] = req.session.eval_domain(args[i])
1077         for k in kwargs.keys():
1078             if isinstance(kwargs[k], common.nonliterals.BaseContext):
1079                 kwargs[k] = req.session.eval_context(kwargs[k])
1080             elif isinstance(kwargs[k], common.nonliterals.BaseDomain):
1081                 kwargs[k] = req.session.eval_domain(kwargs[k])
1082
1083         # Temporary implements future display_name special field for model#read()
1084         if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
1085             if 'display_name' in args[1]:
1086                 names = req.session.model(model).name_get(args[0], **kwargs)
1087                 args[1].remove('display_name')
1088                 r = getattr(req.session.model(model), method)(*args, **kwargs)
1089                 for i in range(len(r)):
1090                     r[i]['display_name'] = names[i][1] or "%s#%d" % (model, names[i][0])
1091                 return r
1092
1093         return getattr(req.session.model(model), method)(*args, **kwargs)
1094
1095     @openerpweb.jsonrequest
1096     def onchange(self, req, model, method, args, context_id=None):
1097         """ Support method for handling onchange calls: behaves much like call
1098         with the following differences:
1099
1100         * Does not take a domain_id
1101         * Is aware of the return value's structure, and will parse the domains
1102           if needed in order to return either parsed literal domains (in JSON)
1103           or non-literal domain instances, allowing those domains to be used
1104           from JS
1105
1106         :param req:
1107         :type req: web.common.http.JsonRequest
1108         :param str model: object type on which to call the method
1109         :param str method: name of the onchange handler method
1110         :param list args: arguments to call the onchange handler with
1111         :param int context_id: index of the context object in the list of
1112                                arguments
1113         :return: result of the onchange call with all domains parsed
1114         """
1115         result = self.call_common(req, model, method, args, context_id=context_id)
1116         if not result or 'domain' not in result:
1117             return result
1118
1119         result['domain'] = dict(
1120             (k, parse_domain(v, req.session))
1121             for k, v in result['domain'].iteritems())
1122
1123         return result
1124
1125     @openerpweb.jsonrequest
1126     def call(self, req, model, method, args, domain_id=None, context_id=None):
1127         return self.call_common(req, model, method, args, domain_id, context_id)
1128     
1129     @openerpweb.jsonrequest
1130     def call_kw(self, req, model, method, args, kwargs):
1131         return self._call_kw(req, model, method, args, kwargs)
1132
1133     @openerpweb.jsonrequest
1134     def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1135         action = self.call_common(req, model, method, args, domain_id, context_id)
1136         if isinstance(action, dict) and action.get('type') != '':
1137             return {'result': clean_action(req, action)}
1138         return {'result': False}
1139
1140     @openerpweb.jsonrequest
1141     def exec_workflow(self, req, model, id, signal):
1142         return req.session.exec_workflow(model, id, signal)
1143
1144 class DataGroup(openerpweb.Controller):
1145     _cp_path = "/web/group"
1146     @openerpweb.jsonrequest
1147     def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
1148         Model = req.session.model(model)
1149         context, domain = eval_context_and_domain(req.session, req.context, domain)
1150
1151         return Model.read_group(
1152             domain or [], fields, group_by_fields, 0, False,
1153             dict(context, group_by=group_by_fields), sort or False)
1154
1155 class View(openerpweb.Controller):
1156     _cp_path = "/web/view"
1157
1158     def fields_view_get(self, req, model, view_id, view_type,
1159                         transform=True, toolbar=False, submenu=False):
1160         Model = req.session.model(model)
1161         context = req.session.eval_context(req.context)
1162         fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
1163         # todo fme?: check that we should pass the evaluated context here
1164         self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
1165         if toolbar and transform:
1166             self.process_toolbar(req, fvg['toolbar'])
1167         return fvg
1168
1169     def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
1170         # depending on how it feels, xmlrpclib.ServerProxy can translate
1171         # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
1172         # enjoy unicode strings which can not be trivially converted to
1173         # strings, and it blows up during parsing.
1174
1175         # So ensure we fix this retardation by converting view xml back to
1176         # bit strings.
1177         if isinstance(fvg['arch'], unicode):
1178             arch = fvg['arch'].encode('utf-8')
1179         else:
1180             arch = fvg['arch']
1181         fvg['arch_string'] = arch
1182
1183         if transform:
1184             evaluation_context = session.evaluation_context(context or {})
1185             xml = self.transform_view(arch, session, evaluation_context)
1186         else:
1187             xml = ElementTree.fromstring(arch)
1188         fvg['arch'] = common.xml2json.from_elementtree(xml, preserve_whitespaces)
1189
1190         if 'id' in fvg['fields']:
1191             # Special case for id's
1192             id_field = fvg['fields']['id']
1193             id_field['original_type'] = id_field['type']
1194             id_field['type'] = 'id'
1195
1196         for field in fvg['fields'].itervalues():
1197             if field.get('views'):
1198                 for view in field["views"].itervalues():
1199                     self.process_view(session, view, None, transform)
1200             if field.get('domain'):
1201                 field["domain"] = parse_domain(field["domain"], session)
1202             if field.get('context'):
1203                 field["context"] = parse_context(field["context"], session)
1204
1205     def process_toolbar(self, req, toolbar):
1206         """
1207         The toolbar is a mapping of section_key: [action_descriptor]
1208
1209         We need to clean all those actions in order to ensure correct
1210         round-tripping
1211         """
1212         for actions in toolbar.itervalues():
1213             for action in actions:
1214                 if 'context' in action:
1215                     action['context'] = parse_context(
1216                         action['context'], req.session)
1217                 if 'domain' in action:
1218                     action['domain'] = parse_domain(
1219                         action['domain'], req.session)
1220
1221     @openerpweb.jsonrequest
1222     def add_custom(self, req, view_id, arch):
1223         CustomView = req.session.model('ir.ui.view.custom')
1224         CustomView.create({
1225             'user_id': req.session._uid,
1226             'ref_id': view_id,
1227             'arch': arch
1228         }, req.session.eval_context(req.context))
1229         return {'result': True}
1230
1231     @openerpweb.jsonrequest
1232     def undo_custom(self, req, view_id, reset=False):
1233         CustomView = req.session.model('ir.ui.view.custom')
1234         context = req.session.eval_context(req.context)
1235         vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1236                                     0, False, False, context)
1237         if vcustom:
1238             if reset:
1239                 CustomView.unlink(vcustom, context)
1240             else:
1241                 CustomView.unlink([vcustom[0]], context)
1242             return {'result': True}
1243         return {'result': False}
1244
1245     def transform_view(self, view_string, session, context=None):
1246         # transform nodes on the fly via iterparse, instead of
1247         # doing it statically on the parsing result
1248         parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
1249         root = None
1250         for event, elem in parser:
1251             if event == "start":
1252                 if root is None:
1253                     root = elem
1254                 self.parse_domains_and_contexts(elem, session)
1255         return root
1256
1257     def parse_domains_and_contexts(self, elem, session):
1258         """ Converts domains and contexts from the view into Python objects,
1259         either literals if they can be parsed by literal_eval or a special
1260         placeholder object if the domain or context refers to free variables.
1261
1262         :param elem: the current node being parsed
1263         :type param: xml.etree.ElementTree.Element
1264         :param session: OpenERP session object, used to store and retrieve
1265                         non-literal objects
1266         :type session: openerpweb.openerpweb.OpenERPSession
1267         """
1268         for el in ['domain', 'filter_domain']:
1269             domain = elem.get(el, '').strip()
1270             if domain:
1271                 elem.set(el, parse_domain(domain, session))
1272                 elem.set(el + '_string', domain)
1273         for el in ['context', 'default_get']:
1274             context_string = elem.get(el, '').strip()
1275             if context_string:
1276                 elem.set(el, parse_context(context_string, session))
1277                 elem.set(el + '_string', context_string)
1278
1279     @openerpweb.jsonrequest
1280     def load(self, req, model, view_id, view_type, toolbar=False):
1281         return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1282
1283 class ListView(View):
1284     _cp_path = "/web/listview"
1285
1286     def process_colors(self, view, row, context):
1287         colors = view['arch']['attrs'].get('colors')
1288
1289         if not colors:
1290             return None
1291
1292         color = [
1293             pair.split(':')[0]
1294             for pair in colors.split(';')
1295             if eval(pair.split(':')[1], dict(context, **row))
1296         ]
1297
1298         if not color:
1299             return None
1300         elif len(color) == 1:
1301             return color[0]
1302         return 'maroon'
1303
1304 class TreeView(View):
1305     _cp_path = "/web/treeview"
1306
1307     @openerpweb.jsonrequest
1308     def action(self, req, model, id):
1309         return load_actions_from_ir_values(
1310             req,'action', 'tree_but_open',[(model, id)],
1311             False)
1312
1313 class SearchView(View):
1314     _cp_path = "/web/searchview"
1315
1316     @openerpweb.jsonrequest
1317     def load(self, req, model, view_id):
1318         fields_view = self.fields_view_get(req, model, view_id, 'search')
1319         return {'fields_view': fields_view}
1320
1321     @openerpweb.jsonrequest
1322     def fields_get(self, req, model):
1323         Model = req.session.model(model)
1324         fields = Model.fields_get(False, req.session.eval_context(req.context))
1325         for field in fields.values():
1326             # shouldn't convert the views too?
1327             if field.get('domain'):
1328                 field["domain"] = parse_domain(field["domain"], req.session)
1329             if field.get('context'):
1330                 field["context"] = parse_context(field["context"], req.session)
1331         return {'fields': fields}
1332
1333     @openerpweb.jsonrequest
1334     def get_filters(self, req, model):
1335         logger = logging.getLogger(__name__ + '.SearchView.get_filters')
1336         Model = req.session.model("ir.filters")
1337         filters = Model.get_filters(model)
1338         for filter in filters:
1339             try:
1340                 parsed_context = parse_context(filter["context"], req.session)
1341                 filter["context"] = (parsed_context
1342                         if not isinstance(parsed_context, common.nonliterals.BaseContext)
1343                         else req.session.eval_context(parsed_context))
1344
1345                 parsed_domain = parse_domain(filter["domain"], req.session)
1346                 filter["domain"] = (parsed_domain
1347                         if not isinstance(parsed_domain, common.nonliterals.BaseDomain)
1348                         else req.session.eval_domain(parsed_domain))
1349             except Exception:
1350                 logger.exception("Failed to parse custom filter %s in %s",
1351                                  filter['name'], model)
1352                 filter['disabled'] = True
1353                 del filter['context']
1354                 del filter['domain']
1355         return filters
1356
1357 class Binary(openerpweb.Controller):
1358     _cp_path = "/web/binary"
1359
1360     @openerpweb.httprequest
1361     def image(self, req, model, id, field, **kw):
1362         last_update = '__last_update'
1363         Model = req.session.model(model)
1364         context = req.session.eval_context(req.context)
1365         headers = [('Content-Type', 'image/png')]
1366         etag = req.httprequest.headers.get('If-None-Match')
1367         hashed_session = hashlib.md5(req.session_id).hexdigest()
1368         id = None if not id else simplejson.loads(id)
1369         if type(id) is list:
1370             id = id[0] # m2o
1371         if etag:
1372             if not id and hashed_session == etag:
1373                 return werkzeug.wrappers.Response(status=304)
1374             else:
1375                 date = Model.read([id], [last_update], context)[0].get(last_update)
1376                 if hashlib.md5(date).hexdigest() == etag:
1377                     return werkzeug.wrappers.Response(status=304)
1378
1379         retag = hashed_session
1380         try:
1381             if not id:
1382                 res = Model.default_get([field], context).get(field)
1383                 image_data = base64.b64decode(res)
1384             else:
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}}))
2002
2003 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: