1 # -*- coding: utf-8 -*-
19 from xml.etree import ElementTree
20 from cStringIO import StringIO
22 import babel.messages.pofile
24 import werkzeug.wrappers
33 from .. import nonliterals
36 #----------------------------------------------------------
38 #----------------------------------------------------------
41 """ Minify js with a clever regex.
42 Taken from http://opensource.perlig.de/rjsmin
43 Apache License, Version 2.0 """
45 """ Substitution callback """
46 groups = match.groups()
52 (groups[4] and '\n') or
53 (groups[5] and ' ') or
54 (groups[6] and ' ') or
55 (groups[7] and ' ') or
60 r'([^\047"/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]|\r?'
61 r'\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|'
62 r'\r)[^"\\\r\n]*)*"))[^\047"/\000-\040]*)|(?:(?<=[(,=:\[!&|?{};\r\n]'
63 r')(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/'
64 r'))*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*'
65 r'(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*'
66 r'))|(?:(?<=[\000-#%-,./:-@\[-^`{-~-]return)(?:[\000-\011\013\014\01'
67 r'6-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*((?:/(?![\r\n/*])[^/'
68 r'\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]'
69 r'*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*))|(?<=[^\000-!#%&(*,./'
70 r':-@\[\\^`{|~])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/'
71 r'*][^*]*\*+)*/))*(?:((?:(?://[^\r\n]*)?[\r\n]))(?:[\000-\011\013\01'
72 r'4\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040"#'
73 r'%-\047)*,./:-@\\-^`|-~])|(?<=[^\000-#%-,./:-@\[-^`{-~-])((?:[\000-'
74 r'\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=[^'
75 r'\000-#%-,./:-@\[-^`{-~-])|(?<=\+)((?:[\000-\011\013\014\016-\040]|'
76 r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=\+)|(?<=-)((?:[\000-\011\0'
77 r'13\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=-)|(?:[\0'
78 r'00-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))+|(?:'
79 r'(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*'
80 r']*\*+(?:[^/*][^*]*\*+)*/))*)+', subber, '\n%s\n' % script
86 proxy = req.session.proxy("db")
88 h = req.httprequest.environ['HTTP_HOST'].split(':')[0]
90 r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
91 dbs = [i for i in dbs if re.match(r, i)]
95 # if only one db is listed returns it else return False
100 except xmlrpclib.Fault:
101 # ignore access denied
105 def module_topological_sort(modules):
106 """ Return a list of module names sorted so that their dependencies of the
107 modules are listed before the module itself
109 modules is a dict of {module_name: dependencies}
111 :param modules: modules to sort
116 dependencies = set(itertools.chain.from_iterable(modules.itervalues()))
117 # incoming edge: dependency on other module (if a depends on b, a has an
118 # incoming edge from b, aka there's an edge from b to a)
119 # outgoing edge: other module depending on this one
121 # [Tarjan 1976], http://en.wikipedia.org/wiki/Topological_sorting#Algorithms
122 #L ← Empty list that will contain the sorted nodes
124 #S ← Set of all nodes with no outgoing edges (modules on which no other
126 S = set(module for module in modules if module not in dependencies)
129 #function visit(node n)
131 #if n has not been visited yet then
135 #change: n not web module, can not be resolved, ignore
136 if n not in modules: return
137 #for each node m with an edge from m to n do (dependencies of n)
143 #for each node n in S do
149 def module_installed(req):
150 # Candidates module the current heuristic is the /static dir
151 loadable = openerpweb.addons_manifest.keys()
154 # Retrieve database installed modules
155 # TODO The following code should move to ir.module.module.list_installed_modules()
156 Modules = req.session.model('ir.module.module')
157 domain = [('state','=','installed'), ('name','in', loadable)]
158 for module in Modules.search_read(domain, ['name', 'dependencies_id']):
159 modules[module['name']] = []
160 deps = module.get('dependencies_id')
162 deps_read = req.session.model('ir.module.module.dependency').read(deps, ['name'])
163 dependencies = [i['name'] for i in deps_read]
164 modules[module['name']] = dependencies
166 sorted_modules = module_topological_sort(modules)
167 return sorted_modules
169 def module_installed_bypass_session(dbname):
170 loadable = openerpweb.addons_manifest.keys()
173 import openerp.modules.registry
174 registry = openerp.modules.registry.RegistryManager.get(dbname)
175 with registry.cursor() as cr:
176 m = registry.get('ir.module.module')
177 # TODO The following code should move to ir.module.module.list_installed_modules()
178 domain = [('state','=','installed'), ('name','in', loadable)]
179 ids = m.search(cr, 1, [('state','=','installed'), ('name','in', loadable)])
180 for module in m.read(cr, 1, ids, ['name', 'dependencies_id']):
181 modules[module['name']] = []
182 deps = module.get('dependencies_id')
184 deps_read = registry.get('ir.module.module.dependency').read(cr, 1, deps, ['name'])
185 dependencies = [i['name'] for i in deps_read]
186 modules[module['name']] = dependencies
189 sorted_modules = module_topological_sort(modules)
190 return sorted_modules
192 def module_boot(req):
193 server_wide_modules = openerp.conf.server_wide_modules or ['web']
196 for i in server_wide_modules:
197 if i in openerpweb.addons_manifest:
199 monodb = db_monodb(req)
201 dbside = module_installed_bypass_session(monodb)
202 dbside = [i for i in dbside if i not in serverside]
203 addons = serverside + dbside
206 def concat_xml(file_list):
207 """Concatenate xml files
209 :param list(str) file_list: list of files to check
210 :returns: (concatenation_result, checksum)
213 checksum = hashlib.new('sha1')
215 return '', checksum.hexdigest()
218 for fname in file_list:
219 with open(fname, 'rb') as fp:
221 checksum.update(contents)
223 xml = ElementTree.parse(fp).getroot()
226 root = ElementTree.Element(xml.tag)
227 #elif root.tag != xml.tag:
228 # raise ValueError("Root tags missmatch: %r != %r" % (root.tag, xml.tag))
230 for child in xml.getchildren():
232 return ElementTree.tostring(root, 'utf-8'), checksum.hexdigest()
234 def concat_files(file_list, reader=None, intersperse=""):
235 """ Concatenates contents of all provided files
237 :param list(str) file_list: list of files to check
238 :param function reader: reading procedure for each file
239 :param str intersperse: string to intersperse between file contents
240 :returns: (concatenation_result, checksum)
243 checksum = hashlib.new('sha1')
245 return '', checksum.hexdigest()
249 with open(f, 'rb') as fp:
253 for fname in file_list:
254 contents = reader(fname)
255 checksum.update(contents)
256 files_content.append(contents)
258 files_concat = intersperse.join(files_content)
259 return files_concat, checksum.hexdigest()
261 def concat_js(file_list):
262 content, checksum = concat_files(file_list, intersperse=';')
263 content = rjsmin(content)
264 return content, checksum
266 def manifest_glob(req, addons, key):
268 addons = module_boot(req)
270 addons = addons.split(',')
273 manifest = openerpweb.addons_manifest.get(addon, None)
276 # ensure does not ends with /
277 addons_path = os.path.join(manifest['addons_path'], '')[:-1]
278 globlist = manifest.get(key, [])
279 for pattern in globlist:
280 for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
281 r.append((path, path[len(addons_path):]))
284 def manifest_list(req, mods, extension):
286 path = '/web/webclient/' + extension
288 path += '?mods=' + mods
290 files = manifest_glob(req, mods, extension)
291 i_am_diabetic = req.httprequest.environ["QUERY_STRING"].count("no_sugar") >= 1 or \
292 req.httprequest.environ.get('HTTP_REFERER', '').count("no_sugar") >= 1
294 return [wp for _fp, wp in files]
296 return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in files]
298 def get_last_modified(files):
299 """ Returns the modification time of the most recently modified
302 :param list(str) files: names of files to check
303 :return: most recent modification time amongst the fileset
304 :rtype: datetime.datetime
308 return max(datetime.datetime.fromtimestamp(os.path.getmtime(f))
310 return datetime.datetime(1970, 1, 1)
312 def make_conditional(req, response, last_modified=None, etag=None):
313 """ Makes the provided response conditional based upon the request,
314 and mandates revalidation from clients
316 Uses Werkzeug's own :meth:`ETagResponseMixin.make_conditional`, after
317 setting ``last_modified`` and ``etag`` correctly on the response object
319 :param req: OpenERP request
320 :type req: web.common.http.WebRequest
321 :param response: Werkzeug response
322 :type response: werkzeug.wrappers.Response
323 :param datetime.datetime last_modified: last modification date of the response content
324 :param str etag: some sort of checksum of the content (deep etag)
325 :return: the response object provided
326 :rtype: werkzeug.wrappers.Response
328 response.cache_control.must_revalidate = True
329 response.cache_control.max_age = 0
331 response.last_modified = last_modified
333 response.set_etag(etag)
334 return response.make_conditional(req.httprequest)
336 def login_and_redirect(req, db, login, key, redirect_url='/'):
337 req.session.authenticate(db, login, key, {})
338 return set_cookie_and_redirect(req, redirect_url)
340 def set_cookie_and_redirect(req, redirect_url):
341 redirect = werkzeug.utils.redirect(redirect_url, 303)
342 redirect.autocorrect_location_header = False
343 cookie_val = urllib2.quote(simplejson.dumps(req.session_id))
344 redirect.set_cookie('instance0|session_id', cookie_val)
347 def eval_context_and_domain(session, context, domain=None):
348 e_context = session.eval_context(context)
349 # should we give the evaluated context as an evaluation context to the domain?
350 e_domain = session.eval_domain(domain or [])
352 return e_context, e_domain
354 def load_actions_from_ir_values(req, key, key2, models, meta):
355 context = req.session.eval_context(req.context)
356 Values = req.session.model('ir.values')
357 actions = Values.get(key, key2, models, meta, context)
359 return [(id, name, clean_action(req, action))
360 for id, name, action in actions]
362 def clean_action(req, action, do_not_eval=False):
363 action.setdefault('flags', {})
365 context = req.session.eval_context(req.context)
366 eval_ctx = req.session.evaluation_context(context)
369 # values come from the server, we can just eval them
370 if action.get('context') and isinstance(action.get('context'), basestring):
371 action['context'] = eval( action['context'], eval_ctx ) or {}
373 if action.get('domain') and isinstance(action.get('domain'), basestring):
374 action['domain'] = eval( action['domain'], eval_ctx ) or []
376 if 'context' in action:
377 action['context'] = parse_context(action['context'], req.session)
378 if 'domain' in action:
379 action['domain'] = parse_domain(action['domain'], req.session)
381 action_type = action.setdefault('type', 'ir.actions.act_window_close')
382 if action_type == 'ir.actions.act_window':
383 return fix_view_modes(action)
386 # I think generate_views,fix_view_modes should go into js ActionManager
387 def generate_views(action):
389 While the server generates a sequence called "views" computing dependencies
390 between a bunch of stuff for views coming directly from the database
391 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
392 to return custom view dictionaries generated on the fly.
394 In that case, there is no ``views`` key available on the action.
396 Since the web client relies on ``action['views']``, generate it here from
397 ``view_mode`` and ``view_id``.
399 Currently handles two different cases:
401 * no view_id, multiple view_mode
402 * single view_id, single view_mode
404 :param dict action: action descriptor dictionary to generate a views key for
406 view_id = action.get('view_id') or False
407 if isinstance(view_id, (list, tuple)):
410 # providing at least one view mode is a requirement, not an option
411 view_modes = action['view_mode'].split(',')
413 if len(view_modes) > 1:
415 raise ValueError('Non-db action dictionaries should provide '
416 'either multiple view modes or a single view '
417 'mode and an optional view id.\n\n Got view '
418 'modes %r and view id %r for action %r' % (
419 view_modes, view_id, action))
420 action['views'] = [(False, mode) for mode in view_modes]
422 action['views'] = [(view_id, view_modes[0])]
424 def fix_view_modes(action):
425 """ For historical reasons, OpenERP has weird dealings in relation to
426 view_mode and the view_type attribute (on window actions):
428 * one of the view modes is ``tree``, which stands for both list views
430 * the choice is made by checking ``view_type``, which is either
431 ``form`` for a list view or ``tree`` for an actual tree view
433 This methods simply folds the view_type into view_mode by adding a
434 new view mode ``list`` which is the result of the ``tree`` view_mode
435 in conjunction with the ``form`` view_type.
437 TODO: this should go into the doc, some kind of "peculiarities" section
439 :param dict action: an action descriptor
440 :returns: nothing, the action is modified in place
442 if not action.get('views'):
443 generate_views(action)
445 if action.pop('view_type', 'form') != 'form':
448 if 'view_mode' in action:
449 action['view_mode'] = ','.join(
450 mode if mode != 'tree' else 'list'
451 for mode in action['view_mode'].split(','))
453 [id, mode if mode != 'tree' else 'list']
454 for id, mode in action['views']
459 def parse_domain(domain, session):
460 """ Parses an arbitrary string containing a domain, transforms it
461 to either a literal domain or a :class:`nonliterals.Domain`
463 :param domain: the domain to parse, if the domain is not a string it
464 is assumed to be a literal domain and is returned as-is
465 :param session: Current OpenERP session
466 :type session: openerpweb.OpenERPSession
468 if not isinstance(domain, basestring):
471 return ast.literal_eval(domain)
474 return nonliterals.Domain(session, domain)
476 def parse_context(context, session):
477 """ Parses an arbitrary string containing a context, transforms it
478 to either a literal context or a :class:`nonliterals.Context`
480 :param context: the context to parse, if the context is not a string it
481 is assumed to be a literal domain and is returned as-is
482 :param session: Current OpenERP session
483 :type session: openerpweb.OpenERPSession
485 if not isinstance(context, basestring):
488 return ast.literal_eval(context)
490 return nonliterals.Context(session, context)
492 def _local_web_translations(trans_file):
495 with open(trans_file) as t_file:
496 po = babel.messages.pofile.read_po(t_file)
500 if x.id and x.string and "openerp-web" in x.auto_comments:
501 messages.append({'id': x.id, 'string': x.string})
504 def xml2json_from_elementtree(el, preserve_whitespaces=False):
506 Simple and straightforward XML-to-JSON converter in Python
508 http://code.google.com/p/xml2json-direct/
512 ns, name = el.tag.rsplit("}", 1)
514 res["namespace"] = ns[1:]
518 for k, v in el.items():
521 if el.text and (preserve_whitespaces or el.text.strip() != ''):
524 kids.append(xml2json_from_elementtree(kid, preserve_whitespaces))
525 if kid.tail and (preserve_whitespaces or kid.tail.strip() != ''):
526 kids.append(kid.tail)
527 res["children"] = kids
530 def content_disposition(filename, req):
531 filename = filename.encode('utf8')
532 escaped = urllib2.quote(filename)
533 browser = req.httprequest.user_agent.browser
534 version = int((req.httprequest.user_agent.version or '0').split('.')[0])
535 if browser == 'msie' and version < 9:
536 return "attachment; filename=%s" % escaped
537 elif browser == 'safari':
538 return "attachment; filename=%s" % filename
540 return "attachment; filename*=UTF-8''%s" % escaped
543 #----------------------------------------------------------
544 # OpenERP Web web Controllers
545 #----------------------------------------------------------
547 html_template = """<!DOCTYPE html>
548 <html style="height: 100%%">
550 <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
551 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
552 <title>OpenERP</title>
553 <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
554 <link rel="stylesheet" href="/web/static/src/css/full.css" />
557 <script type="text/javascript">
559 var s = new openerp.init(%(modules)s);
566 <script src="http://ajax.googleapis.com/ajax/libs/chrome-frame/1/CFInstall.min.js"></script>
568 var test = function() {
573 if (window.localStorage && false) {
574 if (! localStorage.getItem("hasShownGFramePopup")) {
576 localStorage.setItem("hasShownGFramePopup", true);
587 class Home(openerpweb.Controller):
590 @openerpweb.httprequest
591 def index(self, req, s_action=None, **kw):
592 js = "\n ".join('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list(req, None, 'js'))
593 css = "\n ".join('<link rel="stylesheet" href="%s">' % i for i in manifest_list(req, None, 'css'))
595 r = html_template % {
598 'modules': simplejson.dumps(module_boot(req)),
599 'init': 'var wc = new s.web.WebClient();wc.appendTo($(document.body));'
603 @openerpweb.httprequest
604 def login(self, req, db, login, key):
605 return login_and_redirect(req, db, login, key)
607 class WebClient(openerpweb.Controller):
608 _cp_path = "/web/webclient"
610 @openerpweb.jsonrequest
611 def csslist(self, req, mods=None):
612 return manifest_list(req, mods, 'css')
614 @openerpweb.jsonrequest
615 def jslist(self, req, mods=None):
616 return manifest_list(req, mods, 'js')
618 @openerpweb.jsonrequest
619 def qweblist(self, req, mods=None):
620 return manifest_list(req, mods, 'qweb')
622 @openerpweb.httprequest
623 def css(self, req, mods=None):
624 files = list(manifest_glob(req, mods, 'css'))
625 last_modified = get_last_modified(f[0] for f in files)
626 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
627 return werkzeug.wrappers.Response(status=304)
629 file_map = dict(files)
631 rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
632 rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
635 """read the a css file and absolutify all relative uris"""
636 with open(f, 'rb') as fp:
637 data = fp.read().decode('utf-8')
640 # convert FS path into web path
641 web_dir = '/'.join(os.path.dirname(path).split(os.path.sep))
645 r"""@import \1%s/""" % (web_dir,),
651 r"""url(\1%s/""" % (web_dir,),
654 return data.encode('utf-8')
656 content, checksum = concat_files((f[0] for f in files), reader)
658 return make_conditional(
659 req, req.make_response(content, [('Content-Type', 'text/css')]),
660 last_modified, checksum)
662 @openerpweb.httprequest
663 def js(self, req, mods=None):
664 files = [f[0] for f in manifest_glob(req, mods, 'js')]
665 last_modified = get_last_modified(files)
666 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
667 return werkzeug.wrappers.Response(status=304)
669 content, checksum = concat_js(files)
671 return make_conditional(
672 req, req.make_response(content, [('Content-Type', 'application/javascript')]),
673 last_modified, checksum)
675 @openerpweb.httprequest
676 def qweb(self, req, mods=None):
677 files = [f[0] for f in manifest_glob(req, mods, 'qweb')]
678 last_modified = get_last_modified(files)
679 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
680 return werkzeug.wrappers.Response(status=304)
682 content, checksum = concat_xml(files)
684 return make_conditional(
685 req, req.make_response(content, [('Content-Type', 'text/xml')]),
686 last_modified, checksum)
688 @openerpweb.jsonrequest
689 def bootstrap_translations(self, req, mods):
690 """ Load local translations from *.po files, as a temporary solution
691 until we have established a valid session. This is meant only
692 for translating the login page and db management chrome, using
693 the browser's language. """
694 # For performance reasons we only load a single translation, so for
695 # sub-languages (that should only be partially translated) we load the
696 # main language PO instead - that should be enough for the login screen.
697 lang = req.lang.split('_')[0]
699 translations_per_module = {}
700 for addon_name in mods:
701 if openerpweb.addons_manifest[addon_name].get('bootstrap'):
702 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
703 f_name = os.path.join(addons_path, addon_name, "i18n", lang + ".po")
704 if not os.path.exists(f_name):
706 translations_per_module[addon_name] = {'messages': _local_web_translations(f_name)}
708 return {"modules": translations_per_module,
709 "lang_parameters": None}
711 @openerpweb.jsonrequest
712 def translations(self, req, mods, lang):
713 res_lang = req.session.model('res.lang')
714 ids = res_lang.search([("code", "=", lang)])
717 lang_params = res_lang.read(ids[0], ["direction", "date_format", "time_format",
718 "grouping", "decimal_point", "thousands_sep"])
720 # Regional languages (ll_CC) must inherit/override their parent lang (ll), but this is
721 # done server-side when the language is loaded, so we only need to load the user's lang.
722 ir_translation = req.session.model('ir.translation')
723 translations_per_module = {}
724 messages = ir_translation.search_read([('module','in',mods),('lang','=',lang),
725 ('comments','like','openerp-web'),('value','!=',False),
727 ['module','src','value','lang'], order='module')
728 for mod, msg_group in itertools.groupby(messages, key=operator.itemgetter('module')):
729 translations_per_module.setdefault(mod,{'messages':[]})
730 translations_per_module[mod]['messages'].extend({'id': m['src'],
731 'string': m['value']} \
733 return {"modules": translations_per_module,
734 "lang_parameters": lang_params}
736 @openerpweb.jsonrequest
737 def version_info(self, req):
739 "version": openerp.release.version
742 class Proxy(openerpweb.Controller):
743 _cp_path = '/web/proxy'
745 @openerpweb.jsonrequest
746 def load(self, req, path):
747 """ Proxies an HTTP request through a JSON request.
749 It is strongly recommended to not request binary files through this,
750 as the result will be a binary data blob as well.
752 :param req: OpenERP request
753 :param path: actual request path
754 :return: file content
756 from werkzeug.test import Client
757 from werkzeug.wrappers import BaseResponse
759 return Client(req.httprequest.app, BaseResponse).get(path).data
761 class Database(openerpweb.Controller):
762 _cp_path = "/web/database"
764 @openerpweb.jsonrequest
765 def get_list(self, req):
768 @openerpweb.jsonrequest
769 def create(self, req, fields):
770 params = dict(map(operator.itemgetter('name', 'value'), fields))
772 params['super_admin_pwd'],
774 bool(params.get('demo_data')),
776 params['create_admin_pwd']
779 return req.session.proxy("db").create_database(*create_attrs)
781 @openerpweb.jsonrequest
782 def drop(self, req, fields):
783 password, db = operator.itemgetter(
784 'drop_pwd', 'drop_db')(
785 dict(map(operator.itemgetter('name', 'value'), fields)))
788 return req.session.proxy("db").drop(password, db)
789 except xmlrpclib.Fault, e:
790 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
791 return {'error': e.faultCode, 'title': 'Drop Database'}
792 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
794 @openerpweb.httprequest
795 def backup(self, req, backup_db, backup_pwd, token):
797 db_dump = base64.b64decode(
798 req.session.proxy("db").dump(backup_pwd, backup_db))
799 filename = "%(db)s_%(timestamp)s.dump" % {
801 'timestamp': datetime.datetime.utcnow().strftime(
802 "%Y-%m-%d_%H-%M-%SZ")
804 return req.make_response(db_dump,
805 [('Content-Type', 'application/octet-stream; charset=binary'),
806 ('Content-Disposition', content_disposition(filename, req))],
807 {'fileToken': int(token)}
809 except xmlrpclib.Fault, e:
810 return simplejson.dumps([[],[{'error': e.faultCode, 'title': 'backup Database'}]])
812 @openerpweb.httprequest
813 def restore(self, req, db_file, restore_pwd, new_db):
815 data = base64.b64encode(db_file.read())
816 req.session.proxy("db").restore(restore_pwd, new_db, data)
818 except xmlrpclib.Fault, e:
819 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
820 raise Exception("AccessDenied")
822 @openerpweb.jsonrequest
823 def change_password(self, req, fields):
824 old_password, new_password = operator.itemgetter(
825 'old_pwd', 'new_pwd')(
826 dict(map(operator.itemgetter('name', 'value'), fields)))
828 return req.session.proxy("db").change_admin_password(old_password, new_password)
829 except xmlrpclib.Fault, e:
830 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
831 return {'error': e.faultCode, 'title': 'Change Password'}
832 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
834 class Session(openerpweb.Controller):
835 _cp_path = "/web/session"
837 def session_info(self, req):
838 req.session.ensure_valid()
840 "session_id": req.session_id,
841 "uid": req.session._uid,
842 "context": req.session.get_context() if req.session._uid else {},
843 "db": req.session._db,
844 "login": req.session._login,
847 @openerpweb.jsonrequest
848 def get_session_info(self, req):
849 return self.session_info(req)
851 @openerpweb.jsonrequest
852 def authenticate(self, req, db, login, password, base_location=None):
853 wsgienv = req.httprequest.environ
855 base_location=base_location,
856 HTTP_HOST=wsgienv['HTTP_HOST'],
857 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
859 req.session.authenticate(db, login, password, env)
861 return self.session_info(req)
863 @openerpweb.jsonrequest
864 def change_password (self,req,fields):
865 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
866 dict(map(operator.itemgetter('name', 'value'), fields)))
867 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
868 return {'error':'All passwords have to be filled.','title': 'Change Password'}
869 if new_password != confirm_password:
870 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
872 if req.session.model('res.users').change_password(
873 old_password, new_password):
874 return {'new_password':new_password}
876 return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
877 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
879 @openerpweb.jsonrequest
880 def sc_list(self, req):
881 return req.session.model('ir.ui.view_sc').get_sc(
882 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
884 @openerpweb.jsonrequest
885 def get_lang_list(self, req):
888 'lang_list': (req.session.proxy("db").list_lang() or []),
892 return {"error": e, "title": "Languages"}
894 @openerpweb.jsonrequest
895 def modules(self, req):
896 # return all installed modules. Web client is smart enough to not load a module twice
897 return module_installed(req)
899 @openerpweb.jsonrequest
900 def eval_domain_and_context(self, req, contexts, domains,
902 """ Evaluates sequences of domains and contexts, composing them into
903 a single context, domain or group_by sequence.
905 :param list contexts: list of contexts to merge together. Contexts are
906 evaluated in sequence, all previous contexts
907 are part of their own evaluation context
908 (starting at the session context).
909 :param list domains: list of domains to merge together. Domains are
910 evaluated in sequence and appended to one another
911 (implicit AND), their evaluation domain is the
912 result of merging all contexts.
913 :param list group_by_seq: list of domains (which may be in a different
914 order than the ``contexts`` parameter),
915 evaluated in sequence, their ``'group_by'``
916 key is extracted if they have one.
921 the global context created by merging all of
925 the concatenation of all domains
928 a list of fields to group by, potentially empty (in which case
929 no group by should be performed)
931 context, domain = eval_context_and_domain(req.session,
932 nonliterals.CompoundContext(*(contexts or [])),
933 nonliterals.CompoundDomain(*(domains or [])))
935 group_by_sequence = []
936 for candidate in (group_by_seq or []):
937 ctx = req.session.eval_context(candidate, context)
938 group_by = ctx.get('group_by')
941 elif isinstance(group_by, basestring):
942 group_by_sequence.append(group_by)
944 group_by_sequence.extend(group_by)
949 'group_by': group_by_sequence
952 @openerpweb.jsonrequest
953 def save_session_action(self, req, the_action):
955 This method store an action object in the session object and returns an integer
956 identifying that action. The method get_session_action() can be used to get
959 :param the_action: The action to save in the session.
960 :type the_action: anything
961 :return: A key identifying the saved action.
964 saved_actions = req.httpsession.get('saved_actions')
965 if not saved_actions:
966 saved_actions = {"next":0, "actions":{}}
967 req.httpsession['saved_actions'] = saved_actions
968 # we don't allow more than 10 stored actions
969 if len(saved_actions["actions"]) >= 10:
970 del saved_actions["actions"][min(saved_actions["actions"])]
971 key = saved_actions["next"]
972 saved_actions["actions"][key] = the_action
973 saved_actions["next"] = key + 1
976 @openerpweb.jsonrequest
977 def get_session_action(self, req, key):
979 Gets back a previously saved action. This method can return None if the action
980 was saved since too much time (this case should be handled in a smart way).
982 :param key: The key given by save_session_action()
984 :return: The saved action or None.
987 saved_actions = req.httpsession.get('saved_actions')
988 if not saved_actions:
990 return saved_actions["actions"].get(key)
992 @openerpweb.jsonrequest
993 def check(self, req):
994 req.session.assert_valid()
997 @openerpweb.jsonrequest
998 def destroy(self, req):
999 req.session._suicide = True
1001 class Menu(openerpweb.Controller):
1002 _cp_path = "/web/menu"
1004 @openerpweb.jsonrequest
1005 def load(self, req):
1006 return {'data': self.do_load(req)}
1008 @openerpweb.jsonrequest
1009 def get_user_roots(self, req):
1010 return self.do_get_user_roots(req)
1012 def do_get_user_roots(self, req):
1013 """ Return all root menu ids visible for the session user.
1015 :param req: A request object, with an OpenERP session attribute
1016 :type req: < session -> OpenERPSession >
1017 :return: the root menu ids
1021 context = s.eval_context(req.context)
1022 Menus = s.model('ir.ui.menu')
1023 # If a menu action is defined use its domain to get the root menu items
1024 user_menu_id = s.model('res.users').read([s._uid], ['menu_id'], context)[0]['menu_id']
1026 menu_domain = [('parent_id', '=', False)]
1028 domain_string = s.model('ir.actions.act_window').read([user_menu_id[0]], ['domain'], context)[0]['domain']
1030 menu_domain = ast.literal_eval(domain_string)
1032 return Menus.search(menu_domain, 0, False, False, context)
1034 def do_load(self, req):
1035 """ Loads all menu items (all applications and their sub-menus).
1037 :param req: A request object, with an OpenERP session attribute
1038 :type req: < session -> OpenERPSession >
1039 :return: the menu root
1040 :rtype: dict('children': menu_nodes)
1042 context = req.session.eval_context(req.context)
1043 Menus = req.session.model('ir.ui.menu')
1045 menu_roots = Menus.read(self.do_get_user_roots(req), ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1046 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children' : menu_roots}
1048 # menus are loaded fully unlike a regular tree view, cause there are a
1049 # limited number of items (752 when all 6.1 addons are installed)
1050 menu_ids = Menus.search([], 0, False, False, context)
1051 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1052 # adds roots at the end of the sequence, so that they will overwrite
1053 # equivalent menu items from full menu read when put into id:item
1054 # mapping, resulting in children being correctly set on the roots.
1055 menu_items.extend(menu_roots)
1057 # make a tree using parent_id
1058 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
1059 for menu_item in menu_items:
1060 if menu_item['parent_id']:
1061 parent = menu_item['parent_id'][0]
1064 if parent in menu_items_map:
1065 menu_items_map[parent].setdefault(
1066 'children', []).append(menu_item)
1068 # sort by sequence a tree using parent_id
1069 for menu_item in menu_items:
1070 menu_item.setdefault('children', []).sort(
1071 key=operator.itemgetter('sequence'))
1075 @openerpweb.jsonrequest
1076 def action(self, req, menu_id):
1077 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
1078 [('ir.ui.menu', menu_id)], False)
1079 return {"action": actions}
1081 class DataSet(openerpweb.Controller):
1082 _cp_path = "/web/dataset"
1084 @openerpweb.jsonrequest
1085 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
1086 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
1087 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
1089 """ Performs a search() followed by a read() (if needed) using the
1090 provided search criteria
1092 :param req: a JSON-RPC request object
1093 :type req: openerpweb.JsonRequest
1094 :param str model: the name of the model to search on
1095 :param fields: a list of the fields to return in the result records
1097 :param int offset: from which index should the results start being returned
1098 :param int limit: the maximum number of records to return
1099 :param list domain: the search domain for the query
1100 :param list sort: sorting directives
1101 :returns: A structure (dict) with two keys: ids (all the ids matching
1102 the (domain, context) pair) and records (paginated records
1103 matching fields selection set)
1106 Model = req.session.model(model)
1108 context, domain = eval_context_and_domain(
1109 req.session, req.context, domain)
1111 ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
1112 if limit and len(ids) == limit:
1113 length = Model.search_count(domain, context)
1115 length = len(ids) + (offset or 0)
1116 if fields and fields == ['id']:
1117 # shortcut read if we only want the ids
1120 'records': [{'id': id} for id in ids]
1123 records = Model.read(ids, fields or False, context)
1124 records.sort(key=lambda obj: ids.index(obj['id']))
1130 @openerpweb.jsonrequest
1131 def load(self, req, model, id, fields):
1132 m = req.session.model(model)
1134 r = m.read([id], False, req.session.eval_context(req.context))
1137 return {'value': value}
1139 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
1140 has_domain = domain_id is not None and domain_id < len(args)
1141 has_context = context_id is not None and context_id < len(args)
1143 domain = args[domain_id] if has_domain else []
1144 context = args[context_id] if has_context else {}
1145 c, d = eval_context_and_domain(req.session, context, domain)
1149 args[context_id] = c
1151 return self._call_kw(req, model, method, args, {})
1153 def _call_kw(self, req, model, method, args, kwargs):
1154 for i in xrange(len(args)):
1155 if isinstance(args[i], nonliterals.BaseContext):
1156 args[i] = req.session.eval_context(args[i])
1157 elif isinstance(args[i], nonliterals.BaseDomain):
1158 args[i] = req.session.eval_domain(args[i])
1159 for k in kwargs.keys():
1160 if isinstance(kwargs[k], nonliterals.BaseContext):
1161 kwargs[k] = req.session.eval_context(kwargs[k])
1162 elif isinstance(kwargs[k], nonliterals.BaseDomain):
1163 kwargs[k] = req.session.eval_domain(kwargs[k])
1165 # Temporary implements future display_name special field for model#read()
1166 if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
1167 if 'display_name' in args[1]:
1168 names = req.session.model(model).name_get(args[0], **kwargs)
1169 args[1].remove('display_name')
1170 r = getattr(req.session.model(model), method)(*args, **kwargs)
1171 for i in range(len(r)):
1172 r[i]['display_name'] = names[i][1] or "%s#%d" % (model, names[i][0])
1175 return getattr(req.session.model(model), method)(*args, **kwargs)
1177 @openerpweb.jsonrequest
1178 def onchange(self, req, model, method, args, context_id=None):
1179 """ Support method for handling onchange calls: behaves much like call
1180 with the following differences:
1182 * Does not take a domain_id
1183 * Is aware of the return value's structure, and will parse the domains
1184 if needed in order to return either parsed literal domains (in JSON)
1185 or non-literal domain instances, allowing those domains to be used
1189 :type req: web.common.http.JsonRequest
1190 :param str model: object type on which to call the method
1191 :param str method: name of the onchange handler method
1192 :param list args: arguments to call the onchange handler with
1193 :param int context_id: index of the context object in the list of
1195 :return: result of the onchange call with all domains parsed
1197 result = self.call_common(req, model, method, args, context_id=context_id)
1198 if not result or 'domain' not in result:
1201 result['domain'] = dict(
1202 (k, parse_domain(v, req.session))
1203 for k, v in result['domain'].iteritems())
1207 @openerpweb.jsonrequest
1208 def call(self, req, model, method, args, domain_id=None, context_id=None):
1209 return self.call_common(req, model, method, args, domain_id, context_id)
1211 @openerpweb.jsonrequest
1212 def call_kw(self, req, model, method, args, kwargs):
1213 return self._call_kw(req, model, method, args, kwargs)
1215 @openerpweb.jsonrequest
1216 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1217 action = self.call_common(req, model, method, args, domain_id, context_id)
1218 if isinstance(action, dict) and action.get('type') != '':
1219 return clean_action(req, action)
1222 @openerpweb.jsonrequest
1223 def exec_workflow(self, req, model, id, signal):
1224 return req.session.exec_workflow(model, id, signal)
1226 @openerpweb.jsonrequest
1227 def resequence(self, req, model, ids, field='sequence', offset=0):
1228 """ Re-sequences a number of records in the model, by their ids
1230 The re-sequencing starts at the first model of ``ids``, the sequence
1231 number is incremented by one after each record and starts at ``offset``
1233 :param ids: identifiers of the records to resequence, in the new sequence order
1235 :param str field: field used for sequence specification, defaults to
1237 :param int offset: sequence number for first record in ``ids``, allows
1238 starting the resequencing from an arbitrary number,
1241 m = req.session.model(model)
1242 if not m.fields_get([field]):
1244 # python 2.6 has no start parameter
1245 for i, id in enumerate(ids):
1246 m.write(id, { field: i + offset })
1249 class View(openerpweb.Controller):
1250 _cp_path = "/web/view"
1252 def fields_view_get(self, req, model, view_id, view_type,
1253 transform=True, toolbar=False, submenu=False):
1254 Model = req.session.model(model)
1255 context = req.session.eval_context(req.context)
1256 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
1257 # todo fme?: check that we should pass the evaluated context here
1258 self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
1259 if toolbar and transform:
1260 self.process_toolbar(req, fvg['toolbar'])
1263 def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
1264 # depending on how it feels, xmlrpclib.ServerProxy can translate
1265 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
1266 # enjoy unicode strings which can not be trivially converted to
1267 # strings, and it blows up during parsing.
1269 # So ensure we fix this retardation by converting view xml back to
1271 if isinstance(fvg['arch'], unicode):
1272 arch = fvg['arch'].encode('utf-8')
1275 fvg['arch_string'] = arch
1278 evaluation_context = session.evaluation_context(context or {})
1279 xml = self.transform_view(arch, session, evaluation_context)
1281 xml = ElementTree.fromstring(arch)
1282 fvg['arch'] = xml2json_from_elementtree(xml, preserve_whitespaces)
1284 if 'id' in fvg['fields']:
1285 # Special case for id's
1286 id_field = fvg['fields']['id']
1287 id_field['original_type'] = id_field['type']
1288 id_field['type'] = 'id'
1290 for field in fvg['fields'].itervalues():
1291 if field.get('views'):
1292 for view in field["views"].itervalues():
1293 self.process_view(session, view, None, transform)
1294 if field.get('domain'):
1295 field["domain"] = parse_domain(field["domain"], session)
1296 if field.get('context'):
1297 field["context"] = parse_context(field["context"], session)
1299 def process_toolbar(self, req, toolbar):
1301 The toolbar is a mapping of section_key: [action_descriptor]
1303 We need to clean all those actions in order to ensure correct
1306 for actions in toolbar.itervalues():
1307 for action in actions:
1308 if 'context' in action:
1309 action['context'] = parse_context(
1310 action['context'], req.session)
1311 if 'domain' in action:
1312 action['domain'] = parse_domain(
1313 action['domain'], req.session)
1315 @openerpweb.jsonrequest
1316 def add_custom(self, req, view_id, arch):
1317 CustomView = req.session.model('ir.ui.view.custom')
1319 'user_id': req.session._uid,
1322 }, req.session.eval_context(req.context))
1323 return {'result': True}
1325 @openerpweb.jsonrequest
1326 def undo_custom(self, req, view_id, reset=False):
1327 CustomView = req.session.model('ir.ui.view.custom')
1328 context = req.session.eval_context(req.context)
1329 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1330 0, False, False, context)
1333 CustomView.unlink(vcustom, context)
1335 CustomView.unlink([vcustom[0]], context)
1336 return {'result': True}
1337 return {'result': False}
1339 def transform_view(self, view_string, session, context=None):
1340 # transform nodes on the fly via iterparse, instead of
1341 # doing it statically on the parsing result
1342 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
1344 for event, elem in parser:
1345 if event == "start":
1348 self.parse_domains_and_contexts(elem, session)
1351 def parse_domains_and_contexts(self, elem, session):
1352 """ Converts domains and contexts from the view into Python objects,
1353 either literals if they can be parsed by literal_eval or a special
1354 placeholder object if the domain or context refers to free variables.
1356 :param elem: the current node being parsed
1357 :type param: xml.etree.ElementTree.Element
1358 :param session: OpenERP session object, used to store and retrieve
1360 :type session: openerpweb.openerpweb.OpenERPSession
1362 for el in ['domain', 'filter_domain']:
1363 domain = elem.get(el, '').strip()
1365 elem.set(el, parse_domain(domain, session))
1366 elem.set(el + '_string', domain)
1367 for el in ['context', 'default_get']:
1368 context_string = elem.get(el, '').strip()
1370 elem.set(el, parse_context(context_string, session))
1371 elem.set(el + '_string', context_string)
1373 @openerpweb.jsonrequest
1374 def load(self, req, model, view_id, view_type, toolbar=False):
1375 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1377 class TreeView(View):
1378 _cp_path = "/web/treeview"
1380 @openerpweb.jsonrequest
1381 def action(self, req, model, id):
1382 return load_actions_from_ir_values(
1383 req,'action', 'tree_but_open',[(model, id)],
1386 class SearchView(View):
1387 _cp_path = "/web/searchview"
1389 @openerpweb.jsonrequest
1390 def load(self, req, model, view_id):
1391 fields_view = self.fields_view_get(req, model, view_id, 'search')
1392 return {'fields_view': fields_view}
1394 @openerpweb.jsonrequest
1395 def fields_get(self, req, model):
1396 Model = req.session.model(model)
1397 fields = Model.fields_get(False, req.session.eval_context(req.context))
1398 for field in fields.values():
1399 # shouldn't convert the views too?
1400 if field.get('domain'):
1401 field["domain"] = parse_domain(field["domain"], req.session)
1402 if field.get('context'):
1403 field["context"] = parse_context(field["context"], req.session)
1404 return {'fields': fields}
1406 @openerpweb.jsonrequest
1407 def get_filters(self, req, model):
1408 logger = logging.getLogger(__name__ + '.SearchView.get_filters')
1409 Model = req.session.model("ir.filters")
1410 filters = Model.get_filters(model)
1411 for filter in filters:
1413 parsed_context = parse_context(filter["context"], req.session)
1414 filter["context"] = (parsed_context
1415 if not isinstance(parsed_context, nonliterals.BaseContext)
1416 else req.session.eval_context(parsed_context))
1418 parsed_domain = parse_domain(filter["domain"], req.session)
1419 filter["domain"] = (parsed_domain
1420 if not isinstance(parsed_domain, nonliterals.BaseDomain)
1421 else req.session.eval_domain(parsed_domain))
1423 logger.exception("Failed to parse custom filter %s in %s",
1424 filter['name'], model)
1425 filter['disabled'] = True
1426 del filter['context']
1427 del filter['domain']
1430 class Binary(openerpweb.Controller):
1431 _cp_path = "/web/binary"
1433 @openerpweb.httprequest
1434 def image(self, req, model, id, field, **kw):
1435 last_update = '__last_update'
1436 Model = req.session.model(model)
1437 context = req.session.eval_context(req.context)
1438 headers = [('Content-Type', 'image/png')]
1439 etag = req.httprequest.headers.get('If-None-Match')
1440 hashed_session = hashlib.md5(req.session_id).hexdigest()
1441 id = None if not id else simplejson.loads(id)
1442 if type(id) is list:
1445 if not id and hashed_session == etag:
1446 return werkzeug.wrappers.Response(status=304)
1448 date = Model.read([id], [last_update], context)[0].get(last_update)
1449 if hashlib.md5(date).hexdigest() == etag:
1450 return werkzeug.wrappers.Response(status=304)
1452 retag = hashed_session
1455 res = Model.default_get([field], context).get(field)
1458 res = Model.read([id], [last_update, field], context)[0]
1459 retag = hashlib.md5(res.get(last_update)).hexdigest()
1460 image_base64 = res.get(field)
1462 if kw.get('resize'):
1463 resize = kw.get('resize').split(',');
1464 if len(resize) == 2 and int(resize[0]) and int(resize[1]):
1465 width = int(resize[0])
1466 height = int(resize[1])
1467 # resize maximum 500*500
1468 if width > 500: width = 500
1469 if height > 500: height = 500
1470 image_base64 = openerp.tools.image_resize_image(base64_source=image_base64, size=(width, height), encoding='base64', filetype='PNG')
1472 image_data = base64.b64decode(image_base64)
1474 except (TypeError, xmlrpclib.Fault):
1475 image_data = self.placeholder(req)
1476 headers.append(('ETag', retag))
1477 headers.append(('Content-Length', len(image_data)))
1479 ncache = int(kw.get('cache'))
1480 headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1483 return req.make_response(image_data, headers)
1484 def placeholder(self, req):
1485 addons_path = openerpweb.addons_manifest['web']['addons_path']
1486 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1488 @openerpweb.httprequest
1489 def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1490 """ Download link for files stored as binary fields.
1492 If the ``id`` parameter is omitted, fetches the default value for the
1493 binary field (via ``default_get``), otherwise fetches the field for
1494 that precise record.
1496 :param req: OpenERP request
1497 :type req: :class:`web.common.http.HttpRequest`
1498 :param str model: name of the model to fetch the binary from
1499 :param str field: binary field
1500 :param str id: id of the record from which to fetch the binary
1501 :param str filename_field: field holding the file's name, if any
1502 :returns: :class:`werkzeug.wrappers.Response`
1504 Model = req.session.model(model)
1505 context = req.session.eval_context(req.context)
1508 fields.append(filename_field)
1510 res = Model.read([int(id)], fields, context)[0]
1512 res = Model.default_get(fields, context)
1513 filecontent = base64.b64decode(res.get(field, ''))
1515 return req.not_found()
1517 filename = '%s_%s' % (model.replace('.', '_'), id)
1519 filename = res.get(filename_field, '') or filename
1520 return req.make_response(filecontent,
1521 [('Content-Type', 'application/octet-stream'),
1522 ('Content-Disposition', content_disposition(filename, req))])
1524 @openerpweb.httprequest
1525 def saveas_ajax(self, req, data, token):
1526 jdata = simplejson.loads(data)
1527 model = jdata['model']
1528 field = jdata['field']
1529 id = jdata.get('id', None)
1530 filename_field = jdata.get('filename_field', None)
1531 context = jdata.get('context', dict())
1533 context = req.session.eval_context(context)
1534 Model = req.session.model(model)
1537 fields.append(filename_field)
1539 res = Model.read([int(id)], fields, context)[0]
1541 res = Model.default_get(fields, context)
1542 filecontent = base64.b64decode(res.get(field, ''))
1544 raise ValueError("No content found for field '%s' on '%s:%s'" %
1547 filename = '%s_%s' % (model.replace('.', '_'), id)
1549 filename = res.get(filename_field, '') or filename
1550 return req.make_response(filecontent,
1551 headers=[('Content-Type', 'application/octet-stream'),
1552 ('Content-Disposition', content_disposition(filename, req))],
1553 cookies={'fileToken': int(token)})
1555 @openerpweb.httprequest
1556 def upload(self, req, callback, ufile):
1557 # TODO: might be useful to have a configuration flag for max-length file uploads
1559 out = """<script language="javascript" type="text/javascript">
1560 var win = window.top.window;
1561 win.jQuery(win).trigger(%s, %s);
1564 args = [len(data), ufile.filename,
1565 ufile.content_type, base64.b64encode(data)]
1566 except Exception, e:
1567 args = [False, e.message]
1568 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1570 @openerpweb.httprequest
1571 def upload_attachment(self, req, callback, model, id, ufile):
1572 context = req.session.eval_context(req.context)
1573 Model = req.session.model('ir.attachment')
1575 out = """<script language="javascript" type="text/javascript">
1576 var win = window.top.window;
1577 win.jQuery(win).trigger(%s, %s);
1579 attachment_id = Model.create({
1580 'name': ufile.filename,
1581 'datas': base64.encodestring(ufile.read()),
1582 'datas_fname': ufile.filename,
1587 'filename': ufile.filename,
1590 except Exception, e:
1591 args = { 'error': e.message }
1592 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1594 class Action(openerpweb.Controller):
1595 _cp_path = "/web/action"
1597 @openerpweb.jsonrequest
1598 def load(self, req, action_id, do_not_eval=False):
1599 Actions = req.session.model('ir.actions.actions')
1601 context = req.session.eval_context(req.context)
1604 action_id = int(action_id)
1607 module, xmlid = action_id.split('.', 1)
1608 model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
1609 assert model.startswith('ir.actions.')
1611 action_id = 0 # force failed read
1613 base_action = Actions.read([action_id], ['type'], context)
1616 action_type = base_action[0]['type']
1617 if action_type == 'ir.actions.report.xml':
1618 ctx.update({'bin_size': True})
1620 action = req.session.model(action_type).read([action_id], False, ctx)
1622 value = clean_action(req, action[0], do_not_eval)
1625 @openerpweb.jsonrequest
1626 def run(self, req, action_id):
1627 return_action = req.session.model('ir.actions.server').run(
1628 [action_id], req.session.eval_context(req.context))
1630 return clean_action(req, return_action)
1635 _cp_path = "/web/export"
1637 @openerpweb.jsonrequest
1638 def formats(self, req):
1639 """ Returns all valid export formats
1641 :returns: for each export format, a pair of identifier and printable name
1642 :rtype: [(str, str)]
1646 for path, controller in openerpweb.controllers_path.iteritems()
1647 if path.startswith(self._cp_path)
1648 if hasattr(controller, 'fmt')
1649 ], key=operator.itemgetter("label"))
1651 def fields_get(self, req, model):
1652 Model = req.session.model(model)
1653 fields = Model.fields_get(False, req.session.eval_context(req.context))
1656 @openerpweb.jsonrequest
1657 def get_fields(self, req, model, prefix='', parent_name= '',
1658 import_compat=True, parent_field_type=None,
1661 if import_compat and parent_field_type == "many2one":
1664 fields = self.fields_get(req, model)
1667 fields.pop('id', None)
1669 fields['.id'] = fields.pop('id', {'string': 'ID'})
1671 fields_sequence = sorted(fields.iteritems(),
1672 key=lambda field: field[1].get('string', ''))
1675 for field_name, field in fields_sequence:
1677 if exclude and field_name in exclude:
1679 if field.get('readonly'):
1680 # If none of the field's states unsets readonly, skip the field
1681 if all(dict(attrs).get('readonly', True)
1682 for attrs in field.get('states', {}).values()):
1685 id = prefix + (prefix and '/'or '') + field_name
1686 name = parent_name + (parent_name and '/' or '') + field['string']
1687 record = {'id': id, 'string': name,
1688 'value': id, 'children': False,
1689 'field_type': field.get('type'),
1690 'required': field.get('required'),
1691 'relation_field': field.get('relation_field')}
1692 records.append(record)
1694 if len(name.split('/')) < 3 and 'relation' in field:
1695 ref = field.pop('relation')
1696 record['value'] += '/id'
1697 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1699 if not import_compat or field['type'] == 'one2many':
1700 # m2m field in import_compat is childless
1701 record['children'] = True
1705 @openerpweb.jsonrequest
1706 def namelist(self,req, model, export_id):
1707 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1708 export = req.session.model("ir.exports").read([export_id])[0]
1709 export_fields_list = req.session.model("ir.exports.line").read(
1710 export['export_fields'])
1712 fields_data = self.fields_info(
1713 req, model, map(operator.itemgetter('name'), export_fields_list))
1716 {'name': field['name'], 'label': fields_data[field['name']]}
1717 for field in export_fields_list
1720 def fields_info(self, req, model, export_fields):
1722 fields = self.fields_get(req, model)
1724 # To make fields retrieval more efficient, fetch all sub-fields of a
1725 # given field at the same time. Because the order in the export list is
1726 # arbitrary, this requires ordering all sub-fields of a given field
1727 # together so they can be fetched at the same time
1729 # Works the following way:
1730 # * sort the list of fields to export, the default sorting order will
1731 # put the field itself (if present, for xmlid) and all of its
1732 # sub-fields right after it
1733 # * then, group on: the first field of the path (which is the same for
1734 # a field and for its subfields and the length of splitting on the
1735 # first '/', which basically means grouping the field on one side and
1736 # all of the subfields on the other. This way, we have the field (for
1737 # the xmlid) with length 1, and all of the subfields with the same
1738 # base but a length "flag" of 2
1739 # * if we have a normal field (length 1), just add it to the info
1740 # mapping (with its string) as-is
1741 # * otherwise, recursively call fields_info via graft_subfields.
1742 # all graft_subfields does is take the result of fields_info (on the
1743 # field's model) and prepend the current base (current field), which
1744 # rebuilds the whole sub-tree for the field
1746 # result: because we're not fetching the fields_get for half the
1747 # database models, fetching a namelist with a dozen fields (including
1748 # relational data) falls from ~6s to ~300ms (on the leads model).
1749 # export lists with no sub-fields (e.g. import_compatible lists with
1750 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1751 # there's a single fields_get to execute)
1752 for (base, length), subfields in itertools.groupby(
1753 sorted(export_fields),
1754 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1755 subfields = list(subfields)
1757 # subfields is a seq of $base/*rest, and not loaded yet
1758 info.update(self.graft_subfields(
1759 req, fields[base]['relation'], base, fields[base]['string'],
1763 info[base] = fields[base]['string']
1767 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1768 export_fields = [field.split('/', 1)[1] for field in fields]
1770 (prefix + '/' + k, prefix_string + '/' + v)
1771 for k, v in self.fields_info(req, model, export_fields).iteritems())
1773 #noinspection PyPropertyDefinition
1775 def content_type(self):
1776 """ Provides the format's content type """
1777 raise NotImplementedError()
1779 def filename(self, base):
1780 """ Creates a valid filename for the format (with extension) from the
1781 provided base name (exension-less)
1783 raise NotImplementedError()
1785 def from_data(self, fields, rows):
1786 """ Conversion method from OpenERP's export data to whatever the
1787 current export class outputs
1789 :params list fields: a list of fields to export
1790 :params list rows: a list of records to export
1794 raise NotImplementedError()
1796 @openerpweb.httprequest
1797 def index(self, req, data, token):
1798 model, fields, ids, domain, import_compat = \
1799 operator.itemgetter('model', 'fields', 'ids', 'domain',
1801 simplejson.loads(data))
1803 context = req.session.eval_context(req.context)
1804 Model = req.session.model(model)
1805 ids = ids or Model.search(domain, 0, False, False, context)
1807 field_names = map(operator.itemgetter('name'), fields)
1808 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1811 columns_headers = field_names
1813 columns_headers = [val['label'].strip() for val in fields]
1816 return req.make_response(self.from_data(columns_headers, import_data),
1817 headers=[('Content-Disposition',
1818 content_disposition(self.filename(model), req)),
1819 ('Content-Type', self.content_type)],
1820 cookies={'fileToken': int(token)})
1822 class CSVExport(Export):
1823 _cp_path = '/web/export/csv'
1824 fmt = {'tag': 'csv', 'label': 'CSV'}
1827 def content_type(self):
1828 return 'text/csv;charset=utf8'
1830 def filename(self, base):
1831 return base + '.csv'
1833 def from_data(self, fields, rows):
1835 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1837 writer.writerow([name.encode('utf-8') for name in fields])
1842 if isinstance(d, basestring):
1843 d = d.replace('\n',' ').replace('\t',' ')
1845 d = d.encode('utf-8')
1846 except UnicodeError:
1848 if d is False: d = None
1850 writer.writerow(row)
1857 class ExcelExport(Export):
1858 _cp_path = '/web/export/xls'
1862 'error': None if xlwt else "XLWT required"
1866 def content_type(self):
1867 return 'application/vnd.ms-excel'
1869 def filename(self, base):
1870 return base + '.xls'
1872 def from_data(self, fields, rows):
1873 workbook = xlwt.Workbook()
1874 worksheet = workbook.add_sheet('Sheet 1')
1876 for i, fieldname in enumerate(fields):
1877 worksheet.write(0, i, fieldname)
1878 worksheet.col(i).width = 8000 # around 220 pixels
1880 style = xlwt.easyxf('align: wrap yes')
1882 for row_index, row in enumerate(rows):
1883 for cell_index, cell_value in enumerate(row):
1884 if isinstance(cell_value, basestring):
1885 cell_value = re.sub("\r", " ", cell_value)
1886 if cell_value is False: cell_value = None
1887 worksheet.write(row_index + 1, cell_index, cell_value, style)
1896 class Reports(View):
1897 _cp_path = "/web/report"
1898 POLLING_DELAY = 0.25
1900 'doc': 'application/vnd.ms-word',
1901 'html': 'text/html',
1902 'odt': 'application/vnd.oasis.opendocument.text',
1903 'pdf': 'application/pdf',
1904 'sxw': 'application/vnd.sun.xml.writer',
1905 'xls': 'application/vnd.ms-excel',
1908 @openerpweb.httprequest
1909 def index(self, req, action, token):
1910 action = simplejson.loads(action)
1912 report_srv = req.session.proxy("report")
1913 context = req.session.eval_context(
1914 nonliterals.CompoundContext(
1915 req.context or {}, action[ "context"]))
1918 report_ids = context["active_ids"]
1919 if 'report_type' in action:
1920 report_data['report_type'] = action['report_type']
1921 if 'datas' in action:
1922 if 'ids' in action['datas']:
1923 report_ids = action['datas'].pop('ids')
1924 report_data.update(action['datas'])
1926 report_id = report_srv.report(
1927 req.session._db, req.session._uid, req.session._password,
1928 action["report_name"], report_ids,
1929 report_data, context)
1931 report_struct = None
1933 report_struct = report_srv.report_get(
1934 req.session._db, req.session._uid, req.session._password, report_id)
1935 if report_struct["state"]:
1938 time.sleep(self.POLLING_DELAY)
1940 report = base64.b64decode(report_struct['result'])
1941 if report_struct.get('code') == 'zlib':
1942 report = zlib.decompress(report)
1943 report_mimetype = self.TYPES_MAPPING.get(
1944 report_struct['format'], 'octet-stream')
1945 file_name = action.get('name', 'report')
1946 if 'name' not in action:
1947 reports = req.session.model('ir.actions.report.xml')
1948 res_id = reports.search([('report_name', '=', action['report_name']),],
1949 0, False, False, context)
1951 file_name = reports.read(res_id[0], ['name'], context)['name']
1953 file_name = action['report_name']
1954 file_name = '%s.%s' % (file_name, report_struct['format'])
1956 return req.make_response(report,
1958 ('Content-Disposition', content_disposition(file_name, req)),
1959 ('Content-Type', report_mimetype),
1960 ('Content-Length', len(report))],
1961 cookies={'fileToken': int(token)})
1963 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: