[DOC] cmdline: database-related parameters
[odoo/odoo.git] / addons / web / controllers / main.py
1 # -*- coding: utf-8 -*-
2
3 import ast
4 import base64
5 import csv
6 import functools
7 import glob
8 import itertools
9 import jinja2
10 import logging
11 import operator
12 import datetime
13 import hashlib
14 import os
15 import re
16 import simplejson
17 import sys
18 import time
19 import urllib2
20 import zlib
21 from xml.etree import ElementTree
22 from cStringIO import StringIO
23
24 import babel.messages.pofile
25 import werkzeug.utils
26 import werkzeug.wrappers
27 try:
28     import xlwt
29 except ImportError:
30     xlwt = None
31
32 import openerp
33 import openerp.modules.registry
34 from openerp.addons.base.ir.ir_qweb import AssetsBundle, QWebTemplateNotFound
35 from openerp.modules import get_module_resource
36 from openerp.tools import topological_sort
37 from openerp.tools.translate import _
38 from openerp import http
39
40 from openerp.http import request, serialize_exception as _serialize_exception
41
42 _logger = logging.getLogger(__name__)
43
44 if hasattr(sys, 'frozen'):
45     # When running on compiled windows binary, we don't have access to package loader.
46     path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'views'))
47     loader = jinja2.FileSystemLoader(path)
48 else:
49     loader = jinja2.PackageLoader('openerp.addons.web', "views")
50
51 env = jinja2.Environment(loader=loader, autoescape=True)
52 env.filters["json"] = simplejson.dumps
53
54 # 1 week cache for asset bundles as advised by Google Page Speed
55 BUNDLE_MAXAGE = 60 * 60 * 24 * 7
56
57 #----------------------------------------------------------
58 # OpenERP Web helpers
59 #----------------------------------------------------------
60
61 db_list = http.db_list
62
63 db_monodb = http.db_monodb
64
65 def serialize_exception(f):
66     @functools.wraps(f)
67     def wrap(*args, **kwargs):
68         try:
69             return f(*args, **kwargs)
70         except Exception, e:
71             _logger.exception("An exception occured during an http request")
72             se = _serialize_exception(e)
73             error = {
74                 'code': 200,
75                 'message': "Odoo Server Error",
76                 'data': se
77             }
78             return werkzeug.exceptions.InternalServerError(simplejson.dumps(error))
79     return wrap
80
81 def redirect_with_hash(*args, **kw):
82     """
83         .. deprecated:: 8.0
84
85         Use the ``http.redirect_with_hash()`` function instead.
86     """
87     return http.redirect_with_hash(*args, **kw)
88
89 def abort_and_redirect(url):
90     r = request.httprequest
91     response = werkzeug.utils.redirect(url, 302)
92     response = r.app.get_response(r, response, explicit_session=False)
93     werkzeug.exceptions.abort(response)
94
95 def ensure_db(redirect='/web/database/selector'):
96     # This helper should be used in web client auth="none" routes
97     # if those routes needs a db to work with.
98     # If the heuristics does not find any database, then the users will be
99     # redirected to db selector or any url specified by `redirect` argument.
100     # If the db is taken out of a query parameter, it will be checked against
101     # `http.db_filter()` in order to ensure it's legit and thus avoid db
102     # forgering that could lead to xss attacks.
103     db = request.params.get('db')
104
105     # Ensure db is legit
106     if db and db not in http.db_filter([db]):
107         db = None
108
109     if db and not request.session.db:
110         # User asked a specific database on a new session.
111         # That mean the nodb router has been used to find the route
112         # Depending on installed module in the database, the rendering of the page
113         # may depend on data injected by the database route dispatcher.
114         # Thus, we redirect the user to the same page but with the session cookie set.
115         # This will force using the database route dispatcher...
116         r = request.httprequest
117         url_redirect = r.base_url
118         if r.query_string:
119             # Can't use werkzeug.wrappers.BaseRequest.url with encoded hashes:
120             # https://github.com/amigrave/werkzeug/commit/b4a62433f2f7678c234cdcac6247a869f90a7eb7
121             url_redirect += '?' + r.query_string
122         response = werkzeug.utils.redirect(url_redirect, 302)
123         request.session.db = db
124         abort_and_redirect(url_redirect)
125
126     # if db not provided, use the session one
127     if not db and request.session.db and http.db_filter([request.session.db]):
128         db = request.session.db
129
130     # if no database provided and no database in session, use monodb
131     if not db:
132         db = db_monodb(request.httprequest)
133
134     # if no db can be found til here, send to the database selector
135     # the database selector will redirect to database manager if needed
136     if not db:
137         werkzeug.exceptions.abort(werkzeug.utils.redirect(redirect, 303))
138
139     # always switch the session to the computed db
140     if db != request.session.db:
141         request.session.logout()
142         abort_and_redirect(request.httprequest.url)
143
144     request.session.db = db
145
146 def module_installed():
147     # Candidates module the current heuristic is the /static dir
148     loadable = http.addons_manifest.keys()
149     modules = {}
150
151     # Retrieve database installed modules
152     # TODO The following code should move to ir.module.module.list_installed_modules()
153     Modules = request.session.model('ir.module.module')
154     domain = [('state','=','installed'), ('name','in', loadable)]
155     for module in Modules.search_read(domain, ['name', 'dependencies_id']):
156         modules[module['name']] = []
157         deps = module.get('dependencies_id')
158         if deps:
159             deps_read = request.session.model('ir.module.module.dependency').read(deps, ['name'])
160             dependencies = [i['name'] for i in deps_read]
161             modules[module['name']] = dependencies
162
163     sorted_modules = topological_sort(modules)
164     return sorted_modules
165
166 def module_installed_bypass_session(dbname):
167     loadable = http.addons_manifest.keys()
168     modules = {}
169     try:
170         registry = openerp.modules.registry.RegistryManager.get(dbname)
171         with registry.cursor() as cr:
172             m = registry.get('ir.module.module')
173             # TODO The following code should move to ir.module.module.list_installed_modules()
174             domain = [('state','=','installed'), ('name','in', loadable)]
175             ids = m.search(cr, 1, [('state','=','installed'), ('name','in', loadable)])
176             for module in m.read(cr, 1, ids, ['name', 'dependencies_id']):
177                 modules[module['name']] = []
178                 deps = module.get('dependencies_id')
179                 if deps:
180                     deps_read = registry.get('ir.module.module.dependency').read(cr, 1, deps, ['name'])
181                     dependencies = [i['name'] for i in deps_read]
182                     modules[module['name']] = dependencies
183     except Exception,e:
184         pass
185     sorted_modules = topological_sort(modules)
186     return sorted_modules
187
188 def module_boot(db=None):
189     server_wide_modules = openerp.conf.server_wide_modules or ['web']
190     serverside = []
191     dbside = []
192     for i in server_wide_modules:
193         if i in http.addons_manifest:
194             serverside.append(i)
195     monodb = db or db_monodb()
196     if monodb:
197         dbside = module_installed_bypass_session(monodb)
198         dbside = [i for i in dbside if i not in serverside]
199     addons = serverside + dbside
200     return addons
201
202 def concat_xml(file_list):
203     """Concatenate xml files
204
205     :param list(str) file_list: list of files to check
206     :returns: (concatenation_result, checksum)
207     :rtype: (str, str)
208     """
209     checksum = hashlib.new('sha1')
210     if not file_list:
211         return '', checksum.hexdigest()
212
213     root = None
214     for fname in file_list:
215         with open(fname, 'rb') as fp:
216             contents = fp.read()
217             checksum.update(contents)
218             fp.seek(0)
219             xml = ElementTree.parse(fp).getroot()
220
221         if root is None:
222             root = ElementTree.Element(xml.tag)
223         #elif root.tag != xml.tag:
224         #    raise ValueError("Root tags missmatch: %r != %r" % (root.tag, xml.tag))
225
226         for child in xml.getchildren():
227             root.append(child)
228     return ElementTree.tostring(root, 'utf-8'), checksum.hexdigest()
229
230 def fs2web(path):
231     """convert FS path into web path"""
232     return '/'.join(path.split(os.path.sep))
233
234 def manifest_glob(extension, addons=None, db=None, include_remotes=False):
235     if addons is None:
236         addons = module_boot(db=db)
237     else:
238         addons = addons.split(',')
239     r = []
240     for addon in addons:
241         manifest = http.addons_manifest.get(addon, None)
242         if not manifest:
243             continue
244         # ensure does not ends with /
245         addons_path = os.path.join(manifest['addons_path'], '')[:-1]
246         globlist = manifest.get(extension, [])
247         for pattern in globlist:
248             if pattern.startswith(('http://', 'https://', '//')):
249                 if include_remotes:
250                     r.append((None, pattern))
251             else:
252                 for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
253                     r.append((path, fs2web(path[len(addons_path):])))
254     return r
255
256 def manifest_list(extension, mods=None, db=None, debug=None):
257     """ list ressources to load specifying either:
258     mods: a comma separated string listing modules
259     db: a database name (return all installed modules in that database)
260     """
261     if debug is not None:
262         _logger.warning("openerp.addons.web.main.manifest_list(): debug parameter is deprecated")
263     files = manifest_glob(extension, addons=mods, db=db, include_remotes=True)
264     return [wp for _fp, wp in files]
265
266 def get_last_modified(files):
267     """ Returns the modification time of the most recently modified
268     file provided
269
270     :param list(str) files: names of files to check
271     :return: most recent modification time amongst the fileset
272     :rtype: datetime.datetime
273     """
274     files = list(files)
275     if files:
276         return max(datetime.datetime.fromtimestamp(os.path.getmtime(f))
277                    for f in files)
278     return datetime.datetime(1970, 1, 1)
279
280 def make_conditional(response, last_modified=None, etag=None, max_age=0):
281     """ Makes the provided response conditional based upon the request,
282     and mandates revalidation from clients
283
284     Uses Werkzeug's own :meth:`ETagResponseMixin.make_conditional`, after
285     setting ``last_modified`` and ``etag`` correctly on the response object
286
287     :param response: Werkzeug response
288     :type response: werkzeug.wrappers.Response
289     :param datetime.datetime last_modified: last modification date of the response content
290     :param str etag: some sort of checksum of the content (deep etag)
291     :return: the response object provided
292     :rtype: werkzeug.wrappers.Response
293     """
294     response.cache_control.must_revalidate = True
295     response.cache_control.max_age = max_age
296     if last_modified:
297         response.last_modified = last_modified
298     if etag:
299         response.set_etag(etag)
300     return response.make_conditional(request.httprequest)
301
302 def login_and_redirect(db, login, key, redirect_url='/web'):
303     request.session.authenticate(db, login, key)
304     return set_cookie_and_redirect(redirect_url)
305
306 def set_cookie_and_redirect(redirect_url):
307     redirect = werkzeug.utils.redirect(redirect_url, 303)
308     redirect.autocorrect_location_header = False
309     return redirect
310
311 def login_redirect():
312     url = '/web/login?'
313     if request.debug:
314         url += 'debug&'
315     return """<html><head><script>
316         window.location = '%sredirect=' + encodeURIComponent(window.location);
317     </script></head></html>
318     """ % (url,)
319
320 def load_actions_from_ir_values(key, key2, models, meta):
321     Values = request.session.model('ir.values')
322     actions = Values.get(key, key2, models, meta, request.context)
323
324     return [(id, name, clean_action(action))
325             for id, name, action in actions]
326
327 def clean_action(action):
328     action.setdefault('flags', {})
329     action_type = action.setdefault('type', 'ir.actions.act_window_close')
330     if action_type == 'ir.actions.act_window':
331         return fix_view_modes(action)
332     return action
333
334 # I think generate_views,fix_view_modes should go into js ActionManager
335 def generate_views(action):
336     """
337     While the server generates a sequence called "views" computing dependencies
338     between a bunch of stuff for views coming directly from the database
339     (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
340     to return custom view dictionaries generated on the fly.
341
342     In that case, there is no ``views`` key available on the action.
343
344     Since the web client relies on ``action['views']``, generate it here from
345     ``view_mode`` and ``view_id``.
346
347     Currently handles two different cases:
348
349     * no view_id, multiple view_mode
350     * single view_id, single view_mode
351
352     :param dict action: action descriptor dictionary to generate a views key for
353     """
354     view_id = action.get('view_id') or False
355     if isinstance(view_id, (list, tuple)):
356         view_id = view_id[0]
357
358     # providing at least one view mode is a requirement, not an option
359     view_modes = action['view_mode'].split(',')
360
361     if len(view_modes) > 1:
362         if view_id:
363             raise ValueError('Non-db action dictionaries should provide '
364                              'either multiple view modes or a single view '
365                              'mode and an optional view id.\n\n Got view '
366                              'modes %r and view id %r for action %r' % (
367                 view_modes, view_id, action))
368         action['views'] = [(False, mode) for mode in view_modes]
369         return
370     action['views'] = [(view_id, view_modes[0])]
371
372 def fix_view_modes(action):
373     """ For historical reasons, OpenERP has weird dealings in relation to
374     view_mode and the view_type attribute (on window actions):
375
376     * one of the view modes is ``tree``, which stands for both list views
377       and tree views
378     * the choice is made by checking ``view_type``, which is either
379       ``form`` for a list view or ``tree`` for an actual tree view
380
381     This methods simply folds the view_type into view_mode by adding a
382     new view mode ``list`` which is the result of the ``tree`` view_mode
383     in conjunction with the ``form`` view_type.
384
385     TODO: this should go into the doc, some kind of "peculiarities" section
386
387     :param dict action: an action descriptor
388     :returns: nothing, the action is modified in place
389     """
390     if not action.get('views'):
391         generate_views(action)
392
393     if action.pop('view_type', 'form') != 'form':
394         return action
395
396     if 'view_mode' in action:
397         action['view_mode'] = ','.join(
398             mode if mode != 'tree' else 'list'
399             for mode in action['view_mode'].split(','))
400     action['views'] = [
401         [id, mode if mode != 'tree' else 'list']
402         for id, mode in action['views']
403     ]
404
405     return action
406
407 def _local_web_translations(trans_file):
408     messages = []
409     try:
410         with open(trans_file) as t_file:
411             po = babel.messages.pofile.read_po(t_file)
412     except Exception:
413         return
414     for x in po:
415         if x.id and x.string and "openerp-web" in x.auto_comments:
416             messages.append({'id': x.id, 'string': x.string})
417     return messages
418
419 def xml2json_from_elementtree(el, preserve_whitespaces=False):
420     """ xml2json-direct
421     Simple and straightforward XML-to-JSON converter in Python
422     New BSD Licensed
423     http://code.google.com/p/xml2json-direct/
424     """
425     res = {}
426     if el.tag[0] == "{":
427         ns, name = el.tag.rsplit("}", 1)
428         res["tag"] = name
429         res["namespace"] = ns[1:]
430     else:
431         res["tag"] = el.tag
432     res["attrs"] = {}
433     for k, v in el.items():
434         res["attrs"][k] = v
435     kids = []
436     if el.text and (preserve_whitespaces or el.text.strip() != ''):
437         kids.append(el.text)
438     for kid in el:
439         kids.append(xml2json_from_elementtree(kid, preserve_whitespaces))
440         if kid.tail and (preserve_whitespaces or kid.tail.strip() != ''):
441             kids.append(kid.tail)
442     res["children"] = kids
443     return res
444
445 def content_disposition(filename):
446     filename = filename.encode('utf8')
447     escaped = urllib2.quote(filename)
448     browser = request.httprequest.user_agent.browser
449     version = int((request.httprequest.user_agent.version or '0').split('.')[0])
450     if browser == 'msie' and version < 9:
451         return "attachment; filename=%s" % escaped
452     elif browser == 'safari':
453         return "attachment; filename=%s" % filename
454     else:
455         return "attachment; filename*=UTF-8''%s" % escaped
456
457
458 #----------------------------------------------------------
459 # OpenERP Web web Controllers
460 #----------------------------------------------------------
461 class Home(http.Controller):
462
463     @http.route('/', type='http', auth="none")
464     def index(self, s_action=None, db=None, **kw):
465         return http.local_redirect('/web', query=request.params, keep_hash=True)
466
467     @http.route('/web', type='http', auth="none")
468     def web_client(self, s_action=None, **kw):
469         ensure_db()
470
471         if request.session.uid:
472             if kw.get('redirect'):
473                 return werkzeug.utils.redirect(kw.get('redirect'), 303)
474             if not request.uid:
475                 request.uid = request.session.uid
476
477             menu_data = request.registry['ir.ui.menu'].load_menus(request.cr, request.uid, context=request.context)
478             return request.render('web.webclient_bootstrap', qcontext={'menu_data': menu_data})
479         else:
480             return login_redirect()
481
482     @http.route('/web/login', type='http', auth="none")
483     def web_login(self, redirect=None, **kw):
484         ensure_db()
485
486         if request.httprequest.method == 'GET' and redirect and request.session.uid:
487             return http.redirect_with_hash(redirect)
488
489         if not request.uid:
490             request.uid = openerp.SUPERUSER_ID
491
492         values = request.params.copy()
493         if not redirect:
494             redirect = '/web?' + request.httprequest.query_string
495         values['redirect'] = redirect
496
497         try:
498             values['databases'] = http.db_list()
499         except openerp.exceptions.AccessDenied:
500             values['databases'] = None
501
502         if request.httprequest.method == 'POST':
503             old_uid = request.uid
504             uid = request.session.authenticate(request.session.db, request.params['login'], request.params['password'])
505             if uid is not False:
506                 return http.redirect_with_hash(redirect)
507             request.uid = old_uid
508             values['error'] = "Wrong login/password"
509         return request.render('web.login', values)
510
511     @http.route('/login', type='http', auth="none")
512     def login(self, db, login, key, redirect="/web", **kw):
513         if not http.db_filter([db]):
514             return werkzeug.utils.redirect('/', 303)
515         return login_and_redirect(db, login, key, redirect_url=redirect)
516
517     @http.route([
518         '/web/js/<xmlid>',
519         '/web/js/<xmlid>/<version>',
520     ], type='http', auth='public')
521     def js_bundle(self, xmlid, version=None, **kw):
522         try:
523             bundle = AssetsBundle(xmlid)
524         except QWebTemplateNotFound:
525             return request.not_found()
526
527         response = request.make_response(bundle.js(), [('Content-Type', 'application/javascript')])
528         return make_conditional(response, bundle.last_modified, max_age=BUNDLE_MAXAGE)
529
530     @http.route([
531         '/web/css/<xmlid>',
532         '/web/css/<xmlid>/<version>',
533     ], type='http', auth='public')
534     def css_bundle(self, xmlid, version=None, **kw):
535         try:
536             bundle = AssetsBundle(xmlid)
537         except QWebTemplateNotFound:
538             return request.not_found()
539
540         response = request.make_response(bundle.css(), [('Content-Type', 'text/css')])
541         return make_conditional(response, bundle.last_modified, max_age=BUNDLE_MAXAGE)
542
543 class WebClient(http.Controller):
544
545     @http.route('/web/webclient/csslist', type='json', auth="none")
546     def csslist(self, mods=None):
547         return manifest_list('css', mods=mods)
548
549     @http.route('/web/webclient/jslist', type='json', auth="none")
550     def jslist(self, mods=None):
551         return manifest_list('js', mods=mods)
552
553     @http.route('/web/webclient/qweb', type='http', auth="none")
554     def qweb(self, mods=None, db=None):
555         files = [f[0] for f in manifest_glob('qweb', addons=mods, db=db)]
556         last_modified = get_last_modified(files)
557         if request.httprequest.if_modified_since and request.httprequest.if_modified_since >= last_modified:
558             return werkzeug.wrappers.Response(status=304)
559
560         content, checksum = concat_xml(files)
561
562         return make_conditional(
563             request.make_response(content, [('Content-Type', 'text/xml')]),
564             last_modified, checksum)
565
566     @http.route('/web/webclient/bootstrap_translations', type='json', auth="none")
567     def bootstrap_translations(self, mods):
568         """ Load local translations from *.po files, as a temporary solution
569             until we have established a valid session. This is meant only
570             for translating the login page and db management chrome, using
571             the browser's language. """
572         # For performance reasons we only load a single translation, so for
573         # sub-languages (that should only be partially translated) we load the
574         # main language PO instead - that should be enough for the login screen.
575         lang = request.lang.split('_')[0]
576
577         translations_per_module = {}
578         for addon_name in mods:
579             if http.addons_manifest[addon_name].get('bootstrap'):
580                 addons_path = http.addons_manifest[addon_name]['addons_path']
581                 f_name = os.path.join(addons_path, addon_name, "i18n", lang + ".po")
582                 if not os.path.exists(f_name):
583                     continue
584                 translations_per_module[addon_name] = {'messages': _local_web_translations(f_name)}
585
586         return {"modules": translations_per_module,
587                 "lang_parameters": None}
588
589     @http.route('/web/webclient/translations', type='json', auth="none")
590     def translations(self, mods=None, lang=None):
591         request.disable_db = False
592         uid = openerp.SUPERUSER_ID
593         if mods is None:
594             m = request.registry.get('ir.module.module')
595             mods = [x['name'] for x in m.search_read(request.cr, uid,
596                 [('state','=','installed')], ['name'])]
597         if lang is None:
598             lang = request.context["lang"]
599         res_lang = request.registry.get('res.lang')
600         ids = res_lang.search(request.cr, uid, [("code", "=", lang)])
601         lang_params = None
602         if ids:
603             lang_params = res_lang.read(request.cr, uid, ids[0], ["direction", "date_format", "time_format",
604                                                 "grouping", "decimal_point", "thousands_sep"])
605
606         # Regional languages (ll_CC) must inherit/override their parent lang (ll), but this is
607         # done server-side when the language is loaded, so we only need to load the user's lang.
608         ir_translation = request.registry.get('ir.translation')
609         translations_per_module = {}
610         messages = ir_translation.search_read(request.cr, uid, [('module','in',mods),('lang','=',lang),
611                                                ('comments','like','openerp-web'),('value','!=',False),
612                                                ('value','!=','')],
613                                               ['module','src','value','lang'], order='module')
614         for mod, msg_group in itertools.groupby(messages, key=operator.itemgetter('module')):
615             translations_per_module.setdefault(mod,{'messages':[]})
616             translations_per_module[mod]['messages'].extend({'id': m['src'],
617                                                              'string': m['value']} \
618                                                                 for m in msg_group)
619         return {"modules": translations_per_module,
620                 "lang_parameters": lang_params}
621
622     @http.route('/web/webclient/version_info', type='json', auth="none")
623     def version_info(self):
624         return openerp.service.common.exp_version()
625
626     @http.route('/web/tests', type='http', auth="none")
627     def index(self, mod=None, **kwargs):
628         return request.render('web.qunit_suite')
629
630 class Proxy(http.Controller):
631
632     @http.route('/web/proxy/load', type='json', auth="none")
633     def load(self, path):
634         """ Proxies an HTTP request through a JSON request.
635
636         It is strongly recommended to not request binary files through this,
637         as the result will be a binary data blob as well.
638
639         :param path: actual request path
640         :return: file content
641         """
642         from werkzeug.test import Client
643         from werkzeug.wrappers import BaseResponse
644
645         base_url = request.httprequest.base_url
646         return Client(request.httprequest.app, BaseResponse).get(path, base_url=base_url).data
647
648 class Database(http.Controller):
649
650     @http.route('/web/database/selector', type='http', auth="none")
651     def selector(self, **kw):
652         try:
653             dbs = http.db_list()
654             if not dbs:
655                 return http.local_redirect('/web/database/manager')
656         except openerp.exceptions.AccessDenied:
657             dbs = False
658         return env.get_template("database_selector.html").render({
659             'databases': dbs,
660             'debug': request.debug,
661         })
662
663     @http.route('/web/database/manager', type='http', auth="none")
664     def manager(self, **kw):
665         # TODO: migrate the webclient's database manager to server side views
666         request.session.logout()
667         return env.get_template("database_manager.html").render({
668             'modules': simplejson.dumps(module_boot()),
669         })
670
671     @http.route('/web/database/get_list', type='json', auth="none")
672     def get_list(self):
673         # TODO change js to avoid calling this method if in monodb mode
674         try:
675             return http.db_list()
676         except openerp.exceptions.AccessDenied:
677             monodb = db_monodb()
678             if monodb:
679                 return [monodb]
680             raise
681
682     @http.route('/web/database/create', type='json', auth="none")
683     def create(self, fields):
684         params = dict(map(operator.itemgetter('name', 'value'), fields))
685         db_created = request.session.proxy("db").create_database(
686             params['super_admin_pwd'],
687             params['db_name'],
688             bool(params.get('demo_data')),
689             params['db_lang'],
690             params['create_admin_pwd'])
691         if db_created:
692             request.session.authenticate(params['db_name'], 'admin', params['create_admin_pwd'])
693         return db_created
694
695     @http.route('/web/database/duplicate', type='json', auth="none")
696     def duplicate(self, fields):
697         params = dict(map(operator.itemgetter('name', 'value'), fields))
698         duplicate_attrs = (
699             params['super_admin_pwd'],
700             params['db_original_name'],
701             params['db_name'],
702         )
703
704         return request.session.proxy("db").duplicate_database(*duplicate_attrs)
705
706     @http.route('/web/database/drop', type='json', auth="none")
707     def drop(self, fields):
708         password, db = operator.itemgetter(
709             'drop_pwd', 'drop_db')(
710                 dict(map(operator.itemgetter('name', 'value'), fields)))
711
712         try:
713             if request.session.proxy("db").drop(password, db):
714                 return True
715             else:
716                 return False
717         except openerp.exceptions.AccessDenied:
718             return {'error': 'AccessDenied', 'title': 'Drop Database'}
719         except Exception:
720             return {'error': _('Could not drop database !'), 'title': _('Drop Database')}
721
722     @http.route('/web/database/backup', type='http', auth="none")
723     def backup(self, backup_db, backup_pwd, token):
724         try:
725             db_dump = base64.b64decode(
726                 request.session.proxy("db").dump(backup_pwd, backup_db))
727             filename = "%(db)s_%(timestamp)s.dump" % {
728                 'db': backup_db,
729                 'timestamp': datetime.datetime.utcnow().strftime(
730                     "%Y-%m-%d_%H-%M-%SZ")
731             }
732             return request.make_response(db_dump,
733                [('Content-Type', 'application/octet-stream; charset=binary'),
734                ('Content-Disposition', content_disposition(filename))],
735                {'fileToken': token}
736             )
737         except Exception, e:
738             return simplejson.dumps([[],[{'error': openerp.tools.ustr(e), 'title': _('Backup Database')}]])
739
740     @http.route('/web/database/restore', type='http', auth="none")
741     def restore(self, db_file, restore_pwd, new_db, mode):
742         try:
743             copy = mode == 'copy'
744             data = base64.b64encode(db_file.read())
745             request.session.proxy("db").restore(restore_pwd, new_db, data, copy)
746             return ''
747         except openerp.exceptions.AccessDenied, e:
748             raise Exception("AccessDenied")
749
750     @http.route('/web/database/change_password', type='json', auth="none")
751     def change_password(self, fields):
752         old_password, new_password = operator.itemgetter(
753             'old_pwd', 'new_pwd')(
754                 dict(map(operator.itemgetter('name', 'value'), fields)))
755         try:
756             return request.session.proxy("db").change_admin_password(old_password, new_password)
757         except openerp.exceptions.AccessDenied:
758             return {'error': 'AccessDenied', 'title': _('Change Password')}
759         except Exception:
760             return {'error': _('Error, password not changed !'), 'title': _('Change Password')}
761
762 class Session(http.Controller):
763
764     def session_info(self):
765         request.session.ensure_valid()
766         return {
767             "session_id": request.session_id,
768             "uid": request.session.uid,
769             "user_context": request.session.get_context() if request.session.uid else {},
770             "db": request.session.db,
771             "username": request.session.login,
772         }
773
774     @http.route('/web/session/get_session_info', type='json', auth="none")
775     def get_session_info(self):
776         request.uid = request.session.uid
777         request.disable_db = False
778         return self.session_info()
779
780     @http.route('/web/session/authenticate', type='json', auth="none")
781     def authenticate(self, db, login, password, base_location=None):
782         request.session.authenticate(db, login, password)
783
784         return self.session_info()
785
786     @http.route('/web/session/change_password', type='json', auth="user")
787     def change_password(self, fields):
788         old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
789                 dict(map(operator.itemgetter('name', 'value'), fields)))
790         if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
791             return {'error':_('You cannot leave any password empty.'),'title': _('Change Password')}
792         if new_password != confirm_password:
793             return {'error': _('The new password and its confirmation must be identical.'),'title': _('Change Password')}
794         try:
795             if request.session.model('res.users').change_password(
796                 old_password, new_password):
797                 return {'new_password':new_password}
798         except Exception:
799             return {'error': _('The old password you provided is incorrect, your password was not changed.'), 'title': _('Change Password')}
800         return {'error': _('Error, password not changed !'), 'title': _('Change Password')}
801
802     @http.route('/web/session/get_lang_list', type='json', auth="none")
803     def get_lang_list(self):
804         try:
805             return request.session.proxy("db").list_lang() or []
806         except Exception, e:
807             return {"error": e, "title": _("Languages")}
808
809     @http.route('/web/session/modules', type='json', auth="user")
810     def modules(self):
811         # return all installed modules. Web client is smart enough to not load a module twice
812         return module_installed()
813
814     @http.route('/web/session/save_session_action', type='json', auth="user")
815     def save_session_action(self, the_action):
816         """
817         This method store an action object in the session object and returns an integer
818         identifying that action. The method get_session_action() can be used to get
819         back the action.
820
821         :param the_action: The action to save in the session.
822         :type the_action: anything
823         :return: A key identifying the saved action.
824         :rtype: integer
825         """
826         return request.httpsession.save_action(the_action)
827
828     @http.route('/web/session/get_session_action', type='json', auth="user")
829     def get_session_action(self, key):
830         """
831         Gets back a previously saved action. This method can return None if the action
832         was saved since too much time (this case should be handled in a smart way).
833
834         :param key: The key given by save_session_action()
835         :type key: integer
836         :return: The saved action or None.
837         :rtype: anything
838         """
839         return request.httpsession.get_action(key)
840
841     @http.route('/web/session/check', type='json', auth="user")
842     def check(self):
843         request.session.assert_valid()
844         return None
845
846     @http.route('/web/session/destroy', type='json', auth="user")
847     def destroy(self):
848         request.session.logout()
849
850     @http.route('/web/session/logout', type='http', auth="none")
851     def logout(self, redirect='/web'):
852         request.session.logout(keep_db=True)
853         return werkzeug.utils.redirect(redirect, 303)
854
855 class Menu(http.Controller):
856
857     @http.route('/web/menu/load_needaction', type='json', auth="user")
858     def load_needaction(self, menu_ids):
859         """ Loads needaction counters for specific menu ids.
860
861             :return: needaction data
862             :rtype: dict(menu_id: {'needaction_enabled': boolean, 'needaction_counter': int})
863         """
864         return request.session.model('ir.ui.menu').get_needaction_data(menu_ids, request.context)
865
866 class DataSet(http.Controller):
867
868     @http.route('/web/dataset/search_read', type='json', auth="user")
869     def search_read(self, model, fields=False, offset=0, limit=False, domain=None, sort=None):
870         return self.do_search_read(model, fields, offset, limit, domain, sort)
871     def do_search_read(self, model, fields=False, offset=0, limit=False, domain=None
872                        , sort=None):
873         """ Performs a search() followed by a read() (if needed) using the
874         provided search criteria
875
876         :param str model: the name of the model to search on
877         :param fields: a list of the fields to return in the result records
878         :type fields: [str]
879         :param int offset: from which index should the results start being returned
880         :param int limit: the maximum number of records to return
881         :param list domain: the search domain for the query
882         :param list sort: sorting directives
883         :returns: A structure (dict) with two keys: ids (all the ids matching
884                   the (domain, context) pair) and records (paginated records
885                   matching fields selection set)
886         :rtype: list
887         """
888         Model = request.session.model(model)
889
890         records = Model.search_read(domain, fields, offset or 0, limit or False, sort or False,
891                            request.context)
892         if not records:
893             return {
894                 'length': 0,
895                 'records': []
896             }
897         if limit and len(records) == limit:
898             length = Model.search_count(domain, request.context)
899         else:
900             length = len(records) + (offset or 0)
901         return {
902             'length': length,
903             'records': records
904         }
905
906     @http.route('/web/dataset/load', type='json', auth="user")
907     def load(self, model, id, fields):
908         m = request.session.model(model)
909         value = {}
910         r = m.read([id], False, request.context)
911         if r:
912             value = r[0]
913         return {'value': value}
914
915     def call_common(self, model, method, args, domain_id=None, context_id=None):
916         return self._call_kw(model, method, args, {})
917
918     def _call_kw(self, model, method, args, kwargs):
919         # Temporary implements future display_name special field for model#read()
920         if method in ('read', 'search_read') and kwargs.get('context', {}).get('future_display_name'):
921             if 'display_name' in args[1]:
922                 if method == 'read':
923                     names = dict(request.session.model(model).name_get(args[0], **kwargs))
924                 else:
925                     names = dict(request.session.model(model).name_search('', args[0], **kwargs))
926                 args[1].remove('display_name')
927                 records = getattr(request.session.model(model), method)(*args, **kwargs)
928                 for record in records:
929                     record['display_name'] = \
930                         names.get(record['id']) or "{0}#{1}".format(model, (record['id']))
931                 return records
932
933         if method.startswith('_'):
934             raise Exception("Access Denied: Underscore prefixed methods cannot be remotely called")
935
936         return getattr(request.registry.get(model), method)(request.cr, request.uid, *args, **kwargs)
937
938     @http.route('/web/dataset/call', type='json', auth="user")
939     def call(self, model, method, args, domain_id=None, context_id=None):
940         return self._call_kw(model, method, args, {})
941
942     @http.route(['/web/dataset/call_kw', '/web/dataset/call_kw/<path:path>'], type='json', auth="user")
943     def call_kw(self, model, method, args, kwargs, path=None):
944         return self._call_kw(model, method, args, kwargs)
945
946     @http.route('/web/dataset/call_button', type='json', auth="user")
947     def call_button(self, model, method, args, domain_id=None, context_id=None):
948         action = self._call_kw(model, method, args, {})
949         if isinstance(action, dict) and action.get('type') != '':
950             return clean_action(action)
951         return False
952
953     @http.route('/web/dataset/exec_workflow', type='json', auth="user")
954     def exec_workflow(self, model, id, signal):
955         return request.session.exec_workflow(model, id, signal)
956
957     @http.route('/web/dataset/resequence', type='json', auth="user")
958     def resequence(self, model, ids, field='sequence', offset=0):
959         """ Re-sequences a number of records in the model, by their ids
960
961         The re-sequencing starts at the first model of ``ids``, the sequence
962         number is incremented by one after each record and starts at ``offset``
963
964         :param ids: identifiers of the records to resequence, in the new sequence order
965         :type ids: list(id)
966         :param str field: field used for sequence specification, defaults to
967                           "sequence"
968         :param int offset: sequence number for first record in ``ids``, allows
969                            starting the resequencing from an arbitrary number,
970                            defaults to ``0``
971         """
972         m = request.session.model(model)
973         if not m.fields_get([field]):
974             return False
975         # python 2.6 has no start parameter
976         for i, id in enumerate(ids):
977             m.write(id, { field: i + offset })
978         return True
979
980 class View(http.Controller):
981
982     @http.route('/web/view/add_custom', type='json', auth="user")
983     def add_custom(self, view_id, arch):
984         CustomView = request.session.model('ir.ui.view.custom')
985         CustomView.create({
986             'user_id': request.session.uid,
987             'ref_id': view_id,
988             'arch': arch
989         }, request.context)
990         return {'result': True}
991
992     @http.route('/web/view/undo_custom', type='json', auth="user")
993     def undo_custom(self, view_id, reset=False):
994         CustomView = request.session.model('ir.ui.view.custom')
995         vcustom = CustomView.search([('user_id', '=', request.session.uid), ('ref_id' ,'=', view_id)],
996                                     0, False, False, request.context)
997         if vcustom:
998             if reset:
999                 CustomView.unlink(vcustom, request.context)
1000             else:
1001                 CustomView.unlink([vcustom[0]], request.context)
1002             return {'result': True}
1003         return {'result': False}
1004
1005 class TreeView(View):
1006
1007     @http.route('/web/treeview/action', type='json', auth="user")
1008     def action(self, model, id):
1009         return load_actions_from_ir_values(
1010             'action', 'tree_but_open',[(model, id)],
1011             False)
1012
1013 class Binary(http.Controller):
1014
1015     @http.route('/web/binary/image', type='http', auth="public")
1016     def image(self, model, id, field, **kw):
1017         last_update = '__last_update'
1018         Model = request.session.model(model)
1019         headers = [('Content-Type', 'image/png')]
1020         etag = request.httprequest.headers.get('If-None-Match')
1021         hashed_session = hashlib.md5(request.session_id).hexdigest()
1022         retag = hashed_session
1023         id = None if not id else simplejson.loads(id)
1024         if type(id) is list:
1025             id = id[0] # m2o
1026         try:
1027             if etag:
1028                 if not id and hashed_session == etag:
1029                     return werkzeug.wrappers.Response(status=304)
1030                 else:
1031                     date = Model.read([id], [last_update], request.context)[0].get(last_update)
1032                     if hashlib.md5(date).hexdigest() == etag:
1033                         return werkzeug.wrappers.Response(status=304)
1034
1035             if not id:
1036                 res = Model.default_get([field], request.context).get(field)
1037                 image_base64 = res
1038             else:
1039                 res = Model.read([id], [last_update, field], request.context)[0]
1040                 retag = hashlib.md5(res.get(last_update)).hexdigest()
1041                 image_base64 = res.get(field)
1042
1043             if kw.get('resize'):
1044                 resize = kw.get('resize').split(',')
1045                 if len(resize) == 2 and int(resize[0]) and int(resize[1]):
1046                     width = int(resize[0])
1047                     height = int(resize[1])
1048                     # resize maximum 500*500
1049                     if width > 500: width = 500
1050                     if height > 500: height = 500
1051                     image_base64 = openerp.tools.image_resize_image(base64_source=image_base64, size=(width, height), encoding='base64', filetype='PNG')
1052
1053             image_data = base64.b64decode(image_base64)
1054
1055         except Exception:
1056             image_data = self.placeholder()
1057         headers.append(('ETag', retag))
1058         headers.append(('Content-Length', len(image_data)))
1059         try:
1060             ncache = int(kw.get('cache'))
1061             headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1062         except:
1063             pass
1064         return request.make_response(image_data, headers)
1065
1066     def placeholder(self, image='placeholder.png'):
1067         addons_path = http.addons_manifest['web']['addons_path']
1068         return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', image), 'rb').read()
1069
1070     @http.route('/web/binary/saveas', type='http', auth="public")
1071     @serialize_exception
1072     def saveas(self, model, field, id=None, filename_field=None, **kw):
1073         """ Download link for files stored as binary fields.
1074
1075         If the ``id`` parameter is omitted, fetches the default value for the
1076         binary field (via ``default_get``), otherwise fetches the field for
1077         that precise record.
1078
1079         :param str model: name of the model to fetch the binary from
1080         :param str field: binary field
1081         :param str id: id of the record from which to fetch the binary
1082         :param str filename_field: field holding the file's name, if any
1083         :returns: :class:`werkzeug.wrappers.Response`
1084         """
1085         Model = request.session.model(model)
1086         fields = [field]
1087         if filename_field:
1088             fields.append(filename_field)
1089         if id:
1090             res = Model.read([int(id)], fields, request.context)[0]
1091         else:
1092             res = Model.default_get(fields, request.context)
1093         filecontent = base64.b64decode(res.get(field, ''))
1094         if not filecontent:
1095             return request.not_found()
1096         else:
1097             filename = '%s_%s' % (model.replace('.', '_'), id)
1098             if filename_field:
1099                 filename = res.get(filename_field, '') or filename
1100             return request.make_response(filecontent,
1101                 [('Content-Type', 'application/octet-stream'),
1102                  ('Content-Disposition', content_disposition(filename))])
1103
1104     @http.route('/web/binary/saveas_ajax', type='http', auth="public")
1105     @serialize_exception
1106     def saveas_ajax(self, data, token):
1107         jdata = simplejson.loads(data)
1108         model = jdata['model']
1109         field = jdata['field']
1110         data = jdata['data']
1111         id = jdata.get('id', None)
1112         filename_field = jdata.get('filename_field', None)
1113         context = jdata.get('context', {})
1114
1115         Model = request.session.model(model)
1116         fields = [field]
1117         if filename_field:
1118             fields.append(filename_field)
1119         if data:
1120             res = { field: data }
1121         elif id:
1122             res = Model.read([int(id)], fields, context)[0]
1123         else:
1124             res = Model.default_get(fields, context)
1125         filecontent = base64.b64decode(res.get(field, ''))
1126         if not filecontent:
1127             raise ValueError(_("No content found for field '%s' on '%s:%s'") %
1128                 (field, model, id))
1129         else:
1130             filename = '%s_%s' % (model.replace('.', '_'), id)
1131             if filename_field:
1132                 filename = res.get(filename_field, '') or filename
1133             return request.make_response(filecontent,
1134                 headers=[('Content-Type', 'application/octet-stream'),
1135                         ('Content-Disposition', content_disposition(filename))],
1136                 cookies={'fileToken': token})
1137
1138     @http.route('/web/binary/upload', type='http', auth="user")
1139     @serialize_exception
1140     def upload(self, callback, ufile):
1141         # TODO: might be useful to have a configuration flag for max-length file uploads
1142         out = """<script language="javascript" type="text/javascript">
1143                     var win = window.top.window;
1144                     win.jQuery(win).trigger(%s, %s);
1145                 </script>"""
1146         try:
1147             data = ufile.read()
1148             args = [len(data), ufile.filename,
1149                     ufile.content_type, base64.b64encode(data)]
1150         except Exception, e:
1151             args = [False, e.message]
1152         return out % (simplejson.dumps(callback), simplejson.dumps(args))
1153
1154     @http.route('/web/binary/upload_attachment', type='http', auth="user")
1155     @serialize_exception
1156     def upload_attachment(self, callback, model, id, ufile):
1157         Model = request.session.model('ir.attachment')
1158         out = """<script language="javascript" type="text/javascript">
1159                     var win = window.top.window;
1160                     win.jQuery(win).trigger(%s, %s);
1161                 </script>"""
1162         try:
1163             attachment_id = Model.create({
1164                 'name': ufile.filename,
1165                 'datas': base64.encodestring(ufile.read()),
1166                 'datas_fname': ufile.filename,
1167                 'res_model': model,
1168                 'res_id': int(id)
1169             }, request.context)
1170             args = {
1171                 'filename': ufile.filename,
1172                 'id':  attachment_id
1173             }
1174         except Exception:
1175             args = {'error': "Something horrible happened"}
1176         return out % (simplejson.dumps(callback), simplejson.dumps(args))
1177
1178     @http.route([
1179         '/web/binary/company_logo',
1180         '/logo',
1181         '/logo.png',
1182     ], type='http', auth="none")
1183     def company_logo(self, dbname=None, **kw):
1184         imgname = 'logo.png'
1185         placeholder = functools.partial(get_module_resource, 'web', 'static', 'src', 'img')
1186         uid = None
1187         if request.session.db:
1188             dbname = request.session.db
1189             uid = request.session.uid
1190         elif dbname is None:
1191             dbname = db_monodb()
1192
1193         if not uid:
1194             uid = openerp.SUPERUSER_ID
1195
1196         if not dbname:
1197             response = http.send_file(placeholder(imgname))
1198         else:
1199             try:
1200                 # create an empty registry
1201                 registry = openerp.modules.registry.Registry(dbname)
1202                 with registry.cursor() as cr:
1203                     cr.execute("""SELECT c.logo_web, c.write_date
1204                                     FROM res_users u
1205                                LEFT JOIN res_company c
1206                                       ON c.id = u.company_id
1207                                    WHERE u.id = %s
1208                                """, (uid,))
1209                     row = cr.fetchone()
1210                     if row and row[0]:
1211                         image_data = StringIO(str(row[0]).decode('base64'))
1212                         response = http.send_file(image_data, filename=imgname, mtime=row[1])
1213                     else:
1214                         response = http.send_file(placeholder('nologo.png'))
1215             except Exception:
1216                 response = http.send_file(placeholder(imgname))
1217
1218         return response
1219
1220 class Action(http.Controller):
1221
1222     @http.route('/web/action/load', type='json', auth="user")
1223     def load(self, action_id, do_not_eval=False, additional_context=None):
1224         Actions = request.session.model('ir.actions.actions')
1225         value = False
1226         try:
1227             action_id = int(action_id)
1228         except ValueError:
1229             try:
1230                 module, xmlid = action_id.split('.', 1)
1231                 model, action_id = request.session.model('ir.model.data').get_object_reference(module, xmlid)
1232                 assert model.startswith('ir.actions.')
1233             except Exception:
1234                 action_id = 0   # force failed read
1235
1236         base_action = Actions.read([action_id], ['type'], request.context)
1237         if base_action:
1238             ctx = request.context
1239             action_type = base_action[0]['type']
1240             if action_type == 'ir.actions.report.xml':
1241                 ctx.update({'bin_size': True})
1242             if additional_context:
1243                 ctx.update(additional_context)
1244             action = request.session.model(action_type).read([action_id], False, ctx)
1245             if action:
1246                 value = clean_action(action[0])
1247         return value
1248
1249     @http.route('/web/action/run', type='json', auth="user")
1250     def run(self, action_id):
1251         return_action = request.session.model('ir.actions.server').run(
1252             [action_id], request.context)
1253         if return_action:
1254             return clean_action(return_action)
1255         else:
1256             return False
1257
1258 class Export(http.Controller):
1259
1260     @http.route('/web/export/formats', type='json', auth="user")
1261     def formats(self):
1262         """ Returns all valid export formats
1263
1264         :returns: for each export format, a pair of identifier and printable name
1265         :rtype: [(str, str)]
1266         """
1267         return [
1268             {'tag': 'csv', 'label': 'CSV'},
1269             {'tag': 'xls', 'label': 'Excel', 'error': None if xlwt else "XLWT required"},
1270         ]
1271
1272     def fields_get(self, model):
1273         Model = request.session.model(model)
1274         fields = Model.fields_get(False, request.context)
1275         return fields
1276
1277     @http.route('/web/export/get_fields', type='json', auth="user")
1278     def get_fields(self, model, prefix='', parent_name= '',
1279                    import_compat=True, parent_field_type=None,
1280                    exclude=None):
1281
1282         if import_compat and parent_field_type == "many2one":
1283             fields = {}
1284         else:
1285             fields = self.fields_get(model)
1286
1287         if import_compat:
1288             fields.pop('id', None)
1289         else:
1290             fields['.id'] = fields.pop('id', {'string': 'ID'})
1291
1292         fields_sequence = sorted(fields.iteritems(),
1293             key=lambda field: openerp.tools.ustr(field[1].get('string', '')))
1294
1295         records = []
1296         for field_name, field in fields_sequence:
1297             if import_compat:
1298                 if exclude and field_name in exclude:
1299                     continue
1300                 if field.get('readonly'):
1301                     # If none of the field's states unsets readonly, skip the field
1302                     if all(dict(attrs).get('readonly', True)
1303                            for attrs in field.get('states', {}).values()):
1304                         continue
1305             if not field.get('exportable', True):
1306                 continue
1307
1308             id = prefix + (prefix and '/'or '') + field_name
1309             name = parent_name + (parent_name and '/' or '') + field['string']
1310             record = {'id': id, 'string': name,
1311                       'value': id, 'children': False,
1312                       'field_type': field.get('type'),
1313                       'required': field.get('required'),
1314                       'relation_field': field.get('relation_field')}
1315             records.append(record)
1316
1317             if len(name.split('/')) < 3 and 'relation' in field:
1318                 ref = field.pop('relation')
1319                 record['value'] += '/id'
1320                 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1321
1322                 if not import_compat or field['type'] == 'one2many':
1323                     # m2m field in import_compat is childless
1324                     record['children'] = True
1325
1326         return records
1327
1328     @http.route('/web/export/namelist', type='json', auth="user")
1329     def namelist(self, model, export_id):
1330         # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1331         export = request.session.model("ir.exports").read([export_id])[0]
1332         export_fields_list = request.session.model("ir.exports.line").read(
1333             export['export_fields'])
1334
1335         fields_data = self.fields_info(
1336             model, map(operator.itemgetter('name'), export_fields_list))
1337
1338         return [
1339             {'name': field['name'], 'label': fields_data[field['name']]}
1340             for field in export_fields_list
1341         ]
1342
1343     def fields_info(self, model, export_fields):
1344         info = {}
1345         fields = self.fields_get(model)
1346         if ".id" in export_fields:
1347             fields['.id'] = fields.pop('id', {'string': 'ID'})
1348
1349         # To make fields retrieval more efficient, fetch all sub-fields of a
1350         # given field at the same time. Because the order in the export list is
1351         # arbitrary, this requires ordering all sub-fields of a given field
1352         # together so they can be fetched at the same time
1353         #
1354         # Works the following way:
1355         # * sort the list of fields to export, the default sorting order will
1356         #   put the field itself (if present, for xmlid) and all of its
1357         #   sub-fields right after it
1358         # * then, group on: the first field of the path (which is the same for
1359         #   a field and for its subfields and the length of splitting on the
1360         #   first '/', which basically means grouping the field on one side and
1361         #   all of the subfields on the other. This way, we have the field (for
1362         #   the xmlid) with length 1, and all of the subfields with the same
1363         #   base but a length "flag" of 2
1364         # * if we have a normal field (length 1), just add it to the info
1365         #   mapping (with its string) as-is
1366         # * otherwise, recursively call fields_info via graft_subfields.
1367         #   all graft_subfields does is take the result of fields_info (on the
1368         #   field's model) and prepend the current base (current field), which
1369         #   rebuilds the whole sub-tree for the field
1370         #
1371         # result: because we're not fetching the fields_get for half the
1372         # database models, fetching a namelist with a dozen fields (including
1373         # relational data) falls from ~6s to ~300ms (on the leads model).
1374         # export lists with no sub-fields (e.g. import_compatible lists with
1375         # no o2m) are even more efficient (from the same 6s to ~170ms, as
1376         # there's a single fields_get to execute)
1377         for (base, length), subfields in itertools.groupby(
1378                 sorted(export_fields),
1379                 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1380             subfields = list(subfields)
1381             if length == 2:
1382                 # subfields is a seq of $base/*rest, and not loaded yet
1383                 info.update(self.graft_subfields(
1384                     fields[base]['relation'], base, fields[base]['string'],
1385                     subfields
1386                 ))
1387             elif base in fields:
1388                 info[base] = fields[base]['string']
1389
1390         return info
1391
1392     def graft_subfields(self, model, prefix, prefix_string, fields):
1393         export_fields = [field.split('/', 1)[1] for field in fields]
1394         return (
1395             (prefix + '/' + k, prefix_string + '/' + v)
1396             for k, v in self.fields_info(model, export_fields).iteritems())
1397
1398 class ExportFormat(object):
1399     raw_data = False
1400
1401     @property
1402     def content_type(self):
1403         """ Provides the format's content type """
1404         raise NotImplementedError()
1405
1406     def filename(self, base):
1407         """ Creates a valid filename for the format (with extension) from the
1408          provided base name (exension-less)
1409         """
1410         raise NotImplementedError()
1411
1412     def from_data(self, fields, rows):
1413         """ Conversion method from OpenERP's export data to whatever the
1414         current export class outputs
1415
1416         :params list fields: a list of fields to export
1417         :params list rows: a list of records to export
1418         :returns:
1419         :rtype: bytes
1420         """
1421         raise NotImplementedError()
1422
1423     def base(self, data, token):
1424         params = simplejson.loads(data)
1425         model, fields, ids, domain, import_compat = \
1426             operator.itemgetter('model', 'fields', 'ids', 'domain',
1427                                 'import_compat')(
1428                 params)
1429
1430         Model = request.session.model(model)
1431         context = dict(request.context or {}, **params.get('context', {}))
1432         ids = ids or Model.search(domain, 0, False, False, context)
1433
1434         field_names = map(operator.itemgetter('name'), fields)
1435         import_data = Model.export_data(ids, field_names, self.raw_data, context=context).get('datas',[])
1436
1437         if import_compat:
1438             columns_headers = field_names
1439         else:
1440             columns_headers = [val['label'].strip() for val in fields]
1441
1442
1443         return request.make_response(self.from_data(columns_headers, import_data),
1444             headers=[('Content-Disposition',
1445                             content_disposition(self.filename(model))),
1446                      ('Content-Type', self.content_type)],
1447             cookies={'fileToken': token})
1448
1449 class CSVExport(ExportFormat, http.Controller):
1450
1451     @http.route('/web/export/csv', type='http', auth="user")
1452     @serialize_exception
1453     def index(self, data, token):
1454         return self.base(data, token)
1455
1456     @property
1457     def content_type(self):
1458         return 'text/csv;charset=utf8'
1459
1460     def filename(self, base):
1461         return base + '.csv'
1462
1463     def from_data(self, fields, rows):
1464         fp = StringIO()
1465         writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1466
1467         writer.writerow([name.encode('utf-8') for name in fields])
1468
1469         for data in rows:
1470             row = []
1471             for d in data:
1472                 if isinstance(d, basestring):
1473                     d = d.replace('\n',' ').replace('\t',' ')
1474                     try:
1475                         d = d.encode('utf-8')
1476                     except UnicodeError:
1477                         pass
1478                 if d is False: d = None
1479                 row.append(d)
1480             writer.writerow(row)
1481
1482         fp.seek(0)
1483         data = fp.read()
1484         fp.close()
1485         return data
1486
1487 class ExcelExport(ExportFormat, http.Controller):
1488     # Excel needs raw data to correctly handle numbers and date values
1489     raw_data = True
1490
1491     @http.route('/web/export/xls', type='http', auth="user")
1492     @serialize_exception
1493     def index(self, data, token):
1494         return self.base(data, token)
1495
1496     @property
1497     def content_type(self):
1498         return 'application/vnd.ms-excel'
1499
1500     def filename(self, base):
1501         return base + '.xls'
1502
1503     def from_data(self, fields, rows):
1504         workbook = xlwt.Workbook()
1505         worksheet = workbook.add_sheet('Sheet 1')
1506
1507         for i, fieldname in enumerate(fields):
1508             worksheet.write(0, i, fieldname)
1509             worksheet.col(i).width = 8000 # around 220 pixels
1510
1511         base_style = xlwt.easyxf('align: wrap yes')
1512         date_style = xlwt.easyxf('align: wrap yes', num_format_str='YYYY-MM-DD')
1513         datetime_style = xlwt.easyxf('align: wrap yes', num_format_str='YYYY-MM-DD HH:mm:SS')
1514
1515         for row_index, row in enumerate(rows):
1516             for cell_index, cell_value in enumerate(row):
1517                 cell_style = base_style
1518                 if isinstance(cell_value, basestring):
1519                     cell_value = re.sub("\r", " ", cell_value)
1520                 elif isinstance(cell_value, datetime.datetime):
1521                     cell_style = datetime_style
1522                 elif isinstance(cell_value, datetime.date):
1523                     cell_style = date_style
1524                 worksheet.write(row_index + 1, cell_index, cell_value, cell_style)
1525
1526         fp = StringIO()
1527         workbook.save(fp)
1528         fp.seek(0)
1529         data = fp.read()
1530         fp.close()
1531         return data
1532
1533 class Reports(http.Controller):
1534     POLLING_DELAY = 0.25
1535     TYPES_MAPPING = {
1536         'doc': 'application/vnd.ms-word',
1537         'html': 'text/html',
1538         'odt': 'application/vnd.oasis.opendocument.text',
1539         'pdf': 'application/pdf',
1540         'sxw': 'application/vnd.sun.xml.writer',
1541         'xls': 'application/vnd.ms-excel',
1542     }
1543
1544     @http.route('/web/report', type='http', auth="user")
1545     @serialize_exception
1546     def index(self, action, token):
1547         action = simplejson.loads(action)
1548
1549         report_srv = request.session.proxy("report")
1550         context = dict(request.context)
1551         context.update(action["context"])
1552
1553         report_data = {}
1554         report_ids = context.get("active_ids", None)
1555         if 'report_type' in action:
1556             report_data['report_type'] = action['report_type']
1557         if 'datas' in action:
1558             if 'ids' in action['datas']:
1559                 report_ids = action['datas'].pop('ids')
1560             report_data.update(action['datas'])
1561
1562         report_id = report_srv.report(
1563             request.session.db, request.session.uid, request.session.password,
1564             action["report_name"], report_ids,
1565             report_data, context)
1566
1567         report_struct = None
1568         while True:
1569             report_struct = report_srv.report_get(
1570                 request.session.db, request.session.uid, request.session.password, report_id)
1571             if report_struct["state"]:
1572                 break
1573
1574             time.sleep(self.POLLING_DELAY)
1575
1576         report = base64.b64decode(report_struct['result'])
1577         if report_struct.get('code') == 'zlib':
1578             report = zlib.decompress(report)
1579         report_mimetype = self.TYPES_MAPPING.get(
1580             report_struct['format'], 'octet-stream')
1581         file_name = action.get('name', 'report')
1582         if 'name' not in action:
1583             reports = request.session.model('ir.actions.report.xml')
1584             res_id = reports.search([('report_name', '=', action['report_name']),],
1585                                     0, False, False, context)
1586             if len(res_id) > 0:
1587                 file_name = reports.read(res_id[0], ['name'], context)['name']
1588             else:
1589                 file_name = action['report_name']
1590         file_name = '%s.%s' % (file_name, report_struct['format'])
1591
1592         return request.make_response(report,
1593              headers=[
1594                  ('Content-Disposition', content_disposition(file_name)),
1595                  ('Content-Type', report_mimetype),
1596                  ('Content-Length', len(report))],
1597              cookies={'fileToken': token})
1598
1599 class Apps(http.Controller):
1600     @http.route('/apps/<app>', auth='user')
1601     def get_app_url(self, req, app):
1602         act_window_obj = request.session.model('ir.actions.act_window')
1603         ir_model_data = request.session.model('ir.model.data')
1604         try:
1605             action_id = ir_model_data.get_object_reference('base', 'open_module_tree')[1]
1606             action = act_window_obj.read(action_id, ['name', 'type', 'res_model', 'view_mode', 'view_type', 'context', 'views', 'domain'])
1607             action['target'] = 'current'
1608         except ValueError:
1609             action = False
1610         try:
1611             app_id = ir_model_data.get_object_reference('base', 'module_%s' % app)[1]
1612         except ValueError:
1613             app_id = False
1614
1615         if action and app_id:
1616             action['res_id'] = app_id
1617             action['view_mode'] = 'form'
1618             action['views'] = [(False, u'form')]
1619
1620         sakey = Session().save_session_action(action)
1621         debug = '?debug' if req.debug else ''
1622         return werkzeug.utils.redirect('/web{0}#sa={1}'.format(debug, sakey))
1623
1624
1625
1626 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: