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.tools.translate import _
39 #----------------------------------------------------------
41 #----------------------------------------------------------
44 """ Minify js with a clever regex.
45 Taken from http://opensource.perlig.de/rjsmin
46 Apache License, Version 2.0 """
48 """ Substitution callback """
49 groups = match.groups()
55 (groups[4] and '\n') or
56 (groups[5] and ' ') or
57 (groups[6] and ' ') or
58 (groups[7] and ' ') or
63 r'([^\047"/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]|\r?'
64 r'\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|'
65 r'\r)[^"\\\r\n]*)*"))[^\047"/\000-\040]*)|(?:(?<=[(,=:\[!&|?{};\r\n]'
66 r')(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/'
67 r'))*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*'
68 r'(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*'
69 r'))|(?:(?<=[\000-#%-,./:-@\[-^`{-~-]return)(?:[\000-\011\013\014\01'
70 r'6-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*((?:/(?![\r\n/*])[^/'
71 r'\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]'
72 r'*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*))|(?<=[^\000-!#%&(*,./'
73 r':-@\[\\^`{|~])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/'
74 r'*][^*]*\*+)*/))*(?:((?:(?://[^\r\n]*)?[\r\n]))(?:[\000-\011\013\01'
75 r'4\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040"#'
76 r'%-\047)*,./:-@\\-^`|-~])|(?<=[^\000-#%-,./:-@\[-^`{-~-])((?:[\000-'
77 r'\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=[^'
78 r'\000-#%-,./:-@\[-^`{-~-])|(?<=\+)((?:[\000-\011\013\014\016-\040]|'
79 r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=\+)|(?<=-)((?:[\000-\011\0'
80 r'13\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=-)|(?:[\0'
81 r'00-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))+|(?:'
82 r'(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*'
83 r']*\*+(?:[^/*][^*]*\*+)*/))*)+', subber, '\n%s\n' % script
88 proxy = req.session.proxy("db")
90 h = req.httprequest.environ['HTTP_HOST'].split(':')[0]
92 r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
93 dbs = [i for i in dbs if re.match(r, i)]
96 def db_monodb_redirect(req):
100 # 1 try the db in the url
101 db_url = req.params.get('db')
103 return (db_url, False)
107 except xmlrpclib.Fault:
108 # ignore access denied
111 # 2 use the database from the cookie if it's listable and still listed
112 cookie_db = req.httprequest.cookies.get('last_used_database')
120 # redirect to the chosen db if multiple are available
121 if db and len(dbs) > 1:
122 query = dict(urlparse.parse_qsl(req.httprequest.query_string, keep_blank_values=True))
123 query.update({ 'db': db })
124 redirect = req.httprequest.path + '?' + urllib.urlencode(query)
125 return (db, redirect)
128 # if only one db exists, return it else return False
129 return db_monodb_redirect(req)[0]
131 def redirect_with_hash(req, url, code=303):
132 if req.httprequest.user_agent.browser == 'msie':
134 version = float(req.httprequest.user_agent.version)
136 return "<html><head><script>window.location = '%s#' + location.hash;</script></head></html>" % url
139 return werkzeug.utils.redirect(url, code)
141 def module_topological_sort(modules):
142 """ Return a list of module names sorted so that their dependencies of the
143 modules are listed before the module itself
145 modules is a dict of {module_name: dependencies}
147 :param modules: modules to sort
152 dependencies = set(itertools.chain.from_iterable(modules.itervalues()))
153 # incoming edge: dependency on other module (if a depends on b, a has an
154 # incoming edge from b, aka there's an edge from b to a)
155 # outgoing edge: other module depending on this one
157 # [Tarjan 1976], http://en.wikipedia.org/wiki/Topological_sorting#Algorithms
158 #L ← Empty list that will contain the sorted nodes
160 #S ← Set of all nodes with no outgoing edges (modules on which no other
162 S = set(module for module in modules if module not in dependencies)
165 #function visit(node n)
167 #if n has not been visited yet then
171 #change: n not web module, can not be resolved, ignore
172 if n not in modules: return
173 #for each node m with an edge from m to n do (dependencies of n)
179 #for each node n in S do
185 def module_installed(req):
186 # Candidates module the current heuristic is the /static dir
187 loadable = openerpweb.addons_manifest.keys()
190 # Retrieve database installed modules
191 # TODO The following code should move to ir.module.module.list_installed_modules()
192 Modules = req.session.model('ir.module.module')
193 domain = [('state','=','installed'), ('name','in', loadable)]
194 for module in Modules.search_read(domain, ['name', 'dependencies_id']):
195 modules[module['name']] = []
196 deps = module.get('dependencies_id')
198 deps_read = req.session.model('ir.module.module.dependency').read(deps, ['name'])
199 dependencies = [i['name'] for i in deps_read]
200 modules[module['name']] = dependencies
202 sorted_modules = module_topological_sort(modules)
203 return sorted_modules
205 def module_installed_bypass_session(dbname):
206 loadable = openerpweb.addons_manifest.keys()
209 registry = openerp.modules.registry.RegistryManager.get(dbname)
210 with registry.cursor() as cr:
211 m = registry.get('ir.module.module')
212 # TODO The following code should move to ir.module.module.list_installed_modules()
213 domain = [('state','=','installed'), ('name','in', loadable)]
214 ids = m.search(cr, 1, [('state','=','installed'), ('name','in', loadable)])
215 for module in m.read(cr, 1, ids, ['name', 'dependencies_id']):
216 modules[module['name']] = []
217 deps = module.get('dependencies_id')
219 deps_read = registry.get('ir.module.module.dependency').read(cr, 1, deps, ['name'])
220 dependencies = [i['name'] for i in deps_read]
221 modules[module['name']] = dependencies
224 sorted_modules = module_topological_sort(modules)
225 return sorted_modules
227 def module_boot(req, db=None):
228 server_wide_modules = openerp.conf.server_wide_modules or ['web']
231 for i in server_wide_modules:
232 if i in openerpweb.addons_manifest:
234 monodb = db or db_monodb(req)
236 dbside = module_installed_bypass_session(monodb)
237 dbside = [i for i in dbside if i not in serverside]
238 addons = serverside + dbside
241 def concat_xml(file_list):
242 """Concatenate xml files
244 :param list(str) file_list: list of files to check
245 :returns: (concatenation_result, checksum)
248 checksum = hashlib.new('sha1')
250 return '', checksum.hexdigest()
253 for fname in file_list:
254 with open(fname, 'rb') as fp:
256 checksum.update(contents)
258 xml = ElementTree.parse(fp).getroot()
261 root = ElementTree.Element(xml.tag)
262 #elif root.tag != xml.tag:
263 # raise ValueError("Root tags missmatch: %r != %r" % (root.tag, xml.tag))
265 for child in xml.getchildren():
267 return ElementTree.tostring(root, 'utf-8'), checksum.hexdigest()
269 def concat_files(file_list, reader=None, intersperse=""):
270 """ Concatenates contents of all provided files
272 :param list(str) file_list: list of files to check
273 :param function reader: reading procedure for each file
274 :param str intersperse: string to intersperse between file contents
275 :returns: (concatenation_result, checksum)
278 checksum = hashlib.new('sha1')
280 return '', checksum.hexdigest()
284 with open(f, 'rb') as fp:
288 for fname in file_list:
289 contents = reader(fname)
290 checksum.update(contents)
291 files_content.append(contents)
293 files_concat = intersperse.join(files_content)
294 return files_concat, checksum.hexdigest()
298 def concat_js(file_list):
299 content, checksum = concat_files(file_list, intersperse=';')
300 if checksum in concat_js_cache:
301 content = concat_js_cache[checksum]
303 content = rjsmin(content)
304 concat_js_cache[checksum] = content
305 return content, checksum
308 """convert FS path into web path"""
309 return '/'.join(path.split(os.path.sep))
311 def manifest_glob(req, extension, addons=None, db=None):
313 addons = module_boot(req, db=db)
315 addons = addons.split(',')
318 manifest = openerpweb.addons_manifest.get(addon, None)
321 # ensure does not ends with /
322 addons_path = os.path.join(manifest['addons_path'], '')[:-1]
323 globlist = manifest.get(extension, [])
324 for pattern in globlist:
325 for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
326 r.append((path, fs2web(path[len(addons_path):])))
329 def manifest_list(req, extension, mods=None, db=None):
330 """ list ressources to load specifying either:
331 mods: a comma separated string listing modules
332 db: a database name (return all installed modules in that database)
335 path = '/web/webclient/' + extension
337 path += '?' + urllib.urlencode({'mods': mods})
339 path += '?' + urllib.urlencode({'db': db})
341 files = manifest_glob(req, extension, addons=mods, db=db)
342 return [wp for _fp, wp in files]
344 def get_last_modified(files):
345 """ Returns the modification time of the most recently modified
348 :param list(str) files: names of files to check
349 :return: most recent modification time amongst the fileset
350 :rtype: datetime.datetime
354 return max(datetime.datetime.fromtimestamp(os.path.getmtime(f))
356 return datetime.datetime(1970, 1, 1)
358 def make_conditional(req, response, last_modified=None, etag=None):
359 """ Makes the provided response conditional based upon the request,
360 and mandates revalidation from clients
362 Uses Werkzeug's own :meth:`ETagResponseMixin.make_conditional`, after
363 setting ``last_modified`` and ``etag`` correctly on the response object
365 :param req: OpenERP request
366 :type req: web.common.http.WebRequest
367 :param response: Werkzeug response
368 :type response: werkzeug.wrappers.Response
369 :param datetime.datetime last_modified: last modification date of the response content
370 :param str etag: some sort of checksum of the content (deep etag)
371 :return: the response object provided
372 :rtype: werkzeug.wrappers.Response
374 response.cache_control.must_revalidate = True
375 response.cache_control.max_age = 0
377 response.last_modified = last_modified
379 response.set_etag(etag)
380 return response.make_conditional(req.httprequest)
382 def login_and_redirect(req, db, login, key, redirect_url='/'):
383 wsgienv = req.httprequest.environ
385 base_location=req.httprequest.url_root.rstrip('/'),
386 HTTP_HOST=wsgienv['HTTP_HOST'],
387 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
389 req.session.authenticate(db, login, key, env)
390 return set_cookie_and_redirect(req, redirect_url)
392 def set_cookie_and_redirect(req, redirect_url):
393 redirect = werkzeug.utils.redirect(redirect_url, 303)
394 redirect.autocorrect_location_header = False
395 cookie_val = urllib2.quote(simplejson.dumps(req.session_id))
396 redirect.set_cookie('instance0|session_id', cookie_val)
399 def load_actions_from_ir_values(req, key, key2, models, meta):
400 Values = req.session.model('ir.values')
401 actions = Values.get(key, key2, models, meta, req.context)
403 return [(id, name, clean_action(req, action))
404 for id, name, action in actions]
406 def clean_action(req, action):
407 action.setdefault('flags', {})
408 action_type = action.setdefault('type', 'ir.actions.act_window_close')
409 if action_type == 'ir.actions.act_window':
410 return fix_view_modes(action)
413 # I think generate_views,fix_view_modes should go into js ActionManager
414 def generate_views(action):
416 While the server generates a sequence called "views" computing dependencies
417 between a bunch of stuff for views coming directly from the database
418 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
419 to return custom view dictionaries generated on the fly.
421 In that case, there is no ``views`` key available on the action.
423 Since the web client relies on ``action['views']``, generate it here from
424 ``view_mode`` and ``view_id``.
426 Currently handles two different cases:
428 * no view_id, multiple view_mode
429 * single view_id, single view_mode
431 :param dict action: action descriptor dictionary to generate a views key for
433 view_id = action.get('view_id') or False
434 if isinstance(view_id, (list, tuple)):
437 # providing at least one view mode is a requirement, not an option
438 view_modes = action['view_mode'].split(',')
440 if len(view_modes) > 1:
442 raise ValueError('Non-db action dictionaries should provide '
443 'either multiple view modes or a single view '
444 'mode and an optional view id.\n\n Got view '
445 'modes %r and view id %r for action %r' % (
446 view_modes, view_id, action))
447 action['views'] = [(False, mode) for mode in view_modes]
449 action['views'] = [(view_id, view_modes[0])]
451 def fix_view_modes(action):
452 """ For historical reasons, OpenERP has weird dealings in relation to
453 view_mode and the view_type attribute (on window actions):
455 * one of the view modes is ``tree``, which stands for both list views
457 * the choice is made by checking ``view_type``, which is either
458 ``form`` for a list view or ``tree`` for an actual tree view
460 This methods simply folds the view_type into view_mode by adding a
461 new view mode ``list`` which is the result of the ``tree`` view_mode
462 in conjunction with the ``form`` view_type.
464 TODO: this should go into the doc, some kind of "peculiarities" section
466 :param dict action: an action descriptor
467 :returns: nothing, the action is modified in place
469 if not action.get('views'):
470 generate_views(action)
472 if action.pop('view_type', 'form') != 'form':
475 if 'view_mode' in action:
476 action['view_mode'] = ','.join(
477 mode if mode != 'tree' else 'list'
478 for mode in action['view_mode'].split(','))
480 [id, mode if mode != 'tree' else 'list']
481 for id, mode in action['views']
486 def _local_web_translations(trans_file):
489 with open(trans_file) as t_file:
490 po = babel.messages.pofile.read_po(t_file)
494 if x.id and x.string and "openerp-web" in x.auto_comments:
495 messages.append({'id': x.id, 'string': x.string})
498 def xml2json_from_elementtree(el, preserve_whitespaces=False):
500 Simple and straightforward XML-to-JSON converter in Python
502 http://code.google.com/p/xml2json-direct/
506 ns, name = el.tag.rsplit("}", 1)
508 res["namespace"] = ns[1:]
512 for k, v in el.items():
515 if el.text and (preserve_whitespaces or el.text.strip() != ''):
518 kids.append(xml2json_from_elementtree(kid, preserve_whitespaces))
519 if kid.tail and (preserve_whitespaces or kid.tail.strip() != ''):
520 kids.append(kid.tail)
521 res["children"] = kids
524 def content_disposition(filename, req):
525 filename = filename.encode('utf8')
526 escaped = urllib2.quote(filename)
527 browser = req.httprequest.user_agent.browser
528 version = int((req.httprequest.user_agent.version or '0').split('.')[0])
529 if browser == 'msie' and version < 9:
530 return "attachment; filename=%s" % escaped
531 elif browser == 'safari':
532 return "attachment; filename=%s" % filename
534 return "attachment; filename*=UTF-8''%s" % escaped
537 #----------------------------------------------------------
538 # OpenERP Web web Controllers
539 #----------------------------------------------------------
541 html_template = """<!DOCTYPE html>
542 <html style="height: 100%%">
544 <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
545 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
546 <title>OpenERP</title>
547 <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
548 <link rel="stylesheet" href="/web/static/src/css/full.css" />
551 <script type="text/javascript">
553 var s = new openerp.init(%(modules)s);
560 <script src="//ajax.googleapis.com/ajax/libs/chrome-frame/1/CFInstall.min.js"></script>
561 <script>CFInstall.check({mode: "overlay"});</script>
567 class Home(openerpweb.Controller):
570 @openerpweb.httprequest
571 def index(self, req, s_action=None, db=None, **kw):
572 db, redir = db_monodb_redirect(req)
574 return redirect_with_hash(req, redir)
576 js = "\n ".join('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list(req, 'js', db=db))
577 css = "\n ".join('<link rel="stylesheet" href="%s">' % i for i in manifest_list(req, 'css', db=db))
579 r = html_template % {
582 'modules': simplejson.dumps(module_boot(req, db=db)),
583 'init': 'var wc = new s.web.WebClient();wc.appendTo($(document.body));'
587 @openerpweb.httprequest
588 def login(self, req, db, login, key):
589 return login_and_redirect(req, db, login, key)
591 class WebClient(openerpweb.Controller):
592 _cp_path = "/web/webclient"
594 @openerpweb.jsonrequest
595 def csslist(self, req, mods=None):
596 return manifest_list(req, 'css', mods=mods)
598 @openerpweb.jsonrequest
599 def jslist(self, req, mods=None):
600 return manifest_list(req, 'js', mods=mods)
602 @openerpweb.jsonrequest
603 def qweblist(self, req, mods=None):
604 return manifest_list(req, 'qweb', mods=mods)
606 @openerpweb.httprequest
607 def css(self, req, mods=None, db=None):
608 files = list(manifest_glob(req, 'css', addons=mods, db=db))
609 last_modified = get_last_modified(f[0] for f in files)
610 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
611 return werkzeug.wrappers.Response(status=304)
613 file_map = dict(files)
615 rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
616 rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
619 """read the a css file and absolutify all relative uris"""
620 with open(f, 'rb') as fp:
621 data = fp.read().decode('utf-8')
624 web_dir = os.path.dirname(path)
628 r"""@import \1%s/""" % (web_dir,),
634 r"""url(\1%s/""" % (web_dir,),
637 return data.encode('utf-8')
639 content, checksum = concat_files((f[0] for f in files), reader)
641 return make_conditional(
642 req, req.make_response(content, [('Content-Type', 'text/css')]),
643 last_modified, checksum)
645 @openerpweb.httprequest
646 def js(self, req, mods=None, db=None):
647 files = [f[0] for f in manifest_glob(req, 'js', addons=mods, db=db)]
648 last_modified = get_last_modified(files)
649 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
650 return werkzeug.wrappers.Response(status=304)
652 content, checksum = concat_js(files)
654 return make_conditional(
655 req, req.make_response(content, [('Content-Type', 'application/javascript')]),
656 last_modified, checksum)
658 @openerpweb.httprequest
659 def qweb(self, req, mods=None, db=None):
660 files = [f[0] for f in manifest_glob(req, 'qweb', addons=mods, db=db)]
661 last_modified = get_last_modified(files)
662 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
663 return werkzeug.wrappers.Response(status=304)
665 content, checksum = concat_xml(files)
667 return make_conditional(
668 req, req.make_response(content, [('Content-Type', 'text/xml')]),
669 last_modified, checksum)
671 @openerpweb.jsonrequest
672 def bootstrap_translations(self, req, mods):
673 """ Load local translations from *.po files, as a temporary solution
674 until we have established a valid session. This is meant only
675 for translating the login page and db management chrome, using
676 the browser's language. """
677 # For performance reasons we only load a single translation, so for
678 # sub-languages (that should only be partially translated) we load the
679 # main language PO instead - that should be enough for the login screen.
680 lang = req.lang.split('_')[0]
682 translations_per_module = {}
683 for addon_name in mods:
684 if openerpweb.addons_manifest[addon_name].get('bootstrap'):
685 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
686 f_name = os.path.join(addons_path, addon_name, "i18n", lang + ".po")
687 if not os.path.exists(f_name):
689 translations_per_module[addon_name] = {'messages': _local_web_translations(f_name)}
691 return {"modules": translations_per_module,
692 "lang_parameters": None}
694 @openerpweb.jsonrequest
695 def translations(self, req, mods, lang):
696 res_lang = req.session.model('res.lang')
697 ids = res_lang.search([("code", "=", lang)])
700 lang_params = res_lang.read(ids[0], ["direction", "date_format", "time_format",
701 "grouping", "decimal_point", "thousands_sep"])
703 # Regional languages (ll_CC) must inherit/override their parent lang (ll), but this is
704 # done server-side when the language is loaded, so we only need to load the user's lang.
705 ir_translation = req.session.model('ir.translation')
706 translations_per_module = {}
707 messages = ir_translation.search_read([('module','in',mods),('lang','=',lang),
708 ('comments','like','openerp-web'),('value','!=',False),
710 ['module','src','value','lang'], order='module')
711 for mod, msg_group in itertools.groupby(messages, key=operator.itemgetter('module')):
712 translations_per_module.setdefault(mod,{'messages':[]})
713 translations_per_module[mod]['messages'].extend({'id': m['src'],
714 'string': m['value']} \
716 return {"modules": translations_per_module,
717 "lang_parameters": lang_params}
719 @openerpweb.jsonrequest
720 def version_info(self, req):
721 return openerp.service.web_services.RPC_VERSION_1
723 class Proxy(openerpweb.Controller):
724 _cp_path = '/web/proxy'
726 @openerpweb.jsonrequest
727 def load(self, req, path):
728 """ Proxies an HTTP request through a JSON request.
730 It is strongly recommended to not request binary files through this,
731 as the result will be a binary data blob as well.
733 :param req: OpenERP request
734 :param path: actual request path
735 :return: file content
737 from werkzeug.test import Client
738 from werkzeug.wrappers import BaseResponse
740 return Client(req.httprequest.app, BaseResponse).get(path).data
742 class Database(openerpweb.Controller):
743 _cp_path = "/web/database"
745 @openerpweb.jsonrequest
746 def get_list(self, req):
749 @openerpweb.jsonrequest
750 def create(self, req, fields):
751 params = dict(map(operator.itemgetter('name', 'value'), fields))
752 return req.session.proxy("db").create_database(
753 params['super_admin_pwd'],
755 bool(params.get('demo_data')),
757 params['create_admin_pwd'])
759 @openerpweb.jsonrequest
760 def duplicate(self, req, fields):
761 params = dict(map(operator.itemgetter('name', 'value'), fields))
762 return req.session.proxy("db").duplicate_database(
763 params['super_admin_pwd'],
764 params['db_original_name'],
767 @openerpweb.jsonrequest
768 def duplicate(self, req, fields):
769 params = dict(map(operator.itemgetter('name', 'value'), fields))
771 params['super_admin_pwd'],
772 params['db_original_name'],
776 return req.session.proxy("db").duplicate_database(*duplicate_attrs)
778 @openerpweb.jsonrequest
779 def drop(self, req, fields):
780 password, db = operator.itemgetter(
781 'drop_pwd', 'drop_db')(
782 dict(map(operator.itemgetter('name', 'value'), fields)))
785 return req.session.proxy("db").drop(password, db)
786 except xmlrpclib.Fault, e:
787 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
788 return {'error': e.faultCode, 'title': 'Drop Database'}
789 return {'error': _('Could not drop database !'), 'title': _('Drop Database')}
791 @openerpweb.httprequest
792 def backup(self, req, backup_db, backup_pwd, token):
794 db_dump = base64.b64decode(
795 req.session.proxy("db").dump(backup_pwd, backup_db))
796 filename = "%(db)s_%(timestamp)s.dump" % {
798 'timestamp': datetime.datetime.utcnow().strftime(
799 "%Y-%m-%d_%H-%M-%SZ")
801 return req.make_response(db_dump,
802 [('Content-Type', 'application/octet-stream; charset=binary'),
803 ('Content-Disposition', content_disposition(filename, req))],
804 {'fileToken': int(token)}
806 except xmlrpclib.Fault, e:
807 return simplejson.dumps([[],[{'error': e.faultCode, 'title': _('Backup Database')}]])
809 @openerpweb.httprequest
810 def restore(self, req, db_file, restore_pwd, new_db):
812 data = base64.b64encode(db_file.read())
813 req.session.proxy("db").restore(restore_pwd, new_db, data)
815 except xmlrpclib.Fault, e:
816 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
817 raise Exception("AccessDenied")
819 @openerpweb.jsonrequest
820 def change_password(self, req, fields):
821 old_password, new_password = operator.itemgetter(
822 'old_pwd', 'new_pwd')(
823 dict(map(operator.itemgetter('name', 'value'), fields)))
825 return req.session.proxy("db").change_admin_password(old_password, new_password)
826 except xmlrpclib.Fault, e:
827 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
828 return {'error': e.faultCode, 'title': _('Change Password')}
829 return {'error': _('Error, password not changed !'), 'title': _('Change Password')}
831 class Session(openerpweb.Controller):
832 _cp_path = "/web/session"
834 def session_info(self, req):
835 req.session.ensure_valid()
837 "session_id": req.session_id,
838 "uid": req.session._uid,
839 "user_context": req.session.get_context() if req.session._uid else {},
840 "db": req.session._db,
841 "username": req.session._login,
844 @openerpweb.jsonrequest
845 def get_session_info(self, req):
846 return self.session_info(req)
848 @openerpweb.jsonrequest
849 def authenticate(self, req, db, login, password, base_location=None):
850 wsgienv = req.httprequest.environ
852 base_location=base_location,
853 HTTP_HOST=wsgienv['HTTP_HOST'],
854 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
856 req.session.authenticate(db, login, password, env)
858 return self.session_info(req)
860 @openerpweb.jsonrequest
861 def change_password (self,req,fields):
862 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
863 dict(map(operator.itemgetter('name', 'value'), fields)))
864 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
865 return {'error':_('You cannot leave any password empty.'),'title': _('Change Password')}
866 if new_password != confirm_password:
867 return {'error': _('The new password and its confirmation must be identical.'),'title': _('Change Password')}
869 if req.session.model('res.users').change_password(
870 old_password, new_password):
871 return {'new_password':new_password}
873 return {'error': _('The old password you provided is incorrect, your password was not changed.'), 'title': _('Change Password')}
874 return {'error': _('Error, password not changed !'), 'title': _('Change Password')}
876 @openerpweb.jsonrequest
877 def sc_list(self, req):
878 return req.session.model('ir.ui.view_sc').get_sc(
879 req.session._uid, "ir.ui.menu", req.context)
881 @openerpweb.jsonrequest
882 def get_lang_list(self, req):
884 return req.session.proxy("db").list_lang() or []
886 return {"error": e, "title": _("Languages")}
888 @openerpweb.jsonrequest
889 def modules(self, req):
890 # return all installed modules. Web client is smart enough to not load a module twice
891 return module_installed(req)
893 @openerpweb.jsonrequest
894 def save_session_action(self, req, the_action):
896 This method store an action object in the session object and returns an integer
897 identifying that action. The method get_session_action() can be used to get
900 :param the_action: The action to save in the session.
901 :type the_action: anything
902 :return: A key identifying the saved action.
905 saved_actions = req.httpsession.get('saved_actions')
906 if not saved_actions:
907 saved_actions = {"next":1, "actions":{}}
908 req.httpsession['saved_actions'] = saved_actions
909 # we don't allow more than 10 stored actions
910 if len(saved_actions["actions"]) >= 10:
911 del saved_actions["actions"][min(saved_actions["actions"])]
912 key = saved_actions["next"]
913 saved_actions["actions"][key] = the_action
914 saved_actions["next"] = key + 1
917 @openerpweb.jsonrequest
918 def get_session_action(self, req, key):
920 Gets back a previously saved action. This method can return None if the action
921 was saved since too much time (this case should be handled in a smart way).
923 :param key: The key given by save_session_action()
925 :return: The saved action or None.
928 saved_actions = req.httpsession.get('saved_actions')
929 if not saved_actions:
931 return saved_actions["actions"].get(key)
933 @openerpweb.jsonrequest
934 def check(self, req):
935 req.session.assert_valid()
938 @openerpweb.jsonrequest
939 def destroy(self, req):
940 req.session._suicide = True
942 class Menu(openerpweb.Controller):
943 _cp_path = "/web/menu"
945 @openerpweb.jsonrequest
946 def get_user_roots(self, req):
947 """ Return all root menu ids visible for the session user.
949 :param req: A request object, with an OpenERP session attribute
950 :type req: < session -> OpenERPSession >
951 :return: the root menu ids
955 Menus = s.model('ir.ui.menu')
956 # If a menu action is defined use its domain to get the root menu items
957 user_menu_id = s.model('res.users').read([s._uid], ['menu_id'],
958 req.context)[0]['menu_id']
960 menu_domain = [('parent_id', '=', False)]
962 domain_string = s.model('ir.actions.act_window').read(
963 [user_menu_id[0]], ['domain'],req.context)[0]['domain']
965 menu_domain = ast.literal_eval(domain_string)
967 return Menus.search(menu_domain, 0, False, False, req.context)
969 @openerpweb.jsonrequest
971 """ Loads all menu items (all applications and their sub-menus).
973 :param req: A request object, with an OpenERP session attribute
974 :type req: < session -> OpenERPSession >
975 :return: the menu root
976 :rtype: dict('children': menu_nodes)
978 Menus = req.session.model('ir.ui.menu')
980 fields = ['name', 'sequence', 'parent_id', 'action']
981 menu_root_ids = self.get_user_roots(req)
982 menu_roots = Menus.read(menu_root_ids, fields, req.context) if menu_root_ids else []
986 'parent_id': [-1, ''],
987 'children': menu_roots,
988 'all_menu_ids': menu_root_ids,
993 # menus are loaded fully unlike a regular tree view, cause there are a
994 # limited number of items (752 when all 6.1 addons are installed)
995 menu_ids = Menus.search([('id', 'child_of', menu_root_ids)], 0, False, False, req.context)
996 menu_items = Menus.read(menu_ids, fields, req.context)
997 # adds roots at the end of the sequence, so that they will overwrite
998 # equivalent menu items from full menu read when put into id:item
999 # mapping, resulting in children being correctly set on the roots.
1000 menu_items.extend(menu_roots)
1001 menu_root['all_menu_ids'] = menu_ids # includes menu_root_ids!
1003 # make a tree using parent_id
1004 menu_items_map = dict(
1005 (menu_item["id"], menu_item) for menu_item in menu_items)
1006 for menu_item in menu_items:
1007 if menu_item['parent_id']:
1008 parent = menu_item['parent_id'][0]
1011 if parent in menu_items_map:
1012 menu_items_map[parent].setdefault(
1013 'children', []).append(menu_item)
1015 # sort by sequence a tree using parent_id
1016 for menu_item in menu_items:
1017 menu_item.setdefault('children', []).sort(
1018 key=operator.itemgetter('sequence'))
1022 @openerpweb.jsonrequest
1023 def load_needaction(self, req, menu_ids):
1024 """ Loads needaction counters for specific menu ids.
1026 :return: needaction data
1027 :rtype: dict(menu_id: {'needaction_enabled': boolean, 'needaction_counter': int})
1029 return req.session.model('ir.ui.menu').get_needaction_data(menu_ids, req.context)
1031 @openerpweb.jsonrequest
1032 def action(self, req, menu_id):
1033 # still used by web_shortcut
1034 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
1035 [('ir.ui.menu', menu_id)], False)
1036 return {"action": actions}
1038 class DataSet(openerpweb.Controller):
1039 _cp_path = "/web/dataset"
1041 @openerpweb.jsonrequest
1042 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
1043 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
1044 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
1046 """ Performs a search() followed by a read() (if needed) using the
1047 provided search criteria
1049 :param req: a JSON-RPC request object
1050 :type req: openerpweb.JsonRequest
1051 :param str model: the name of the model to search on
1052 :param fields: a list of the fields to return in the result records
1054 :param int offset: from which index should the results start being returned
1055 :param int limit: the maximum number of records to return
1056 :param list domain: the search domain for the query
1057 :param list sort: sorting directives
1058 :returns: A structure (dict) with two keys: ids (all the ids matching
1059 the (domain, context) pair) and records (paginated records
1060 matching fields selection set)
1063 Model = req.session.model(model)
1065 ids = Model.search(domain, offset or 0, limit or False, sort or False,
1067 if limit and len(ids) == limit:
1068 length = Model.search_count(domain, req.context)
1070 length = len(ids) + (offset or 0)
1071 if fields and fields == ['id']:
1072 # shortcut read if we only want the ids
1075 'records': [{'id': id} for id in ids]
1078 records = Model.read(ids, fields or False, req.context)
1079 records.sort(key=lambda obj: ids.index(obj['id']))
1085 @openerpweb.jsonrequest
1086 def load(self, req, model, id, fields):
1087 m = req.session.model(model)
1089 r = m.read([id], False, req.context)
1092 return {'value': value}
1094 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
1095 return self._call_kw(req, model, method, args, {})
1097 def _call_kw(self, req, model, method, args, kwargs):
1098 # Temporary implements future display_name special field for model#read()
1099 if method == 'read' and kwargs.get('context', {}).get('future_display_name'):
1100 if 'display_name' in args[1]:
1101 names = dict(req.session.model(model).name_get(args[0], **kwargs))
1102 args[1].remove('display_name')
1103 records = req.session.model(model).read(*args, **kwargs)
1104 for record in records:
1105 record['display_name'] = \
1106 names.get(record['id']) or "%s#%d" % (model, (record['id']))
1109 return getattr(req.session.model(model), method)(*args, **kwargs)
1111 @openerpweb.jsonrequest
1112 def call(self, req, model, method, args, domain_id=None, context_id=None):
1113 return self._call_kw(req, model, method, args, {})
1115 @openerpweb.jsonrequest
1116 def call_kw(self, req, model, method, args, kwargs):
1117 return self._call_kw(req, model, method, args, kwargs)
1119 @openerpweb.jsonrequest
1120 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1121 action = self._call_kw(req, model, method, args, {})
1122 if isinstance(action, dict) and action.get('type') != '':
1123 return clean_action(req, action)
1126 @openerpweb.jsonrequest
1127 def exec_workflow(self, req, model, id, signal):
1128 return req.session.exec_workflow(model, id, signal)
1130 @openerpweb.jsonrequest
1131 def resequence(self, req, model, ids, field='sequence', offset=0):
1132 """ Re-sequences a number of records in the model, by their ids
1134 The re-sequencing starts at the first model of ``ids``, the sequence
1135 number is incremented by one after each record and starts at ``offset``
1137 :param ids: identifiers of the records to resequence, in the new sequence order
1139 :param str field: field used for sequence specification, defaults to
1141 :param int offset: sequence number for first record in ``ids``, allows
1142 starting the resequencing from an arbitrary number,
1145 m = req.session.model(model)
1146 if not m.fields_get([field]):
1148 # python 2.6 has no start parameter
1149 for i, id in enumerate(ids):
1150 m.write(id, { field: i + offset })
1153 class View(openerpweb.Controller):
1154 _cp_path = "/web/view"
1156 @openerpweb.jsonrequest
1157 def add_custom(self, req, view_id, arch):
1158 CustomView = req.session.model('ir.ui.view.custom')
1160 'user_id': req.session._uid,
1164 return {'result': True}
1166 @openerpweb.jsonrequest
1167 def undo_custom(self, req, view_id, reset=False):
1168 CustomView = req.session.model('ir.ui.view.custom')
1169 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1170 0, False, False, req.context)
1173 CustomView.unlink(vcustom, req.context)
1175 CustomView.unlink([vcustom[0]], req.context)
1176 return {'result': True}
1177 return {'result': False}
1179 class TreeView(View):
1180 _cp_path = "/web/treeview"
1182 @openerpweb.jsonrequest
1183 def action(self, req, model, id):
1184 return load_actions_from_ir_values(
1185 req,'action', 'tree_but_open',[(model, id)],
1188 class Binary(openerpweb.Controller):
1189 _cp_path = "/web/binary"
1191 @openerpweb.httprequest
1192 def image(self, req, model, id, field, **kw):
1193 last_update = '__last_update'
1194 Model = req.session.model(model)
1195 headers = [('Content-Type', 'image/png')]
1196 etag = req.httprequest.headers.get('If-None-Match')
1197 hashed_session = hashlib.md5(req.session_id).hexdigest()
1198 id = None if not id else simplejson.loads(id)
1199 if type(id) is list:
1202 if not id and hashed_session == etag:
1203 return werkzeug.wrappers.Response(status=304)
1205 date = Model.read([id], [last_update], req.context)[0].get(last_update)
1206 if hashlib.md5(date).hexdigest() == etag:
1207 return werkzeug.wrappers.Response(status=304)
1209 retag = hashed_session
1212 res = Model.default_get([field], req.context).get(field)
1215 res = Model.read([id], [last_update, field], req.context)[0]
1216 retag = hashlib.md5(res.get(last_update)).hexdigest()
1217 image_base64 = res.get(field)
1219 if kw.get('resize'):
1220 resize = kw.get('resize').split(',')
1221 if len(resize) == 2 and int(resize[0]) and int(resize[1]):
1222 width = int(resize[0])
1223 height = int(resize[1])
1224 # resize maximum 500*500
1225 if width > 500: width = 500
1226 if height > 500: height = 500
1227 image_base64 = openerp.tools.image_resize_image(base64_source=image_base64, size=(width, height), encoding='base64', filetype='PNG')
1229 image_data = base64.b64decode(image_base64)
1231 except (TypeError, xmlrpclib.Fault):
1232 image_data = self.placeholder(req)
1233 headers.append(('ETag', retag))
1234 headers.append(('Content-Length', len(image_data)))
1236 ncache = int(kw.get('cache'))
1237 headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1240 return req.make_response(image_data, headers)
1242 def placeholder(self, req, image='placeholder.png'):
1243 addons_path = openerpweb.addons_manifest['web']['addons_path']
1244 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', image), 'rb').read()
1246 @openerpweb.httprequest
1247 def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1248 """ Download link for files stored as binary fields.
1250 If the ``id`` parameter is omitted, fetches the default value for the
1251 binary field (via ``default_get``), otherwise fetches the field for
1252 that precise record.
1254 :param req: OpenERP request
1255 :type req: :class:`web.common.http.HttpRequest`
1256 :param str model: name of the model to fetch the binary from
1257 :param str field: binary field
1258 :param str id: id of the record from which to fetch the binary
1259 :param str filename_field: field holding the file's name, if any
1260 :returns: :class:`werkzeug.wrappers.Response`
1262 Model = req.session.model(model)
1265 fields.append(filename_field)
1267 res = Model.read([int(id)], fields, req.context)[0]
1269 res = Model.default_get(fields, req.context)
1270 filecontent = base64.b64decode(res.get(field, ''))
1272 return req.not_found()
1274 filename = '%s_%s' % (model.replace('.', '_'), id)
1276 filename = res.get(filename_field, '') or filename
1277 return req.make_response(filecontent,
1278 [('Content-Type', 'application/octet-stream'),
1279 ('Content-Disposition', content_disposition(filename, req))])
1281 @openerpweb.httprequest
1282 def saveas_ajax(self, req, data, token):
1283 jdata = simplejson.loads(data)
1284 model = jdata['model']
1285 field = jdata['field']
1286 data = jdata['data']
1287 id = jdata.get('id', None)
1288 filename_field = jdata.get('filename_field', None)
1289 context = jdata.get('context', {})
1291 Model = req.session.model(model)
1294 fields.append(filename_field)
1296 res = { field: data }
1298 res = Model.read([int(id)], fields, context)[0]
1300 res = Model.default_get(fields, context)
1301 filecontent = base64.b64decode(res.get(field, ''))
1303 raise ValueError(_("No content found for field '%s' on '%s:%s'") %
1306 filename = '%s_%s' % (model.replace('.', '_'), id)
1308 filename = res.get(filename_field, '') or filename
1309 return req.make_response(filecontent,
1310 headers=[('Content-Type', 'application/octet-stream'),
1311 ('Content-Disposition', content_disposition(filename, req))],
1312 cookies={'fileToken': int(token)})
1314 @openerpweb.httprequest
1315 def upload(self, req, callback, ufile):
1316 # TODO: might be useful to have a configuration flag for max-length file uploads
1317 out = """<script language="javascript" type="text/javascript">
1318 var win = window.top.window;
1319 win.jQuery(win).trigger(%s, %s);
1323 args = [len(data), ufile.filename,
1324 ufile.content_type, base64.b64encode(data)]
1325 except Exception, e:
1326 args = [False, e.message]
1327 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1329 @openerpweb.httprequest
1330 def upload_attachment(self, req, callback, model, id, ufile):
1331 Model = req.session.model('ir.attachment')
1332 out = """<script language="javascript" type="text/javascript">
1333 var win = window.top.window;
1334 win.jQuery(win).trigger(%s, %s);
1337 attachment_id = Model.create({
1338 'name': ufile.filename,
1339 'datas': base64.encodestring(ufile.read()),
1340 'datas_fname': ufile.filename,
1345 'filename': ufile.filename,
1348 except xmlrpclib.Fault, e:
1349 args = {'error':e.faultCode }
1350 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1352 @openerpweb.httprequest
1353 def company_logo(self, req, dbname=None):
1354 # TODO add etag, refactor to use /image code for etag
1357 dbname = req.session._db
1358 uid = req.session._uid
1359 elif dbname is None:
1360 dbname = db_monodb(req)
1363 uid = openerp.SUPERUSER_ID
1366 image_data = self.placeholder(req, 'logo.png')
1368 registry = openerp.modules.registry.RegistryManager.get(dbname)
1369 with registry.cursor() as cr:
1370 user = registry.get('res.users').browse(cr, uid, uid)
1371 if user.company_id.logo_web:
1372 image_data = user.company_id.logo_web.decode('base64')
1374 image_data = self.placeholder(req, 'nologo.png')
1376 ('Content-Type', 'image/png'),
1377 ('Content-Length', len(image_data)),
1379 return req.make_response(image_data, headers)
1381 class Action(openerpweb.Controller):
1382 _cp_path = "/web/action"
1384 @openerpweb.jsonrequest
1385 def load(self, req, action_id, do_not_eval=False):
1386 Actions = req.session.model('ir.actions.actions')
1389 action_id = int(action_id)
1392 module, xmlid = action_id.split('.', 1)
1393 model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
1394 assert model.startswith('ir.actions.')
1396 action_id = 0 # force failed read
1398 base_action = Actions.read([action_id], ['type'], req.context)
1401 action_type = base_action[0]['type']
1402 if action_type == 'ir.actions.report.xml':
1403 ctx.update({'bin_size': True})
1404 ctx.update(req.context)
1405 action = req.session.model(action_type).read([action_id], False, ctx)
1407 value = clean_action(req, action[0])
1410 @openerpweb.jsonrequest
1411 def run(self, req, action_id):
1412 return_action = req.session.model('ir.actions.server').run(
1413 [action_id], req.context)
1415 return clean_action(req, return_action)
1420 _cp_path = "/web/export"
1422 @openerpweb.jsonrequest
1423 def formats(self, req):
1424 """ Returns all valid export formats
1426 :returns: for each export format, a pair of identifier and printable name
1427 :rtype: [(str, str)]
1431 for path, controller in openerpweb.controllers_path.iteritems()
1432 if path.startswith(self._cp_path)
1433 if hasattr(controller, 'fmt')
1434 ], key=operator.itemgetter("label"))
1436 def fields_get(self, req, model):
1437 Model = req.session.model(model)
1438 fields = Model.fields_get(False, req.context)
1441 @openerpweb.jsonrequest
1442 def get_fields(self, req, model, prefix='', parent_name= '',
1443 import_compat=True, parent_field_type=None,
1446 if import_compat and parent_field_type == "many2one":
1449 fields = self.fields_get(req, model)
1452 fields.pop('id', None)
1454 fields['.id'] = fields.pop('id', {'string': 'ID'})
1456 fields_sequence = sorted(fields.iteritems(),
1457 key=lambda field: field[1].get('string', ''))
1460 for field_name, field in fields_sequence:
1462 if exclude and field_name in exclude:
1464 if field.get('readonly'):
1465 # If none of the field's states unsets readonly, skip the field
1466 if all(dict(attrs).get('readonly', True)
1467 for attrs in field.get('states', {}).values()):
1470 id = prefix + (prefix and '/'or '') + field_name
1471 name = parent_name + (parent_name and '/' or '') + field['string']
1472 record = {'id': id, 'string': name,
1473 'value': id, 'children': False,
1474 'field_type': field.get('type'),
1475 'required': field.get('required'),
1476 'relation_field': field.get('relation_field')}
1477 records.append(record)
1479 if len(name.split('/')) < 3 and 'relation' in field:
1480 ref = field.pop('relation')
1481 record['value'] += '/id'
1482 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1484 if not import_compat or field['type'] == 'one2many':
1485 # m2m field in import_compat is childless
1486 record['children'] = True
1490 @openerpweb.jsonrequest
1491 def namelist(self,req, model, export_id):
1492 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1493 export = req.session.model("ir.exports").read([export_id])[0]
1494 export_fields_list = req.session.model("ir.exports.line").read(
1495 export['export_fields'])
1497 fields_data = self.fields_info(
1498 req, model, map(operator.itemgetter('name'), export_fields_list))
1501 {'name': field['name'], 'label': fields_data[field['name']]}
1502 for field in export_fields_list
1505 def fields_info(self, req, model, export_fields):
1507 fields = self.fields_get(req, model)
1508 if ".id" in export_fields:
1509 fields['.id'] = fields.pop('id', {'string': 'ID'})
1511 # To make fields retrieval more efficient, fetch all sub-fields of a
1512 # given field at the same time. Because the order in the export list is
1513 # arbitrary, this requires ordering all sub-fields of a given field
1514 # together so they can be fetched at the same time
1516 # Works the following way:
1517 # * sort the list of fields to export, the default sorting order will
1518 # put the field itself (if present, for xmlid) and all of its
1519 # sub-fields right after it
1520 # * then, group on: the first field of the path (which is the same for
1521 # a field and for its subfields and the length of splitting on the
1522 # first '/', which basically means grouping the field on one side and
1523 # all of the subfields on the other. This way, we have the field (for
1524 # the xmlid) with length 1, and all of the subfields with the same
1525 # base but a length "flag" of 2
1526 # * if we have a normal field (length 1), just add it to the info
1527 # mapping (with its string) as-is
1528 # * otherwise, recursively call fields_info via graft_subfields.
1529 # all graft_subfields does is take the result of fields_info (on the
1530 # field's model) and prepend the current base (current field), which
1531 # rebuilds the whole sub-tree for the field
1533 # result: because we're not fetching the fields_get for half the
1534 # database models, fetching a namelist with a dozen fields (including
1535 # relational data) falls from ~6s to ~300ms (on the leads model).
1536 # export lists with no sub-fields (e.g. import_compatible lists with
1537 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1538 # there's a single fields_get to execute)
1539 for (base, length), subfields in itertools.groupby(
1540 sorted(export_fields),
1541 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1542 subfields = list(subfields)
1544 # subfields is a seq of $base/*rest, and not loaded yet
1545 info.update(self.graft_subfields(
1546 req, fields[base]['relation'], base, fields[base]['string'],
1550 info[base] = fields[base]['string']
1554 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1555 export_fields = [field.split('/', 1)[1] for field in fields]
1557 (prefix + '/' + k, prefix_string + '/' + v)
1558 for k, v in self.fields_info(req, model, export_fields).iteritems())
1560 #noinspection PyPropertyDefinition
1562 def content_type(self):
1563 """ Provides the format's content type """
1564 raise NotImplementedError()
1566 def filename(self, base):
1567 """ Creates a valid filename for the format (with extension) from the
1568 provided base name (exension-less)
1570 raise NotImplementedError()
1572 def from_data(self, fields, rows):
1573 """ Conversion method from OpenERP's export data to whatever the
1574 current export class outputs
1576 :params list fields: a list of fields to export
1577 :params list rows: a list of records to export
1581 raise NotImplementedError()
1583 @openerpweb.httprequest
1584 def index(self, req, data, token):
1585 model, fields, ids, domain, import_compat = \
1586 operator.itemgetter('model', 'fields', 'ids', 'domain',
1588 simplejson.loads(data))
1590 Model = req.session.model(model)
1591 ids = ids or Model.search(domain, 0, False, False, req.context)
1593 field_names = map(operator.itemgetter('name'), fields)
1594 import_data = Model.export_data(ids, field_names, req.context).get('datas',[])
1597 columns_headers = field_names
1599 columns_headers = [val['label'].strip() for val in fields]
1602 return req.make_response(self.from_data(columns_headers, import_data),
1603 headers=[('Content-Disposition',
1604 content_disposition(self.filename(model), req)),
1605 ('Content-Type', self.content_type)],
1606 cookies={'fileToken': int(token)})
1608 class CSVExport(Export):
1609 _cp_path = '/web/export/csv'
1610 fmt = {'tag': 'csv', 'label': 'CSV'}
1613 def content_type(self):
1614 return 'text/csv;charset=utf8'
1616 def filename(self, base):
1617 return base + '.csv'
1619 def from_data(self, fields, rows):
1621 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1623 writer.writerow([name.encode('utf-8') for name in fields])
1628 if isinstance(d, basestring):
1629 d = d.replace('\n',' ').replace('\t',' ')
1631 d = d.encode('utf-8')
1632 except UnicodeError:
1634 if d is False: d = None
1636 writer.writerow(row)
1643 class ExcelExport(Export):
1644 _cp_path = '/web/export/xls'
1648 'error': None if xlwt else "XLWT required"
1652 def content_type(self):
1653 return 'application/vnd.ms-excel'
1655 def filename(self, base):
1656 return base + '.xls'
1658 def from_data(self, fields, rows):
1659 workbook = xlwt.Workbook()
1660 worksheet = workbook.add_sheet('Sheet 1')
1662 for i, fieldname in enumerate(fields):
1663 worksheet.write(0, i, fieldname)
1664 worksheet.col(i).width = 8000 # around 220 pixels
1666 style = xlwt.easyxf('align: wrap yes')
1668 for row_index, row in enumerate(rows):
1669 for cell_index, cell_value in enumerate(row):
1670 if isinstance(cell_value, basestring):
1671 cell_value = re.sub("\r", " ", cell_value)
1672 if cell_value is False: cell_value = None
1673 worksheet.write(row_index + 1, cell_index, cell_value, style)
1682 class Reports(View):
1683 _cp_path = "/web/report"
1684 POLLING_DELAY = 0.25
1686 'doc': 'application/vnd.ms-word',
1687 'html': 'text/html',
1688 'odt': 'application/vnd.oasis.opendocument.text',
1689 'pdf': 'application/pdf',
1690 'sxw': 'application/vnd.sun.xml.writer',
1691 'xls': 'application/vnd.ms-excel',
1694 @openerpweb.httprequest
1695 def index(self, req, action, token):
1696 action = simplejson.loads(action)
1698 report_srv = req.session.proxy("report")
1699 context = dict(req.context)
1700 context.update(action["context"])
1703 report_ids = context["active_ids"]
1704 if 'report_type' in action:
1705 report_data['report_type'] = action['report_type']
1706 if 'datas' in action:
1707 if 'ids' in action['datas']:
1708 report_ids = action['datas'].pop('ids')
1709 report_data.update(action['datas'])
1711 report_id = report_srv.report(
1712 req.session._db, req.session._uid, req.session._password,
1713 action["report_name"], report_ids,
1714 report_data, context)
1716 report_struct = None
1718 report_struct = report_srv.report_get(
1719 req.session._db, req.session._uid, req.session._password, report_id)
1720 if report_struct["state"]:
1723 time.sleep(self.POLLING_DELAY)
1725 report = base64.b64decode(report_struct['result'])
1726 if report_struct.get('code') == 'zlib':
1727 report = zlib.decompress(report)
1728 report_mimetype = self.TYPES_MAPPING.get(
1729 report_struct['format'], 'octet-stream')
1730 file_name = action.get('name', 'report')
1731 if 'name' not in action:
1732 reports = req.session.model('ir.actions.report.xml')
1733 res_id = reports.search([('report_name', '=', action['report_name']),],
1734 0, False, False, context)
1736 file_name = reports.read(res_id[0], ['name'], context)['name']
1738 file_name = action['report_name']
1739 file_name = '%s.%s' % (file_name, report_struct['format'])
1741 return req.make_response(report,
1743 ('Content-Disposition', content_disposition(file_name, req)),
1744 ('Content-Type', report_mimetype),
1745 ('Content-Length', len(report))],
1746 cookies={'fileToken': int(token)})
1748 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: