[WIP] Resequence
[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         with registry.cursor() as cr:
168             m = registry.get('ir.module.module')
169             # TODO The following code should move to ir.module.module.list_installed_modules()
170             domain = [('state','=','installed'), ('name','in', loadable)]
171             ids = m.search(cr, 1, [('state','=','installed'), ('name','in', loadable)])
172             for module in m.read(cr, 1, ids, ['name', 'dependencies_id']):
173                 modules[module['name']] = []
174                 deps = module.get('dependencies_id')
175                 if deps:
176                     deps_read = registry.get('ir.module.module.dependency').read(cr, 1, deps, ['name'])
177                     dependencies = [i['name'] for i in deps_read]
178                     modules[module['name']] = dependencies
179     except Exception,e:
180         pass
181     sorted_modules = module_topological_sort(modules)
182     return sorted_modules
183
184 def module_boot(req):
185     dbs = db_list(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 len(dbs) == 1:
192         # if only one db load every module at boot
193         dbside = module_installed_bypass_session(dbs[0])
194         dbside = [i for i in dbside if i not in serverside]
195     addons = serverside + dbside
196     return addons
197
198 def concat_xml(file_list):
199     """Concatenate xml files
200
201     :param list(str) file_list: list of files to check
202     :returns: (concatenation_result, checksum)
203     :rtype: (str, str)
204     """
205     checksum = hashlib.new('sha1')
206     if not file_list:
207         return '', checksum.hexdigest()
208
209     root = None
210     for fname in file_list:
211         with open(fname, 'rb') as fp:
212             contents = fp.read()
213             checksum.update(contents)
214             fp.seek(0)
215             xml = ElementTree.parse(fp).getroot()
216
217         if root is None:
218             root = ElementTree.Element(xml.tag)
219         #elif root.tag != xml.tag:
220         #    raise ValueError("Root tags missmatch: %r != %r" % (root.tag, xml.tag))
221
222         for child in xml.getchildren():
223             root.append(child)
224     return ElementTree.tostring(root, 'utf-8'), checksum.hexdigest()
225
226 def concat_files(file_list, reader=None, intersperse=""):
227     """ Concatenates contents of all provided files
228
229     :param list(str) file_list: list of files to check
230     :param function reader: reading procedure for each file
231     :param str intersperse: string to intersperse between file contents
232     :returns: (concatenation_result, checksum)
233     :rtype: (str, str)
234     """
235     checksum = hashlib.new('sha1')
236     if not file_list:
237         return '', checksum.hexdigest()
238
239     if reader is None:
240         def reader(f):
241             with open(f, 'rb') as fp:
242                 return fp.read()
243
244     files_content = []
245     for fname in file_list:
246         contents = reader(fname)
247         checksum.update(contents)
248         files_content.append(contents)
249
250     files_concat = intersperse.join(files_content)
251     return files_concat, checksum.hexdigest()
252
253 def manifest_glob(req, addons, key):
254     if addons is None:
255         addons = module_boot(req)
256     else:
257         addons = addons.split(',')
258     r = []
259     for addon in addons:
260         manifest = openerpweb.addons_manifest.get(addon, None)
261         if not manifest:
262             continue
263         # ensure does not ends with /
264         addons_path = os.path.join(manifest['addons_path'], '')[:-1]
265         globlist = manifest.get(key, [])
266         for pattern in globlist:
267             for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
268                 r.append((path, path[len(addons_path):]))
269     return r
270
271 def manifest_list(req, mods, extension):
272     if not req.debug:
273         path = '/web/webclient/' + extension
274         if mods is not None:
275             path += '?mods=' + mods
276         return [path]
277     files = manifest_glob(req, mods, extension)
278     i_am_diabetic = req.httprequest.environ["QUERY_STRING"].count("no_sugar") >= 1 or \
279                     req.httprequest.environ.get('HTTP_REFERER', '').count("no_sugar") >= 1
280     if i_am_diabetic:
281         return [wp for _fp, wp in files]
282     else:
283         return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in files]
284
285 def get_last_modified(files):
286     """ Returns the modification time of the most recently modified
287     file provided
288
289     :param list(str) files: names of files to check
290     :return: most recent modification time amongst the fileset
291     :rtype: datetime.datetime
292     """
293     files = list(files)
294     if files:
295         return max(datetime.datetime.fromtimestamp(os.path.getmtime(f))
296                    for f in files)
297     return datetime.datetime(1970, 1, 1)
298
299 def make_conditional(req, response, last_modified=None, etag=None):
300     """ Makes the provided response conditional based upon the request,
301     and mandates revalidation from clients
302
303     Uses Werkzeug's own :meth:`ETagResponseMixin.make_conditional`, after
304     setting ``last_modified`` and ``etag`` correctly on the response object
305
306     :param req: OpenERP request
307     :type req: web.common.http.WebRequest
308     :param response: Werkzeug response
309     :type response: werkzeug.wrappers.Response
310     :param datetime.datetime last_modified: last modification date of the response content
311     :param str etag: some sort of checksum of the content (deep etag)
312     :return: the response object provided
313     :rtype: werkzeug.wrappers.Response
314     """
315     response.cache_control.must_revalidate = True
316     response.cache_control.max_age = 0
317     if last_modified:
318         response.last_modified = last_modified
319     if etag:
320         response.set_etag(etag)
321     return response.make_conditional(req.httprequest)
322
323 def login_and_redirect(req, db, login, key, redirect_url='/'):
324     req.session.authenticate(db, login, key, {})
325     return set_cookie_and_redirect(req, redirect_url)
326
327 def set_cookie_and_redirect(req, redirect_url):
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     @openerpweb.jsonrequest
1140     def resequence(self, req, model, ids):
1141         m = req.session.model(model)
1142         if not len(m.fields_get(['sequence'])):
1143             return False
1144         for i in range(len(ids)):
1145             m.write([ids[i]], { 'sequence': i })
1146         return True
1147
1148 class DataGroup(openerpweb.Controller):
1149     _cp_path = "/web/group"
1150     @openerpweb.jsonrequest
1151     def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
1152         Model = req.session.model(model)
1153         context, domain = eval_context_and_domain(req.session, req.context, domain)
1154
1155         return Model.read_group(
1156             domain or [], fields, group_by_fields, 0, False,
1157             dict(context, group_by=group_by_fields), sort or False)
1158
1159 class View(openerpweb.Controller):
1160     _cp_path = "/web/view"
1161
1162     def fields_view_get(self, req, model, view_id, view_type,
1163                         transform=True, toolbar=False, submenu=False):
1164         Model = req.session.model(model)
1165         context = req.session.eval_context(req.context)
1166         fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
1167         # todo fme?: check that we should pass the evaluated context here
1168         self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
1169         if toolbar and transform:
1170             self.process_toolbar(req, fvg['toolbar'])
1171         return fvg
1172
1173     def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
1174         # depending on how it feels, xmlrpclib.ServerProxy can translate
1175         # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
1176         # enjoy unicode strings which can not be trivially converted to
1177         # strings, and it blows up during parsing.
1178
1179         # So ensure we fix this retardation by converting view xml back to
1180         # bit strings.
1181         if isinstance(fvg['arch'], unicode):
1182             arch = fvg['arch'].encode('utf-8')
1183         else:
1184             arch = fvg['arch']
1185         fvg['arch_string'] = arch
1186
1187         if transform:
1188             evaluation_context = session.evaluation_context(context or {})
1189             xml = self.transform_view(arch, session, evaluation_context)
1190         else:
1191             xml = ElementTree.fromstring(arch)
1192         fvg['arch'] = common.xml2json.from_elementtree(xml, preserve_whitespaces)
1193
1194         if 'id' in fvg['fields']:
1195             # Special case for id's
1196             id_field = fvg['fields']['id']
1197             id_field['original_type'] = id_field['type']
1198             id_field['type'] = 'id'
1199
1200         for field in fvg['fields'].itervalues():
1201             if field.get('views'):
1202                 for view in field["views"].itervalues():
1203                     self.process_view(session, view, None, transform)
1204             if field.get('domain'):
1205                 field["domain"] = parse_domain(field["domain"], session)
1206             if field.get('context'):
1207                 field["context"] = parse_context(field["context"], session)
1208
1209     def process_toolbar(self, req, toolbar):
1210         """
1211         The toolbar is a mapping of section_key: [action_descriptor]
1212
1213         We need to clean all those actions in order to ensure correct
1214         round-tripping
1215         """
1216         for actions in toolbar.itervalues():
1217             for action in actions:
1218                 if 'context' in action:
1219                     action['context'] = parse_context(
1220                         action['context'], req.session)
1221                 if 'domain' in action:
1222                     action['domain'] = parse_domain(
1223                         action['domain'], req.session)
1224
1225     @openerpweb.jsonrequest
1226     def add_custom(self, req, view_id, arch):
1227         CustomView = req.session.model('ir.ui.view.custom')
1228         CustomView.create({
1229             'user_id': req.session._uid,
1230             'ref_id': view_id,
1231             'arch': arch
1232         }, req.session.eval_context(req.context))
1233         return {'result': True}
1234
1235     @openerpweb.jsonrequest
1236     def undo_custom(self, req, view_id, reset=False):
1237         CustomView = req.session.model('ir.ui.view.custom')
1238         context = req.session.eval_context(req.context)
1239         vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1240                                     0, False, False, context)
1241         if vcustom:
1242             if reset:
1243                 CustomView.unlink(vcustom, context)
1244             else:
1245                 CustomView.unlink([vcustom[0]], context)
1246             return {'result': True}
1247         return {'result': False}
1248
1249     def transform_view(self, view_string, session, context=None):
1250         # transform nodes on the fly via iterparse, instead of
1251         # doing it statically on the parsing result
1252         parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
1253         root = None
1254         for event, elem in parser:
1255             if event == "start":
1256                 if root is None:
1257                     root = elem
1258                 self.parse_domains_and_contexts(elem, session)
1259         return root
1260
1261     def parse_domains_and_contexts(self, elem, session):
1262         """ Converts domains and contexts from the view into Python objects,
1263         either literals if they can be parsed by literal_eval or a special
1264         placeholder object if the domain or context refers to free variables.
1265
1266         :param elem: the current node being parsed
1267         :type param: xml.etree.ElementTree.Element
1268         :param session: OpenERP session object, used to store and retrieve
1269                         non-literal objects
1270         :type session: openerpweb.openerpweb.OpenERPSession
1271         """
1272         for el in ['domain', 'filter_domain']:
1273             domain = elem.get(el, '').strip()
1274             if domain:
1275                 elem.set(el, parse_domain(domain, session))
1276                 elem.set(el + '_string', domain)
1277         for el in ['context', 'default_get']:
1278             context_string = elem.get(el, '').strip()
1279             if context_string:
1280                 elem.set(el, parse_context(context_string, session))
1281                 elem.set(el + '_string', context_string)
1282
1283     @openerpweb.jsonrequest
1284     def load(self, req, model, view_id, view_type, toolbar=False):
1285         return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1286
1287 class ListView(View):
1288     _cp_path = "/web/listview"
1289
1290     def process_colors(self, view, row, context):
1291         colors = view['arch']['attrs'].get('colors')
1292
1293         if not colors:
1294             return None
1295
1296         color = [
1297             pair.split(':')[0]
1298             for pair in colors.split(';')
1299             if eval(pair.split(':')[1], dict(context, **row))
1300         ]
1301
1302         if not color:
1303             return None
1304         elif len(color) == 1:
1305             return color[0]
1306         return 'maroon'
1307
1308 class TreeView(View):
1309     _cp_path = "/web/treeview"
1310
1311     @openerpweb.jsonrequest
1312     def action(self, req, model, id):
1313         return load_actions_from_ir_values(
1314             req,'action', 'tree_but_open',[(model, id)],
1315             False)
1316
1317 class SearchView(View):
1318     _cp_path = "/web/searchview"
1319
1320     @openerpweb.jsonrequest
1321     def load(self, req, model, view_id):
1322         fields_view = self.fields_view_get(req, model, view_id, 'search')
1323         return {'fields_view': fields_view}
1324
1325     @openerpweb.jsonrequest
1326     def fields_get(self, req, model):
1327         Model = req.session.model(model)
1328         fields = Model.fields_get(False, req.session.eval_context(req.context))
1329         for field in fields.values():
1330             # shouldn't convert the views too?
1331             if field.get('domain'):
1332                 field["domain"] = parse_domain(field["domain"], req.session)
1333             if field.get('context'):
1334                 field["context"] = parse_context(field["context"], req.session)
1335         return {'fields': fields}
1336
1337     @openerpweb.jsonrequest
1338     def get_filters(self, req, model):
1339         logger = logging.getLogger(__name__ + '.SearchView.get_filters')
1340         Model = req.session.model("ir.filters")
1341         filters = Model.get_filters(model)
1342         for filter in filters:
1343             try:
1344                 parsed_context = parse_context(filter["context"], req.session)
1345                 filter["context"] = (parsed_context
1346                         if not isinstance(parsed_context, common.nonliterals.BaseContext)
1347                         else req.session.eval_context(parsed_context))
1348
1349                 parsed_domain = parse_domain(filter["domain"], req.session)
1350                 filter["domain"] = (parsed_domain
1351                         if not isinstance(parsed_domain, common.nonliterals.BaseDomain)
1352                         else req.session.eval_domain(parsed_domain))
1353             except Exception:
1354                 logger.exception("Failed to parse custom filter %s in %s",
1355                                  filter['name'], model)
1356                 filter['disabled'] = True
1357                 del filter['context']
1358                 del filter['domain']
1359         return filters
1360
1361 class Binary(openerpweb.Controller):
1362     _cp_path = "/web/binary"
1363
1364     @openerpweb.httprequest
1365     def image(self, req, model, id, field, **kw):
1366         last_update = '__last_update'
1367         Model = req.session.model(model)
1368         context = req.session.eval_context(req.context)
1369         headers = [('Content-Type', 'image/png')]
1370         etag = req.httprequest.headers.get('If-None-Match')
1371         hashed_session = hashlib.md5(req.session_id).hexdigest()
1372         id = None if not id else simplejson.loads(id)
1373         if type(id) is list:
1374             id = id[0] # m2o
1375         if etag:
1376             if not id and hashed_session == etag:
1377                 return werkzeug.wrappers.Response(status=304)
1378             else:
1379                 date = Model.read([id], [last_update], context)[0].get(last_update)
1380                 if hashlib.md5(date).hexdigest() == etag:
1381                     return werkzeug.wrappers.Response(status=304)
1382
1383         retag = hashed_session
1384         try:
1385             if not id:
1386                 res = Model.default_get([field], context).get(field)
1387                 image_data = base64.b64decode(res)
1388             else:
1389                 res = Model.read([id], [last_update, field], context)[0]
1390                 retag = hashlib.md5(res.get(last_update)).hexdigest()
1391                 image_data = base64.b64decode(res.get(field))
1392         except (TypeError, xmlrpclib.Fault):
1393             image_data = self.placeholder(req)
1394         headers.append(('ETag', retag))
1395         headers.append(('Content-Length', len(image_data)))
1396         try:
1397             ncache = int(kw.get('cache'))
1398             headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1399         except:
1400             pass
1401         return req.make_response(image_data, headers)
1402     def placeholder(self, req):
1403         addons_path = openerpweb.addons_manifest['web']['addons_path']
1404         return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1405     def content_disposition(self, filename, req):
1406         filename = filename.encode('utf8')
1407         escaped = urllib2.quote(filename)
1408         browser = req.httprequest.user_agent.browser
1409         version = int((req.httprequest.user_agent.version or '0').split('.')[0])
1410         if browser == 'msie' and version < 9:
1411             return "attachment; filename=%s" % escaped
1412         elif browser == 'safari':
1413             return "attachment; filename=%s" % filename
1414         else:
1415             return "attachment; filename*=UTF-8''%s" % escaped
1416
1417     @openerpweb.httprequest
1418     def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1419         """ Download link for files stored as binary fields.
1420
1421         If the ``id`` parameter is omitted, fetches the default value for the
1422         binary field (via ``default_get``), otherwise fetches the field for
1423         that precise record.
1424
1425         :param req: OpenERP request
1426         :type req: :class:`web.common.http.HttpRequest`
1427         :param str model: name of the model to fetch the binary from
1428         :param str field: binary field
1429         :param str id: id of the record from which to fetch the binary
1430         :param str filename_field: field holding the file's name, if any
1431         :returns: :class:`werkzeug.wrappers.Response`
1432         """
1433         Model = req.session.model(model)
1434         context = req.session.eval_context(req.context)
1435         fields = [field]
1436         if filename_field:
1437             fields.append(filename_field)
1438         if id:
1439             res = Model.read([int(id)], fields, context)[0]
1440         else:
1441             res = Model.default_get(fields, context)
1442         filecontent = base64.b64decode(res.get(field, ''))
1443         if not filecontent:
1444             return req.not_found()
1445         else:
1446             filename = '%s_%s' % (model.replace('.', '_'), id)
1447             if filename_field:
1448                 filename = res.get(filename_field, '') or filename
1449             return req.make_response(filecontent,
1450                 [('Content-Type', 'application/octet-stream'),
1451                  ('Content-Disposition', self.content_disposition(filename, req))])
1452
1453     @openerpweb.httprequest
1454     def saveas_ajax(self, req, data, token):
1455         jdata = simplejson.loads(data)
1456         model = jdata['model']
1457         field = jdata['field']
1458         id = jdata.get('id', None)
1459         filename_field = jdata.get('filename_field', None)
1460         context = jdata.get('context', dict())
1461
1462         context = req.session.eval_context(context)
1463         Model = req.session.model(model)
1464         fields = [field]
1465         if filename_field:
1466             fields.append(filename_field)
1467         if id:
1468             res = Model.read([int(id)], fields, context)[0]
1469         else:
1470             res = Model.default_get(fields, context)
1471         filecontent = base64.b64decode(res.get(field, ''))
1472         if not filecontent:
1473             raise ValueError("No content found for field '%s' on '%s:%s'" %
1474                 (field, model, id))
1475         else:
1476             filename = '%s_%s' % (model.replace('.', '_'), id)
1477             if filename_field:
1478                 filename = res.get(filename_field, '') or filename
1479             return req.make_response(filecontent,
1480                 headers=[('Content-Type', 'application/octet-stream'),
1481                         ('Content-Disposition', self.content_disposition(filename, req))],
1482                 cookies={'fileToken': int(token)})
1483
1484     @openerpweb.httprequest
1485     def upload(self, req, callback, ufile):
1486         # TODO: might be useful to have a configuration flag for max-length file uploads
1487         try:
1488             out = """<script language="javascript" type="text/javascript">
1489                         var win = window.top.window;
1490                         win.jQuery(win).trigger(%s, %s);
1491                     </script>"""
1492             data = ufile.read()
1493             args = [len(data), ufile.filename,
1494                     ufile.content_type, base64.b64encode(data)]
1495         except Exception, e:
1496             args = [False, e.message]
1497         return out % (simplejson.dumps(callback), simplejson.dumps(args))
1498
1499     @openerpweb.httprequest
1500     def upload_attachment(self, req, callback, model, id, ufile):
1501         context = req.session.eval_context(req.context)
1502         Model = req.session.model('ir.attachment')
1503         try:
1504             out = """<script language="javascript" type="text/javascript">
1505                         var win = window.top.window;
1506                         win.jQuery(win).trigger(%s, %s);
1507                     </script>"""
1508             attachment_id = Model.create({
1509                 'name': ufile.filename,
1510                 'datas': base64.encodestring(ufile.read()),
1511                 'datas_fname': ufile.filename,
1512                 'res_model': model,
1513                 'res_id': int(id)
1514             }, context)
1515             args = {
1516                 'filename': ufile.filename,
1517                 'id':  attachment_id
1518             }
1519         except Exception, e:
1520             args = { 'error': e.message }
1521         return out % (simplejson.dumps(callback), simplejson.dumps(args))
1522
1523 class Action(openerpweb.Controller):
1524     _cp_path = "/web/action"
1525
1526     # For most actions, the type attribute and the model name are the same, but
1527     # there are exceptions. This dict is used to remap action type attributes
1528     # to the "real" model name when they differ.
1529     action_mapping = {
1530         "ir.actions.act_url": "ir.actions.url",
1531     }
1532
1533     @openerpweb.jsonrequest
1534     def load(self, req, action_id, do_not_eval=False):
1535         Actions = req.session.model('ir.actions.actions')
1536         value = False
1537         context = req.session.eval_context(req.context)
1538
1539         try:
1540             action_id = int(action_id)
1541         except ValueError:
1542             try:
1543                 module, xmlid = action_id.split('.', 1)
1544                 model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
1545                 assert model.startswith('ir.actions.')
1546             except Exception:
1547                 action_id = 0   # force failed read
1548
1549         base_action = Actions.read([action_id], ['type'], context)
1550         if base_action:
1551             ctx = {}
1552             action_type = base_action[0]['type']
1553             if action_type == 'ir.actions.report.xml':
1554                 ctx.update({'bin_size': True})
1555             ctx.update(context)
1556             action_model = self.action_mapping.get(action_type, action_type)
1557             action = req.session.model(action_model).read([action_id], False, ctx)
1558             if action:
1559                 value = clean_action(req, action[0], do_not_eval)
1560         return {'result': value}
1561
1562     @openerpweb.jsonrequest
1563     def run(self, req, action_id):
1564         return_action = req.session.model('ir.actions.server').run(
1565             [action_id], req.session.eval_context(req.context))
1566         if return_action:
1567             return clean_action(req, return_action)
1568         else:
1569             return False
1570
1571 class Export(View):
1572     _cp_path = "/web/export"
1573
1574     @openerpweb.jsonrequest
1575     def formats(self, req):
1576         """ Returns all valid export formats
1577
1578         :returns: for each export format, a pair of identifier and printable name
1579         :rtype: [(str, str)]
1580         """
1581         return sorted([
1582             controller.fmt
1583             for path, controller in openerpweb.controllers_path.iteritems()
1584             if path.startswith(self._cp_path)
1585             if hasattr(controller, 'fmt')
1586         ], key=operator.itemgetter("label"))
1587
1588     def fields_get(self, req, model):
1589         Model = req.session.model(model)
1590         fields = Model.fields_get(False, req.session.eval_context(req.context))
1591         return fields
1592
1593     @openerpweb.jsonrequest
1594     def get_fields(self, req, model, prefix='', parent_name= '',
1595                    import_compat=True, parent_field_type=None,
1596                    exclude=None):
1597
1598         if import_compat and parent_field_type == "many2one":
1599             fields = {}
1600         else:
1601             fields = self.fields_get(req, model)
1602
1603         if import_compat:
1604             fields.pop('id', None)
1605         else:
1606             fields['.id'] = fields.pop('id', {'string': 'ID'})
1607
1608         fields_sequence = sorted(fields.iteritems(),
1609             key=lambda field: field[1].get('string', ''))
1610
1611         records = []
1612         for field_name, field in fields_sequence:
1613             if import_compat:
1614                 if exclude and field_name in exclude:
1615                     continue
1616                 if field.get('readonly'):
1617                     # If none of the field's states unsets readonly, skip the field
1618                     if all(dict(attrs).get('readonly', True)
1619                            for attrs in field.get('states', {}).values()):
1620                         continue
1621
1622             id = prefix + (prefix and '/'or '') + field_name
1623             name = parent_name + (parent_name and '/' or '') + field['string']
1624             record = {'id': id, 'string': name,
1625                       'value': id, 'children': False,
1626                       'field_type': field.get('type'),
1627                       'required': field.get('required'),
1628                       'relation_field': field.get('relation_field')}
1629             records.append(record)
1630
1631             if len(name.split('/')) < 3 and 'relation' in field:
1632                 ref = field.pop('relation')
1633                 record['value'] += '/id'
1634                 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1635
1636                 if not import_compat or field['type'] == 'one2many':
1637                     # m2m field in import_compat is childless
1638                     record['children'] = True
1639
1640         return records
1641
1642     @openerpweb.jsonrequest
1643     def namelist(self,req,  model, export_id):
1644         # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1645         export = req.session.model("ir.exports").read([export_id])[0]
1646         export_fields_list = req.session.model("ir.exports.line").read(
1647             export['export_fields'])
1648
1649         fields_data = self.fields_info(
1650             req, model, map(operator.itemgetter('name'), export_fields_list))
1651
1652         return [
1653             {'name': field['name'], 'label': fields_data[field['name']]}
1654             for field in export_fields_list
1655         ]
1656
1657     def fields_info(self, req, model, export_fields):
1658         info = {}
1659         fields = self.fields_get(req, model)
1660
1661         # To make fields retrieval more efficient, fetch all sub-fields of a
1662         # given field at the same time. Because the order in the export list is
1663         # arbitrary, this requires ordering all sub-fields of a given field
1664         # together so they can be fetched at the same time
1665         #
1666         # Works the following way:
1667         # * sort the list of fields to export, the default sorting order will
1668         #   put the field itself (if present, for xmlid) and all of its
1669         #   sub-fields right after it
1670         # * then, group on: the first field of the path (which is the same for
1671         #   a field and for its subfields and the length of splitting on the
1672         #   first '/', which basically means grouping the field on one side and
1673         #   all of the subfields on the other. This way, we have the field (for
1674         #   the xmlid) with length 1, and all of the subfields with the same
1675         #   base but a length "flag" of 2
1676         # * if we have a normal field (length 1), just add it to the info
1677         #   mapping (with its string) as-is
1678         # * otherwise, recursively call fields_info via graft_subfields.
1679         #   all graft_subfields does is take the result of fields_info (on the
1680         #   field's model) and prepend the current base (current field), which
1681         #   rebuilds the whole sub-tree for the field
1682         #
1683         # result: because we're not fetching the fields_get for half the
1684         # database models, fetching a namelist with a dozen fields (including
1685         # relational data) falls from ~6s to ~300ms (on the leads model).
1686         # export lists with no sub-fields (e.g. import_compatible lists with
1687         # no o2m) are even more efficient (from the same 6s to ~170ms, as
1688         # there's a single fields_get to execute)
1689         for (base, length), subfields in itertools.groupby(
1690                 sorted(export_fields),
1691                 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1692             subfields = list(subfields)
1693             if length == 2:
1694                 # subfields is a seq of $base/*rest, and not loaded yet
1695                 info.update(self.graft_subfields(
1696                     req, fields[base]['relation'], base, fields[base]['string'],
1697                     subfields
1698                 ))
1699             else:
1700                 info[base] = fields[base]['string']
1701
1702         return info
1703
1704     def graft_subfields(self, req, model, prefix, prefix_string, fields):
1705         export_fields = [field.split('/', 1)[1] for field in fields]
1706         return (
1707             (prefix + '/' + k, prefix_string + '/' + v)
1708             for k, v in self.fields_info(req, model, export_fields).iteritems())
1709
1710     #noinspection PyPropertyDefinition
1711     @property
1712     def content_type(self):
1713         """ Provides the format's content type """
1714         raise NotImplementedError()
1715
1716     def filename(self, base):
1717         """ Creates a valid filename for the format (with extension) from the
1718          provided base name (exension-less)
1719         """
1720         raise NotImplementedError()
1721
1722     def from_data(self, fields, rows):
1723         """ Conversion method from OpenERP's export data to whatever the
1724         current export class outputs
1725
1726         :params list fields: a list of fields to export
1727         :params list rows: a list of records to export
1728         :returns:
1729         :rtype: bytes
1730         """
1731         raise NotImplementedError()
1732
1733     @openerpweb.httprequest
1734     def index(self, req, data, token):
1735         model, fields, ids, domain, import_compat = \
1736             operator.itemgetter('model', 'fields', 'ids', 'domain',
1737                                 'import_compat')(
1738                 simplejson.loads(data))
1739
1740         context = req.session.eval_context(req.context)
1741         Model = req.session.model(model)
1742         ids = ids or Model.search(domain, 0, False, False, context)
1743
1744         field_names = map(operator.itemgetter('name'), fields)
1745         import_data = Model.export_data(ids, field_names, context).get('datas',[])
1746
1747         if import_compat:
1748             columns_headers = field_names
1749         else:
1750             columns_headers = [val['label'].strip() for val in fields]
1751
1752
1753         return req.make_response(self.from_data(columns_headers, import_data),
1754             headers=[('Content-Disposition', 'attachment; filename="%s"' % self.filename(model)),
1755                      ('Content-Type', self.content_type)],
1756             cookies={'fileToken': int(token)})
1757
1758 class CSVExport(Export):
1759     _cp_path = '/web/export/csv'
1760     fmt = {'tag': 'csv', 'label': 'CSV'}
1761
1762     @property
1763     def content_type(self):
1764         return 'text/csv;charset=utf8'
1765
1766     def filename(self, base):
1767         return base + '.csv'
1768
1769     def from_data(self, fields, rows):
1770         fp = StringIO()
1771         writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1772
1773         writer.writerow([name.encode('utf-8') for name in fields])
1774
1775         for data in rows:
1776             row = []
1777             for d in data:
1778                 if isinstance(d, basestring):
1779                     d = d.replace('\n',' ').replace('\t',' ')
1780                     try:
1781                         d = d.encode('utf-8')
1782                     except UnicodeError:
1783                         pass
1784                 if d is False: d = None
1785                 row.append(d)
1786             writer.writerow(row)
1787
1788         fp.seek(0)
1789         data = fp.read()
1790         fp.close()
1791         return data
1792
1793 class ExcelExport(Export):
1794     _cp_path = '/web/export/xls'
1795     fmt = {
1796         'tag': 'xls',
1797         'label': 'Excel',
1798         'error': None if xlwt else "XLWT required"
1799     }
1800
1801     @property
1802     def content_type(self):
1803         return 'application/vnd.ms-excel'
1804
1805     def filename(self, base):
1806         return base + '.xls'
1807
1808     def from_data(self, fields, rows):
1809         workbook = xlwt.Workbook()
1810         worksheet = workbook.add_sheet('Sheet 1')
1811
1812         for i, fieldname in enumerate(fields):
1813             worksheet.write(0, i, fieldname)
1814             worksheet.col(i).width = 8000 # around 220 pixels
1815
1816         style = xlwt.easyxf('align: wrap yes')
1817
1818         for row_index, row in enumerate(rows):
1819             for cell_index, cell_value in enumerate(row):
1820                 if isinstance(cell_value, basestring):
1821                     cell_value = re.sub("\r", " ", cell_value)
1822                 if cell_value is False: cell_value = None
1823                 worksheet.write(row_index + 1, cell_index, cell_value, style)
1824
1825         fp = StringIO()
1826         workbook.save(fp)
1827         fp.seek(0)
1828         data = fp.read()
1829         fp.close()
1830         return data
1831
1832 class Reports(View):
1833     _cp_path = "/web/report"
1834     POLLING_DELAY = 0.25
1835     TYPES_MAPPING = {
1836         'doc': 'application/vnd.ms-word',
1837         'html': 'text/html',
1838         'odt': 'application/vnd.oasis.opendocument.text',
1839         'pdf': 'application/pdf',
1840         'sxw': 'application/vnd.sun.xml.writer',
1841         'xls': 'application/vnd.ms-excel',
1842     }
1843
1844     @openerpweb.httprequest
1845     def index(self, req, action, token):
1846         action = simplejson.loads(action)
1847
1848         report_srv = req.session.proxy("report")
1849         context = req.session.eval_context(
1850             common.nonliterals.CompoundContext(
1851                 req.context or {}, action[ "context"]))
1852
1853         report_data = {}
1854         report_ids = context["active_ids"]
1855         if 'report_type' in action:
1856             report_data['report_type'] = action['report_type']
1857         if 'datas' in action:
1858             if 'ids' in action['datas']:
1859                 report_ids = action['datas'].pop('ids')
1860             report_data.update(action['datas'])
1861
1862         report_id = report_srv.report(
1863             req.session._db, req.session._uid, req.session._password,
1864             action["report_name"], report_ids,
1865             report_data, context)
1866
1867         report_struct = None
1868         while True:
1869             report_struct = report_srv.report_get(
1870                 req.session._db, req.session._uid, req.session._password, report_id)
1871             if report_struct["state"]:
1872                 break
1873
1874             time.sleep(self.POLLING_DELAY)
1875
1876         report = base64.b64decode(report_struct['result'])
1877         if report_struct.get('code') == 'zlib':
1878             report = zlib.decompress(report)
1879         report_mimetype = self.TYPES_MAPPING.get(
1880             report_struct['format'], 'octet-stream')
1881         file_name = None
1882         if 'name' not in action:
1883             reports = req.session.model('ir.actions.report.xml')
1884             res_id = reports.search([('report_name', '=', action['report_name']),],
1885                                     0, False, False, context)
1886             if len(res_id) > 0:
1887                 file_name = reports.read(res_id[0], ['name'], context)['name']
1888             else:
1889                 file_name = action['report_name']
1890
1891         return req.make_response(report,
1892              headers=[
1893                  # maybe we should take of what characters can appear in a file name?
1894                  ('Content-Disposition', 'attachment; filename="%s.%s"' % (file_name, report_struct['format'])),
1895                  ('Content-Type', report_mimetype),
1896                  ('Content-Length', len(report))],
1897              cookies={'fileToken': int(token)})
1898
1899 class Import(View):
1900     _cp_path = "/web/import"
1901
1902     def fields_get(self, req, model):
1903         Model = req.session.model(model)
1904         fields = Model.fields_get(False, req.session.eval_context(req.context))
1905         return fields
1906
1907     @openerpweb.httprequest
1908     def detect_data(self, req, csvfile, csvsep=',', csvdel='"', csvcode='utf-8', jsonp='callback'):
1909         try:
1910             data = list(csv.reader(
1911                 csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
1912         except csv.Error, e:
1913             csvfile.seek(0)
1914             return '<script>window.top.%s(%s);</script>' % (
1915                 jsonp, simplejson.dumps({'error': {
1916                     'message': 'Error parsing CSV file: %s' % e,
1917                     # decodes each byte to a unicode character, which may or
1918                     # may not be printable, but decoding will succeed.
1919                     # Otherwise simplejson will try to decode the `str` using
1920                     # utf-8, which is very likely to blow up on characters out
1921                     # of the ascii range (in range [128, 256))
1922                     'preview': csvfile.read(200).decode('iso-8859-1')}}))
1923
1924         try:
1925             return '<script>window.top.%s(%s);</script>' % (
1926                 jsonp, simplejson.dumps(
1927                     {'records': data[:10]}, encoding=csvcode))
1928         except UnicodeDecodeError:
1929             return '<script>window.top.%s(%s);</script>' % (
1930                 jsonp, simplejson.dumps({
1931                     'message': u"Failed to decode CSV file using encoding %s, "
1932                                u"try switching to a different encoding" % csvcode
1933                 }))
1934
1935     @openerpweb.httprequest
1936     def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
1937                     meta):
1938         modle_obj = req.session.model(model)
1939         skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')(
1940             simplejson.loads(meta))
1941
1942         error = None
1943         if not (csvdel and len(csvdel) == 1):
1944             error = u"The CSV delimiter must be a single character"
1945
1946         if not indices and fields:
1947             error = u"You must select at least one field to import"
1948
1949         if error:
1950             return '<script>window.top.%s(%s);</script>' % (
1951                 jsonp, simplejson.dumps({'error': {'message': error}}))
1952
1953         # skip ignored records (@skip parameter)
1954         # then skip empty lines (not valid csv)
1955         # nb: should these operations be reverted?
1956         rows_to_import = itertools.ifilter(
1957             None,
1958             itertools.islice(
1959                 csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
1960                 skip, None))
1961
1962         # if only one index, itemgetter will return an atom rather than a tuple
1963         if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
1964         else: mapper = operator.itemgetter(*indices)
1965
1966         data = None
1967         error = None
1968         try:
1969             # decode each data row
1970             data = [
1971                 [record.decode(csvcode) for record in row]
1972                 for row in itertools.imap(mapper, rows_to_import)
1973                 # don't insert completely empty rows (can happen due to fields
1974                 # filtering in case of e.g. o2m content rows)
1975                 if any(row)
1976             ]
1977         except UnicodeDecodeError:
1978             error = u"Failed to decode CSV file using encoding %s" % csvcode
1979         except csv.Error, e:
1980             error = u"Could not process CSV file: %s" % e
1981
1982         # If the file contains nothing,
1983         if not data:
1984             error = u"File to import is empty"
1985         if error:
1986             return '<script>window.top.%s(%s);</script>' % (
1987                 jsonp, simplejson.dumps({'error': {'message': error}}))
1988
1989         try:
1990             (code, record, message, _nope) = modle_obj.import_data(
1991                 fields, data, 'init', '', False,
1992                 req.session.eval_context(req.context))
1993         except xmlrpclib.Fault, e:
1994             error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
1995             return '<script>window.top.%s(%s);</script>' % (
1996                 jsonp, simplejson.dumps({'error':error}))
1997
1998         if code != -1:
1999             return '<script>window.top.%s(%s);</script>' % (
2000                 jsonp, simplejson.dumps({'success':True}))
2001
2002         msg = u"Error during import: %s\n\nTrying to import record %r" % (
2003             message, record)
2004         return '<script>window.top.%s(%s);</script>' % (
2005             jsonp, simplejson.dumps({'error': {'message':msg}}))
2006
2007 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: