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