1 # -*- coding: utf-8 -*-
21 from xml.etree import ElementTree
22 from cStringIO import StringIO
24 import babel.messages.pofile
26 import werkzeug.wrappers
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
40 from openerp.http import request, serialize_exception as _serialize_exception
42 _logger = logging.getLogger(__name__)
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)
49 loader = jinja2.PackageLoader('openerp.addons.web', "views")
51 env = jinja2.Environment(loader=loader, autoescape=True)
52 env.filters["json"] = simplejson.dumps
54 # 1 week cache for asset bundles as advised by Google Page Speed
55 BUNDLE_MAXAGE = 60 * 60 * 24 * 7
57 #----------------------------------------------------------
59 #----------------------------------------------------------
61 db_list = http.db_list
63 db_monodb = http.db_monodb
65 def serialize_exception(f):
67 def wrap(*args, **kwargs):
69 return f(*args, **kwargs)
71 _logger.exception("An exception occured during an http request")
72 se = _serialize_exception(e)
75 'message': "Odoo Server Error",
78 return werkzeug.exceptions.InternalServerError(simplejson.dumps(error))
81 def redirect_with_hash(*args, **kw):
85 Use the ``http.redirect_with_hash()`` function instead.
87 return http.redirect_with_hash(*args, **kw)
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)
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')
106 if db and db not in http.db_filter([db]):
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
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)
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
130 # if no database provided and no database in session, use monodb
132 db = db_monodb(request.httprequest)
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
137 werkzeug.exceptions.abort(werkzeug.utils.redirect(redirect, 303))
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)
144 request.session.db = db
146 def module_installed():
147 # Candidates module the current heuristic is the /static dir
148 loadable = http.addons_manifest.keys()
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')
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
163 sorted_modules = topological_sort(modules)
164 return sorted_modules
166 def module_installed_bypass_session(dbname):
167 loadable = http.addons_manifest.keys()
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')
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
185 sorted_modules = topological_sort(modules)
186 return sorted_modules
188 def module_boot(db=None):
189 server_wide_modules = openerp.conf.server_wide_modules or ['web']
192 for i in server_wide_modules:
193 if i in http.addons_manifest:
195 monodb = db or db_monodb()
197 dbside = module_installed_bypass_session(monodb)
198 dbside = [i for i in dbside if i not in serverside]
199 addons = serverside + dbside
202 def concat_xml(file_list):
203 """Concatenate xml files
205 :param list(str) file_list: list of files to check
206 :returns: (concatenation_result, checksum)
209 checksum = hashlib.new('sha1')
211 return '', checksum.hexdigest()
214 for fname in file_list:
215 with open(fname, 'rb') as fp:
217 checksum.update(contents)
219 xml = ElementTree.parse(fp).getroot()
222 root = ElementTree.Element(xml.tag)
223 #elif root.tag != xml.tag:
224 # raise ValueError("Root tags missmatch: %r != %r" % (root.tag, xml.tag))
226 for child in xml.getchildren():
228 return ElementTree.tostring(root, 'utf-8'), checksum.hexdigest()
231 """convert FS path into web path"""
232 return '/'.join(path.split(os.path.sep))
234 def manifest_glob(extension, addons=None, db=None, include_remotes=False):
236 addons = module_boot(db=db)
238 addons = addons.split(',')
241 manifest = http.addons_manifest.get(addon, None)
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://', '//')):
250 r.append((None, pattern))
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):])))
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)
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]
266 def get_last_modified(files):
267 """ Returns the modification time of the most recently modified
270 :param list(str) files: names of files to check
271 :return: most recent modification time amongst the fileset
272 :rtype: datetime.datetime
276 return max(datetime.datetime.fromtimestamp(os.path.getmtime(f))
278 return datetime.datetime(1970, 1, 1)
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
284 Uses Werkzeug's own :meth:`ETagResponseMixin.make_conditional`, after
285 setting ``last_modified`` and ``etag`` correctly on the response object
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
294 response.cache_control.must_revalidate = True
295 response.cache_control.max_age = max_age
297 response.last_modified = last_modified
299 response.set_etag(etag)
300 return response.make_conditional(request.httprequest)
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)
306 def set_cookie_and_redirect(redirect_url):
307 redirect = werkzeug.utils.redirect(redirect_url, 303)
308 redirect.autocorrect_location_header = False
311 def login_redirect():
315 return """<html><head><script>
316 window.location = '%sredirect=' + encodeURIComponent(window.location);
317 </script></head></html>
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)
324 return [(id, name, clean_action(action))
325 for id, name, action in actions]
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)
334 # I think generate_views,fix_view_modes should go into js ActionManager
335 def generate_views(action):
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.
342 In that case, there is no ``views`` key available on the action.
344 Since the web client relies on ``action['views']``, generate it here from
345 ``view_mode`` and ``view_id``.
347 Currently handles two different cases:
349 * no view_id, multiple view_mode
350 * single view_id, single view_mode
352 :param dict action: action descriptor dictionary to generate a views key for
354 view_id = action.get('view_id') or False
355 if isinstance(view_id, (list, tuple)):
358 # providing at least one view mode is a requirement, not an option
359 view_modes = action['view_mode'].split(',')
361 if len(view_modes) > 1:
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]
370 action['views'] = [(view_id, view_modes[0])]
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):
376 * one of the view modes is ``tree``, which stands for both list 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
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.
385 TODO: this should go into the doc, some kind of "peculiarities" section
387 :param dict action: an action descriptor
388 :returns: nothing, the action is modified in place
390 if not action.get('views'):
391 generate_views(action)
393 if action.pop('view_type', 'form') != 'form':
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(','))
401 [id, mode if mode != 'tree' else 'list']
402 for id, mode in action['views']
407 def _local_web_translations(trans_file):
410 with open(trans_file) as t_file:
411 po = babel.messages.pofile.read_po(t_file)
415 if x.id and x.string and "openerp-web" in x.auto_comments:
416 messages.append({'id': x.id, 'string': x.string})
419 def xml2json_from_elementtree(el, preserve_whitespaces=False):
421 Simple and straightforward XML-to-JSON converter in Python
423 http://code.google.com/p/xml2json-direct/
427 ns, name = el.tag.rsplit("}", 1)
429 res["namespace"] = ns[1:]
433 for k, v in el.items():
436 if el.text and (preserve_whitespaces or el.text.strip() != ''):
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
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
455 return "attachment; filename*=UTF-8''%s" % escaped
458 #----------------------------------------------------------
459 # OpenERP Web web Controllers
460 #----------------------------------------------------------
461 class Home(http.Controller):
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)
467 @http.route('/web', type='http', auth="none")
468 def web_client(self, s_action=None, **kw):
470 if request.session.uid:
471 if kw.get('redirect'):
472 return werkzeug.utils.redirect(kw.get('redirect'), 303)
474 request.uid = request.session.uid
476 menu_data = request.registry['ir.ui.menu'].load_menus(request.cr, request.uid, context=request.context)
477 return request.render('web.webclient_bootstrap', qcontext={'menu_data': menu_data})
479 return login_redirect()
481 @http.route('/web/dbredirect', type='http', auth="none")
482 def web_db_redirect(self, redirect='/', **kw):
484 return werkzeug.utils.redirect(redirect, 303)
486 @http.route('/web/login', type='http', auth="none")
487 def web_login(self, redirect=None, **kw):
490 if request.httprequest.method == 'GET' and redirect and request.session.uid:
491 return http.redirect_with_hash(redirect)
494 request.uid = openerp.SUPERUSER_ID
496 values = request.params.copy()
498 redirect = '/web?' + request.httprequest.query_string
499 values['redirect'] = redirect
502 values['databases'] = http.db_list()
503 except openerp.exceptions.AccessDenied:
504 values['databases'] = None
506 if request.httprequest.method == 'POST':
507 old_uid = request.uid
508 uid = request.session.authenticate(request.session.db, request.params['login'], request.params['password'])
510 return http.redirect_with_hash(redirect)
511 request.uid = old_uid
512 values['error'] = "Wrong login/password"
513 return request.render('web.login', values)
515 @http.route('/login', type='http', auth="none")
516 def login(self, db, login, key, redirect="/web", **kw):
517 if not http.db_filter([db]):
518 return werkzeug.utils.redirect('/', 303)
519 return login_and_redirect(db, login, key, redirect_url=redirect)
523 '/web/js/<xmlid>/<version>',
524 ], type='http', auth='public')
525 def js_bundle(self, xmlid, version=None, **kw):
527 bundle = AssetsBundle(xmlid)
528 except QWebTemplateNotFound:
529 return request.not_found()
531 response = request.make_response(bundle.js(), [('Content-Type', 'application/javascript')])
532 return make_conditional(response, bundle.last_modified, max_age=BUNDLE_MAXAGE)
536 '/web/css/<xmlid>/<version>',
537 ], type='http', auth='public')
538 def css_bundle(self, xmlid, version=None, **kw):
540 bundle = AssetsBundle(xmlid)
541 except QWebTemplateNotFound:
542 return request.not_found()
544 response = request.make_response(bundle.css(), [('Content-Type', 'text/css')])
545 return make_conditional(response, bundle.last_modified, max_age=BUNDLE_MAXAGE)
547 class WebClient(http.Controller):
549 @http.route('/web/webclient/csslist', type='json', auth="none")
550 def csslist(self, mods=None):
551 return manifest_list('css', mods=mods)
553 @http.route('/web/webclient/jslist', type='json', auth="none")
554 def jslist(self, mods=None):
555 return manifest_list('js', mods=mods)
557 @http.route('/web/webclient/locale/<string:lang>', type='http', auth="none")
558 def load_locale(self, lang):
559 magic_file_finding = [lang.replace("_",'-').lower(), lang.split('_')[0]]
560 addons_path = http.addons_manifest['web']['addons_path']
561 #load momentjs locale
562 momentjs_locale_file = False
564 for code in magic_file_finding:
566 with open(os.path.join(addons_path, 'web', 'static', 'lib', 'moment', 'locale', code + '.js'), 'r') as f:
567 momentjs_locale = f.read()
568 #we found a locale matching so we can exit
573 #return the content of the locale
574 headers = [('Content-Type', 'application/javascript'), ('Cache-Control', 'max-age=%s' % (36000))]
575 return request.make_response(momentjs_locale, headers)
577 @http.route('/web/webclient/qweb', type='http', auth="none")
578 def qweb(self, mods=None, db=None):
579 files = [f[0] for f in manifest_glob('qweb', addons=mods, db=db)]
580 last_modified = get_last_modified(files)
581 if request.httprequest.if_modified_since and request.httprequest.if_modified_since >= last_modified:
582 return werkzeug.wrappers.Response(status=304)
584 content, checksum = concat_xml(files)
586 return make_conditional(
587 request.make_response(content, [('Content-Type', 'text/xml')]),
588 last_modified, checksum)
590 @http.route('/web/webclient/bootstrap_translations', type='json', auth="none")
591 def bootstrap_translations(self, mods):
592 """ Load local translations from *.po files, as a temporary solution
593 until we have established a valid session. This is meant only
594 for translating the login page and db management chrome, using
595 the browser's language. """
596 # For performance reasons we only load a single translation, so for
597 # sub-languages (that should only be partially translated) we load the
598 # main language PO instead - that should be enough for the login screen.
599 lang = request.lang.split('_')[0]
601 translations_per_module = {}
602 for addon_name in mods:
603 if http.addons_manifest[addon_name].get('bootstrap'):
604 addons_path = http.addons_manifest[addon_name]['addons_path']
605 f_name = os.path.join(addons_path, addon_name, "i18n", lang + ".po")
606 if not os.path.exists(f_name):
608 translations_per_module[addon_name] = {'messages': _local_web_translations(f_name)}
610 return {"modules": translations_per_module,
611 "lang_parameters": None}
613 @http.route('/web/webclient/translations', type='json', auth="none")
614 def translations(self, mods=None, lang=None):
615 request.disable_db = False
616 uid = openerp.SUPERUSER_ID
618 m = request.registry.get('ir.module.module')
619 mods = [x['name'] for x in m.search_read(request.cr, uid,
620 [('state','=','installed')], ['name'])]
622 lang = request.context["lang"]
623 res_lang = request.registry.get('res.lang')
624 ids = res_lang.search(request.cr, uid, [("code", "=", lang)])
627 lang_params = res_lang.read(request.cr, uid, ids[0], ["direction", "date_format", "time_format",
628 "grouping", "decimal_point", "thousands_sep"])
630 # Regional languages (ll_CC) must inherit/override their parent lang (ll), but this is
631 # done server-side when the language is loaded, so we only need to load the user's lang.
632 ir_translation = request.registry.get('ir.translation')
633 translations_per_module = {}
634 messages = ir_translation.search_read(request.cr, uid, [('module','in',mods),('lang','=',lang),
635 ('comments','like','openerp-web'),('value','!=',False),
637 ['module','src','value','lang'], order='module')
638 for mod, msg_group in itertools.groupby(messages, key=operator.itemgetter('module')):
639 translations_per_module.setdefault(mod,{'messages':[]})
640 translations_per_module[mod]['messages'].extend({'id': m['src'],
641 'string': m['value']} \
643 return {"modules": translations_per_module,
644 "lang_parameters": lang_params}
646 @http.route('/web/webclient/version_info', type='json', auth="none")
647 def version_info(self):
648 return openerp.service.common.exp_version()
650 @http.route('/web/tests', type='http', auth="none")
651 def index(self, mod=None, **kwargs):
652 return request.render('web.qunit_suite')
654 class Proxy(http.Controller):
656 @http.route('/web/proxy/load', type='json', auth="none")
657 def load(self, path):
658 """ Proxies an HTTP request through a JSON request.
660 It is strongly recommended to not request binary files through this,
661 as the result will be a binary data blob as well.
663 :param path: actual request path
664 :return: file content
666 from werkzeug.test import Client
667 from werkzeug.wrappers import BaseResponse
669 base_url = request.httprequest.base_url
670 return Client(request.httprequest.app, BaseResponse).get(path, base_url=base_url).data
672 class Database(http.Controller):
674 @http.route('/web/database/selector', type='http', auth="none")
675 def selector(self, **kw):
679 return http.local_redirect('/web/database/manager')
680 except openerp.exceptions.AccessDenied:
682 return env.get_template("database_selector.html").render({
684 'debug': request.debug,
687 @http.route('/web/database/manager', type='http', auth="none")
688 def manager(self, **kw):
689 # TODO: migrate the webclient's database manager to server side views
690 request.session.logout()
691 return env.get_template("database_manager.html").render({
692 'modules': simplejson.dumps(module_boot()),
695 @http.route('/web/database/get_list', type='json', auth="none")
697 # TODO change js to avoid calling this method if in monodb mode
699 return http.db_list()
700 except openerp.exceptions.AccessDenied:
706 @http.route('/web/database/create', type='json', auth="none")
707 def create(self, fields):
708 params = dict(map(operator.itemgetter('name', 'value'), fields))
709 db_created = request.session.proxy("db").create_database(
710 params['super_admin_pwd'],
712 bool(params.get('demo_data')),
714 params['create_admin_pwd'])
716 request.session.authenticate(params['db_name'], 'admin', params['create_admin_pwd'])
719 @http.route('/web/database/duplicate', type='json', auth="none")
720 def duplicate(self, fields):
721 params = dict(map(operator.itemgetter('name', 'value'), fields))
723 params['super_admin_pwd'],
724 params['db_original_name'],
728 return request.session.proxy("db").duplicate_database(*duplicate_attrs)
730 @http.route('/web/database/drop', type='json', auth="none")
731 def drop(self, fields):
732 password, db = operator.itemgetter(
733 'drop_pwd', 'drop_db')(
734 dict(map(operator.itemgetter('name', 'value'), fields)))
737 if request.session.proxy("db").drop(password, db):
741 except openerp.exceptions.AccessDenied:
742 return {'error': 'AccessDenied', 'title': 'Drop Database'}
744 return {'error': _('Could not drop database !'), 'title': _('Drop Database')}
746 @http.route('/web/database/backup', type='http', auth="none")
747 def backup(self, backup_db, backup_pwd, token, **kwargs):
749 format = kwargs.get('format')
750 ext = "zip" if format == 'zip' else "dump"
751 db_dump = base64.b64decode(
752 request.session.proxy("db").dump(backup_pwd, backup_db, format))
753 filename = "%(db)s_%(timestamp)s.%(ext)s" % {
755 'timestamp': datetime.datetime.utcnow().strftime(
756 "%Y-%m-%d_%H-%M-%SZ"),
759 return request.make_response(db_dump,
760 [('Content-Type', 'application/octet-stream; charset=binary'),
761 ('Content-Disposition', content_disposition(filename))],
765 return simplejson.dumps([[],[{'error': openerp.tools.ustr(e), 'title': _('Backup Database')}]])
767 @http.route('/web/database/restore', type='http', auth="none")
768 def restore(self, db_file, restore_pwd, new_db, mode):
770 copy = mode == 'copy'
771 data = base64.b64encode(db_file.read())
772 request.session.proxy("db").restore(restore_pwd, new_db, data, copy)
774 except openerp.exceptions.AccessDenied, e:
775 raise Exception("AccessDenied")
777 @http.route('/web/database/change_password', type='json', auth="none")
778 def change_password(self, fields):
779 old_password, new_password = operator.itemgetter(
780 'old_pwd', 'new_pwd')(
781 dict(map(operator.itemgetter('name', 'value'), fields)))
783 return request.session.proxy("db").change_admin_password(old_password, new_password)
784 except openerp.exceptions.AccessDenied:
785 return {'error': 'AccessDenied', 'title': _('Change Password')}
787 return {'error': _('Error, password not changed !'), 'title': _('Change Password')}
789 class Session(http.Controller):
791 def session_info(self):
792 request.session.ensure_valid()
794 "session_id": request.session_id,
795 "uid": request.session.uid,
796 "user_context": request.session.get_context() if request.session.uid else {},
797 "db": request.session.db,
798 "username": request.session.login,
801 @http.route('/web/session/get_session_info', type='json', auth="none")
802 def get_session_info(self):
803 request.uid = request.session.uid
804 request.disable_db = False
805 return self.session_info()
807 @http.route('/web/session/authenticate', type='json', auth="none")
808 def authenticate(self, db, login, password, base_location=None):
809 request.session.authenticate(db, login, password)
811 return self.session_info()
813 @http.route('/web/session/change_password', type='json', auth="user")
814 def change_password(self, fields):
815 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
816 dict(map(operator.itemgetter('name', 'value'), fields)))
817 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
818 return {'error':_('You cannot leave any password empty.'),'title': _('Change Password')}
819 if new_password != confirm_password:
820 return {'error': _('The new password and its confirmation must be identical.'),'title': _('Change Password')}
822 if request.session.model('res.users').change_password(
823 old_password, new_password):
824 return {'new_password':new_password}
826 return {'error': _('The old password you provided is incorrect, your password was not changed.'), 'title': _('Change Password')}
827 return {'error': _('Error, password not changed !'), 'title': _('Change Password')}
829 @http.route('/web/session/get_lang_list', type='json', auth="none")
830 def get_lang_list(self):
832 return request.session.proxy("db").list_lang() or []
834 return {"error": e, "title": _("Languages")}
836 @http.route('/web/session/modules', type='json', auth="user")
838 # return all installed modules. Web client is smart enough to not load a module twice
839 return module_installed()
841 @http.route('/web/session/save_session_action', type='json', auth="user")
842 def save_session_action(self, the_action):
844 This method store an action object in the session object and returns an integer
845 identifying that action. The method get_session_action() can be used to get
848 :param the_action: The action to save in the session.
849 :type the_action: anything
850 :return: A key identifying the saved action.
853 return request.httpsession.save_action(the_action)
855 @http.route('/web/session/get_session_action', type='json', auth="user")
856 def get_session_action(self, key):
858 Gets back a previously saved action. This method can return None if the action
859 was saved since too much time (this case should be handled in a smart way).
861 :param key: The key given by save_session_action()
863 :return: The saved action or None.
866 return request.httpsession.get_action(key)
868 @http.route('/web/session/check', type='json', auth="user")
870 request.session.assert_valid()
873 @http.route('/web/session/destroy', type='json', auth="user")
875 request.session.logout()
877 @http.route('/web/session/logout', type='http', auth="none")
878 def logout(self, redirect='/web'):
879 request.session.logout(keep_db=True)
880 return werkzeug.utils.redirect(redirect, 303)
882 class Menu(http.Controller):
884 @http.route('/web/menu/load_needaction', type='json', auth="user")
885 def load_needaction(self, menu_ids):
886 """ Loads needaction counters for specific menu ids.
888 :return: needaction data
889 :rtype: dict(menu_id: {'needaction_enabled': boolean, 'needaction_counter': int})
891 return request.session.model('ir.ui.menu').get_needaction_data(menu_ids, request.context)
893 class DataSet(http.Controller):
895 @http.route('/web/dataset/search_read', type='json', auth="user")
896 def search_read(self, model, fields=False, offset=0, limit=False, domain=None, sort=None):
897 return self.do_search_read(model, fields, offset, limit, domain, sort)
898 def do_search_read(self, model, fields=False, offset=0, limit=False, domain=None
900 """ Performs a search() followed by a read() (if needed) using the
901 provided search criteria
903 :param str model: the name of the model to search on
904 :param fields: a list of the fields to return in the result records
906 :param int offset: from which index should the results start being returned
907 :param int limit: the maximum number of records to return
908 :param list domain: the search domain for the query
909 :param list sort: sorting directives
910 :returns: A structure (dict) with two keys: ids (all the ids matching
911 the (domain, context) pair) and records (paginated records
912 matching fields selection set)
915 Model = request.session.model(model)
917 records = Model.search_read(domain, fields, offset or 0, limit or False, sort or False,
924 if limit and len(records) == limit:
925 length = Model.search_count(domain, request.context)
927 length = len(records) + (offset or 0)
933 @http.route('/web/dataset/load', type='json', auth="user")
934 def load(self, model, id, fields):
935 m = request.session.model(model)
937 r = m.read([id], False, request.context)
940 return {'value': value}
942 def call_common(self, model, method, args, domain_id=None, context_id=None):
943 return self._call_kw(model, method, args, {})
945 def _call_kw(self, model, method, args, kwargs):
946 # Temporary implements future display_name special field for model#read()
947 if method in ('read', 'search_read') and kwargs.get('context', {}).get('future_display_name'):
948 if 'display_name' in args[1]:
950 names = dict(request.session.model(model).name_get(args[0], **kwargs))
952 names = dict(request.session.model(model).name_search('', args[0], **kwargs))
953 args[1].remove('display_name')
954 records = getattr(request.session.model(model), method)(*args, **kwargs)
955 for record in records:
956 record['display_name'] = \
957 names.get(record['id']) or "{0}#{1}".format(model, (record['id']))
960 if method.startswith('_'):
961 raise Exception("Access Denied: Underscore prefixed methods cannot be remotely called")
963 return getattr(request.registry.get(model), method)(request.cr, request.uid, *args, **kwargs)
965 @http.route('/web/dataset/call', type='json', auth="user")
966 def call(self, model, method, args, domain_id=None, context_id=None):
967 return self._call_kw(model, method, args, {})
969 @http.route(['/web/dataset/call_kw', '/web/dataset/call_kw/<path:path>'], type='json', auth="user")
970 def call_kw(self, model, method, args, kwargs, path=None):
971 return self._call_kw(model, method, args, kwargs)
973 @http.route('/web/dataset/call_button', type='json', auth="user")
974 def call_button(self, model, method, args, domain_id=None, context_id=None):
975 action = self._call_kw(model, method, args, {})
976 if isinstance(action, dict) and action.get('type') != '':
977 return clean_action(action)
980 @http.route('/web/dataset/exec_workflow', type='json', auth="user")
981 def exec_workflow(self, model, id, signal):
982 return request.session.exec_workflow(model, id, signal)
984 @http.route('/web/dataset/resequence', type='json', auth="user")
985 def resequence(self, model, ids, field='sequence', offset=0):
986 """ Re-sequences a number of records in the model, by their ids
988 The re-sequencing starts at the first model of ``ids``, the sequence
989 number is incremented by one after each record and starts at ``offset``
991 :param ids: identifiers of the records to resequence, in the new sequence order
993 :param str field: field used for sequence specification, defaults to
995 :param int offset: sequence number for first record in ``ids``, allows
996 starting the resequencing from an arbitrary number,
999 m = request.session.model(model)
1000 if not m.fields_get([field]):
1002 # python 2.6 has no start parameter
1003 for i, id in enumerate(ids):
1004 m.write(id, { field: i + offset })
1007 class View(http.Controller):
1009 @http.route('/web/view/add_custom', type='json', auth="user")
1010 def add_custom(self, view_id, arch):
1011 CustomView = request.session.model('ir.ui.view.custom')
1013 'user_id': request.session.uid,
1017 return {'result': True}
1019 @http.route('/web/view/undo_custom', type='json', auth="user")
1020 def undo_custom(self, view_id, reset=False):
1021 CustomView = request.session.model('ir.ui.view.custom')
1022 vcustom = CustomView.search([('user_id', '=', request.session.uid), ('ref_id' ,'=', view_id)],
1023 0, False, False, request.context)
1026 CustomView.unlink(vcustom, request.context)
1028 CustomView.unlink([vcustom[0]], request.context)
1029 return {'result': True}
1030 return {'result': False}
1032 class TreeView(View):
1034 @http.route('/web/treeview/action', type='json', auth="user")
1035 def action(self, model, id):
1036 return load_actions_from_ir_values(
1037 'action', 'tree_but_open',[(model, id)],
1040 class Binary(http.Controller):
1042 @http.route('/web/binary/image', type='http', auth="public")
1043 def image(self, model, id, field, **kw):
1044 last_update = '__last_update'
1045 Model = request.registry[model]
1046 cr, uid, context = request.cr, request.uid, request.context
1047 headers = [('Content-Type', 'image/png')]
1048 etag = request.httprequest.headers.get('If-None-Match')
1049 hashed_session = hashlib.md5(request.session_id).hexdigest()
1050 retag = hashed_session
1051 id = None if not id else simplejson.loads(id)
1052 if type(id) is list:
1056 if not id and hashed_session == etag:
1057 return werkzeug.wrappers.Response(status=304)
1059 date = Model.read(cr, uid, [id], [last_update], context)[0].get(last_update)
1060 if hashlib.md5(date).hexdigest() == etag:
1061 return werkzeug.wrappers.Response(status=304)
1064 res = Model.default_get(cr, uid, [field], context).get(field)
1067 res = Model.read(cr, uid, [id], [last_update, field], context)[0]
1068 retag = hashlib.md5(res.get(last_update)).hexdigest()
1069 image_base64 = res.get(field)
1071 if kw.get('resize'):
1072 resize = kw.get('resize').split(',')
1073 if len(resize) == 2 and int(resize[0]) and int(resize[1]):
1074 width = int(resize[0])
1075 height = int(resize[1])
1076 # resize maximum 500*500
1077 if width > 500: width = 500
1078 if height > 500: height = 500
1079 image_base64 = openerp.tools.image_resize_image(base64_source=image_base64, size=(width, height), encoding='base64', filetype='PNG')
1081 image_data = base64.b64decode(image_base64)
1084 image_data = self.placeholder()
1085 headers.append(('ETag', retag))
1086 headers.append(('Content-Length', len(image_data)))
1088 ncache = int(kw.get('cache'))
1089 headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1092 return request.make_response(image_data, headers)
1094 def placeholder(self, image='placeholder.png'):
1095 addons_path = http.addons_manifest['web']['addons_path']
1096 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', image), 'rb').read()
1098 @http.route('/web/binary/saveas', type='http', auth="public")
1099 @serialize_exception
1100 def saveas(self, model, field, id=None, filename_field=None, **kw):
1101 """ Download link for files stored as binary fields.
1103 If the ``id`` parameter is omitted, fetches the default value for the
1104 binary field (via ``default_get``), otherwise fetches the field for
1105 that precise record.
1107 :param str model: name of the model to fetch the binary from
1108 :param str field: binary field
1109 :param str id: id of the record from which to fetch the binary
1110 :param str filename_field: field holding the file's name, if any
1111 :returns: :class:`werkzeug.wrappers.Response`
1113 Model = request.registry[model]
1114 cr, uid, context = request.cr, request.uid, request.context
1117 fields.append(filename_field)
1119 res = Model.read(cr, uid, [int(id)], fields, context)[0]
1121 res = Model.default_get(cr, uid, fields, context)
1122 filecontent = base64.b64decode(res.get(field, ''))
1124 return request.not_found()
1126 filename = '%s_%s' % (model.replace('.', '_'), id)
1128 filename = res.get(filename_field, '') or filename
1129 return request.make_response(filecontent,
1130 [('Content-Type', 'application/octet-stream'),
1131 ('Content-Disposition', content_disposition(filename))])
1133 @http.route('/web/binary/saveas_ajax', type='http', auth="public")
1134 @serialize_exception
1135 def saveas_ajax(self, data, token):
1136 jdata = simplejson.loads(data)
1137 model = jdata['model']
1138 field = jdata['field']
1139 data = jdata['data']
1140 id = jdata.get('id', None)
1141 filename_field = jdata.get('filename_field', None)
1142 context = jdata.get('context', {})
1144 Model = request.session.model(model)
1147 fields.append(filename_field)
1149 res = { field: data }
1151 res = Model.read([int(id)], fields, context)[0]
1153 res = Model.default_get(fields, context)
1154 filecontent = base64.b64decode(res.get(field, ''))
1156 raise ValueError(_("No content found for field '%s' on '%s:%s'") %
1159 filename = '%s_%s' % (model.replace('.', '_'), id)
1161 filename = res.get(filename_field, '') or filename
1162 return request.make_response(filecontent,
1163 headers=[('Content-Type', 'application/octet-stream'),
1164 ('Content-Disposition', content_disposition(filename))],
1165 cookies={'fileToken': token})
1167 @http.route('/web/binary/upload', type='http', auth="user")
1168 @serialize_exception
1169 def upload(self, callback, ufile):
1170 # TODO: might be useful to have a configuration flag for max-length file uploads
1171 out = """<script language="javascript" type="text/javascript">
1172 var win = window.top.window;
1173 win.jQuery(win).trigger(%s, %s);
1177 args = [len(data), ufile.filename,
1178 ufile.content_type, base64.b64encode(data)]
1179 except Exception, e:
1180 args = [False, e.message]
1181 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1183 @http.route('/web/binary/upload_attachment', type='http', auth="user")
1184 @serialize_exception
1185 def upload_attachment(self, callback, model, id, ufile):
1186 Model = request.session.model('ir.attachment')
1187 out = """<script language="javascript" type="text/javascript">
1188 var win = window.top.window;
1189 win.jQuery(win).trigger(%s, %s);
1192 attachment_id = Model.create({
1193 'name': ufile.filename,
1194 'datas': base64.encodestring(ufile.read()),
1195 'datas_fname': ufile.filename,
1200 'filename': ufile.filename,
1204 args = {'error': "Something horrible happened"}
1205 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1208 '/web/binary/company_logo',
1211 ], type='http', auth="none", cors="*")
1212 def company_logo(self, dbname=None, **kw):
1213 imgname = 'logo.png'
1214 placeholder = functools.partial(get_module_resource, 'web', 'static', 'src', 'img')
1216 if request.session.db:
1217 dbname = request.session.db
1218 uid = request.session.uid
1219 elif dbname is None:
1220 dbname = db_monodb()
1223 uid = openerp.SUPERUSER_ID
1226 response = http.send_file(placeholder(imgname))
1229 # create an empty registry
1230 registry = openerp.modules.registry.Registry(dbname)
1231 with registry.cursor() as cr:
1232 cr.execute("""SELECT c.logo_web, c.write_date
1234 LEFT JOIN res_company c
1235 ON c.id = u.company_id
1240 image_data = StringIO(str(row[0]).decode('base64'))
1241 response = http.send_file(image_data, filename=imgname, mtime=row[1])
1243 response = http.send_file(placeholder('nologo.png'))
1245 response = http.send_file(placeholder(imgname))
1249 class Action(http.Controller):
1251 @http.route('/web/action/load', type='json', auth="user")
1252 def load(self, action_id, do_not_eval=False, additional_context=None):
1253 Actions = request.session.model('ir.actions.actions')
1256 action_id = int(action_id)
1259 module, xmlid = action_id.split('.', 1)
1260 model, action_id = request.session.model('ir.model.data').get_object_reference(module, xmlid)
1261 assert model.startswith('ir.actions.')
1263 action_id = 0 # force failed read
1265 base_action = Actions.read([action_id], ['type'], request.context)
1267 ctx = request.context
1268 action_type = base_action[0]['type']
1269 if action_type == 'ir.actions.report.xml':
1270 ctx.update({'bin_size': True})
1271 if additional_context:
1272 ctx.update(additional_context)
1273 action = request.session.model(action_type).read([action_id], False, ctx)
1275 value = clean_action(action[0])
1278 @http.route('/web/action/run', type='json', auth="user")
1279 def run(self, action_id):
1280 return_action = request.session.model('ir.actions.server').run(
1281 [action_id], request.context)
1283 return clean_action(return_action)
1287 class Export(http.Controller):
1289 @http.route('/web/export/formats', type='json', auth="user")
1291 """ Returns all valid export formats
1293 :returns: for each export format, a pair of identifier and printable name
1294 :rtype: [(str, str)]
1297 {'tag': 'csv', 'label': 'CSV'},
1298 {'tag': 'xls', 'label': 'Excel', 'error': None if xlwt else "XLWT required"},
1301 def fields_get(self, model):
1302 Model = request.session.model(model)
1303 fields = Model.fields_get(False, request.context)
1306 @http.route('/web/export/get_fields', type='json', auth="user")
1307 def get_fields(self, model, prefix='', parent_name= '',
1308 import_compat=True, parent_field_type=None,
1311 if import_compat and parent_field_type == "many2one":
1314 fields = self.fields_get(model)
1317 fields.pop('id', None)
1319 fields['.id'] = fields.pop('id', {'string': 'ID'})
1321 fields_sequence = sorted(fields.iteritems(),
1322 key=lambda field: openerp.tools.ustr(field[1].get('string', '')))
1325 for field_name, field in fields_sequence:
1327 if exclude and field_name in exclude:
1329 if field.get('readonly'):
1330 # If none of the field's states unsets readonly, skip the field
1331 if all(dict(attrs).get('readonly', True)
1332 for attrs in field.get('states', {}).values()):
1334 if not field.get('exportable', True):
1337 id = prefix + (prefix and '/'or '') + field_name
1338 name = parent_name + (parent_name and '/' or '') + field['string']
1339 record = {'id': id, 'string': name,
1340 'value': id, 'children': False,
1341 'field_type': field.get('type'),
1342 'required': field.get('required'),
1343 'relation_field': field.get('relation_field')}
1344 records.append(record)
1346 if len(name.split('/')) < 3 and 'relation' in field:
1347 ref = field.pop('relation')
1348 record['value'] += '/id'
1349 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1351 if not import_compat or field['type'] == 'one2many':
1352 # m2m field in import_compat is childless
1353 record['children'] = True
1357 @http.route('/web/export/namelist', type='json', auth="user")
1358 def namelist(self, model, export_id):
1359 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1360 export = request.session.model("ir.exports").read([export_id])[0]
1361 export_fields_list = request.session.model("ir.exports.line").read(
1362 export['export_fields'])
1364 fields_data = self.fields_info(
1365 model, map(operator.itemgetter('name'), export_fields_list))
1368 {'name': field['name'], 'label': fields_data[field['name']]}
1369 for field in export_fields_list
1372 def fields_info(self, model, export_fields):
1374 fields = self.fields_get(model)
1375 if ".id" in export_fields:
1376 fields['.id'] = fields.pop('id', {'string': 'ID'})
1378 # To make fields retrieval more efficient, fetch all sub-fields of a
1379 # given field at the same time. Because the order in the export list is
1380 # arbitrary, this requires ordering all sub-fields of a given field
1381 # together so they can be fetched at the same time
1383 # Works the following way:
1384 # * sort the list of fields to export, the default sorting order will
1385 # put the field itself (if present, for xmlid) and all of its
1386 # sub-fields right after it
1387 # * then, group on: the first field of the path (which is the same for
1388 # a field and for its subfields and the length of splitting on the
1389 # first '/', which basically means grouping the field on one side and
1390 # all of the subfields on the other. This way, we have the field (for
1391 # the xmlid) with length 1, and all of the subfields with the same
1392 # base but a length "flag" of 2
1393 # * if we have a normal field (length 1), just add it to the info
1394 # mapping (with its string) as-is
1395 # * otherwise, recursively call fields_info via graft_subfields.
1396 # all graft_subfields does is take the result of fields_info (on the
1397 # field's model) and prepend the current base (current field), which
1398 # rebuilds the whole sub-tree for the field
1400 # result: because we're not fetching the fields_get for half the
1401 # database models, fetching a namelist with a dozen fields (including
1402 # relational data) falls from ~6s to ~300ms (on the leads model).
1403 # export lists with no sub-fields (e.g. import_compatible lists with
1404 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1405 # there's a single fields_get to execute)
1406 for (base, length), subfields in itertools.groupby(
1407 sorted(export_fields),
1408 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1409 subfields = list(subfields)
1411 # subfields is a seq of $base/*rest, and not loaded yet
1412 info.update(self.graft_subfields(
1413 fields[base]['relation'], base, fields[base]['string'],
1416 elif base in fields:
1417 info[base] = fields[base]['string']
1421 def graft_subfields(self, model, prefix, prefix_string, fields):
1422 export_fields = [field.split('/', 1)[1] for field in fields]
1424 (prefix + '/' + k, prefix_string + '/' + v)
1425 for k, v in self.fields_info(model, export_fields).iteritems())
1427 class ExportFormat(object):
1431 def content_type(self):
1432 """ Provides the format's content type """
1433 raise NotImplementedError()
1435 def filename(self, base):
1436 """ Creates a valid filename for the format (with extension) from the
1437 provided base name (exension-less)
1439 raise NotImplementedError()
1441 def from_data(self, fields, rows):
1442 """ Conversion method from OpenERP's export data to whatever the
1443 current export class outputs
1445 :params list fields: a list of fields to export
1446 :params list rows: a list of records to export
1450 raise NotImplementedError()
1452 def base(self, data, token):
1453 params = simplejson.loads(data)
1454 model, fields, ids, domain, import_compat = \
1455 operator.itemgetter('model', 'fields', 'ids', 'domain',
1459 Model = request.session.model(model)
1460 context = dict(request.context or {}, **params.get('context', {}))
1461 ids = ids or Model.search(domain, 0, False, False, context)
1463 field_names = map(operator.itemgetter('name'), fields)
1464 import_data = Model.export_data(ids, field_names, self.raw_data, context=context).get('datas',[])
1467 columns_headers = field_names
1469 columns_headers = [val['label'].strip() for val in fields]
1472 return request.make_response(self.from_data(columns_headers, import_data),
1473 headers=[('Content-Disposition',
1474 content_disposition(self.filename(model))),
1475 ('Content-Type', self.content_type)],
1476 cookies={'fileToken': token})
1478 class CSVExport(ExportFormat, http.Controller):
1480 @http.route('/web/export/csv', type='http', auth="user")
1481 @serialize_exception
1482 def index(self, data, token):
1483 return self.base(data, token)
1486 def content_type(self):
1487 return 'text/csv;charset=utf8'
1489 def filename(self, base):
1490 return base + '.csv'
1492 def from_data(self, fields, rows):
1494 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1496 writer.writerow([name.encode('utf-8') for name in fields])
1501 if isinstance(d, basestring):
1502 d = d.replace('\n',' ').replace('\t',' ')
1504 d = d.encode('utf-8')
1505 except UnicodeError:
1507 if d is False: d = None
1509 writer.writerow(row)
1516 class ExcelExport(ExportFormat, http.Controller):
1517 # Excel needs raw data to correctly handle numbers and date values
1520 @http.route('/web/export/xls', type='http', auth="user")
1521 @serialize_exception
1522 def index(self, data, token):
1523 return self.base(data, token)
1526 def content_type(self):
1527 return 'application/vnd.ms-excel'
1529 def filename(self, base):
1530 return base + '.xls'
1532 def from_data(self, fields, rows):
1533 workbook = xlwt.Workbook()
1534 worksheet = workbook.add_sheet('Sheet 1')
1536 for i, fieldname in enumerate(fields):
1537 worksheet.write(0, i, fieldname)
1538 worksheet.col(i).width = 8000 # around 220 pixels
1540 base_style = xlwt.easyxf('align: wrap yes')
1541 date_style = xlwt.easyxf('align: wrap yes', num_format_str='YYYY-MM-DD')
1542 datetime_style = xlwt.easyxf('align: wrap yes', num_format_str='YYYY-MM-DD HH:mm:SS')
1544 for row_index, row in enumerate(rows):
1545 for cell_index, cell_value in enumerate(row):
1546 cell_style = base_style
1547 if isinstance(cell_value, basestring):
1548 cell_value = re.sub("\r", " ", cell_value)
1549 elif isinstance(cell_value, datetime.datetime):
1550 cell_style = datetime_style
1551 elif isinstance(cell_value, datetime.date):
1552 cell_style = date_style
1553 worksheet.write(row_index + 1, cell_index, cell_value, cell_style)
1562 class Reports(http.Controller):
1563 POLLING_DELAY = 0.25
1565 'doc': 'application/vnd.ms-word',
1566 'html': 'text/html',
1567 'odt': 'application/vnd.oasis.opendocument.text',
1568 'pdf': 'application/pdf',
1569 'sxw': 'application/vnd.sun.xml.writer',
1570 'xls': 'application/vnd.ms-excel',
1573 @http.route('/web/report', type='http', auth="user")
1574 @serialize_exception
1575 def index(self, action, token):
1576 action = simplejson.loads(action)
1577 report_srv = request.session.proxy("report")
1578 context = dict(request.context)
1579 context.update(action["context"])
1582 report_ids = context.get("active_ids", None)
1583 if 'report_type' in action:
1584 report_data['report_type'] = action['report_type']
1585 if 'datas' in action:
1586 if 'ids' in action['datas']:
1587 report_ids = action['datas'].pop('ids')
1588 report_data.update(action['datas'])
1590 report_id = report_srv.report(
1591 request.session.db, request.session.uid, request.session.password,
1592 action["report_name"], report_ids,
1593 report_data, context)
1595 report_struct = None
1597 report_struct = report_srv.report_get(
1598 request.session.db, request.session.uid, request.session.password, report_id)
1599 if report_struct["state"]:
1602 time.sleep(self.POLLING_DELAY)
1604 report = base64.b64decode(report_struct['result'])
1605 if report_struct.get('code') == 'zlib':
1606 report = zlib.decompress(report)
1607 report_mimetype = self.TYPES_MAPPING.get(
1608 report_struct['format'], 'octet-stream')
1609 file_name = action.get('name', 'report')
1610 if 'name' not in action:
1611 reports = request.session.model('ir.actions.report.xml')
1612 res_id = reports.search([('report_name', '=', action['report_name']),],
1613 0, False, False, context)
1615 file_name = reports.read(res_id[0], ['name'], context)['name']
1617 file_name = action['report_name']
1618 file_name = '%s.%s' % (file_name, report_struct['format'])
1620 ('Content-Disposition', content_disposition(file_name)),
1621 ('Content-Type', report_mimetype),
1622 ('Content-Length', len(report))]
1623 if action.get('pdf_viewer'):
1625 return request.make_response(report,
1627 cookies={'fileToken': token})
1629 class Apps(http.Controller):
1630 @http.route('/apps/<app>', auth='user')
1631 def get_app_url(self, req, app):
1632 act_window_obj = request.session.model('ir.actions.act_window')
1633 ir_model_data = request.session.model('ir.model.data')
1635 action_id = ir_model_data.get_object_reference('base', 'open_module_tree')[1]
1636 action = act_window_obj.read(action_id, ['name', 'type', 'res_model', 'view_mode', 'view_type', 'context', 'views', 'domain'])
1637 action['target'] = 'current'
1641 app_id = ir_model_data.get_object_reference('base', 'module_%s' % app)[1]
1645 if action and app_id:
1646 action['res_id'] = app_id
1647 action['view_mode'] = 'form'
1648 action['views'] = [(False, u'form')]
1650 sakey = Session().save_session_action(action)
1651 debug = '?debug' if req.debug else ''
1652 return werkzeug.utils.redirect('/web{0}#sa={1}'.format(debug, sakey))
1656 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: