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
85 proxy = req.session.proxy("db")
87 h = req.httprequest.environ['HTTP_HOST'].split(':')[0]
89 r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
90 dbs = [i for i in dbs if re.match(r, i)]
94 # if only one db exists, return it else return False
99 except xmlrpclib.Fault:
100 # ignore access denied
104 def module_topological_sort(modules):
105 """ Return a list of module names sorted so that their dependencies of the
106 modules are listed before the module itself
108 modules is a dict of {module_name: dependencies}
110 :param modules: modules to sort
115 dependencies = set(itertools.chain.from_iterable(modules.itervalues()))
116 # incoming edge: dependency on other module (if a depends on b, a has an
117 # incoming edge from b, aka there's an edge from b to a)
118 # outgoing edge: other module depending on this one
120 # [Tarjan 1976], http://en.wikipedia.org/wiki/Topological_sorting#Algorithms
121 #L ← Empty list that will contain the sorted nodes
123 #S ← Set of all nodes with no outgoing edges (modules on which no other
125 S = set(module for module in modules if module not in dependencies)
128 #function visit(node n)
130 #if n has not been visited yet then
134 #change: n not web module, can not be resolved, ignore
135 if n not in modules: return
136 #for each node m with an edge from m to n do (dependencies of n)
142 #for each node n in S do
148 def module_installed(req):
149 # Candidates module the current heuristic is the /static dir
150 loadable = openerpweb.addons_manifest.keys()
153 # Retrieve database installed modules
154 # TODO The following code should move to ir.module.module.list_installed_modules()
155 Modules = req.session.model('ir.module.module')
156 domain = [('state','=','installed'), ('name','in', loadable)]
157 for module in Modules.search_read(domain, ['name', 'dependencies_id']):
158 modules[module['name']] = []
159 deps = module.get('dependencies_id')
161 deps_read = req.session.model('ir.module.module.dependency').read(deps, ['name'])
162 dependencies = [i['name'] for i in deps_read]
163 modules[module['name']] = dependencies
165 sorted_modules = module_topological_sort(modules)
166 return sorted_modules
168 def module_installed_bypass_session(dbname):
169 loadable = openerpweb.addons_manifest.keys()
172 import openerp.modules.registry
173 registry = openerp.modules.registry.RegistryManager.get(dbname)
174 with registry.cursor() as cr:
175 m = registry.get('ir.module.module')
176 # TODO The following code should move to ir.module.module.list_installed_modules()
177 domain = [('state','=','installed'), ('name','in', loadable)]
178 ids = m.search(cr, 1, [('state','=','installed'), ('name','in', loadable)])
179 for module in m.read(cr, 1, ids, ['name', 'dependencies_id']):
180 modules[module['name']] = []
181 deps = module.get('dependencies_id')
183 deps_read = registry.get('ir.module.module.dependency').read(cr, 1, deps, ['name'])
184 dependencies = [i['name'] for i in deps_read]
185 modules[module['name']] = dependencies
188 sorted_modules = module_topological_sort(modules)
189 return sorted_modules
191 def module_boot(req, db=None):
192 server_wide_modules = openerp.conf.server_wide_modules or ['web']
195 for i in server_wide_modules:
196 if i in openerpweb.addons_manifest:
198 monodb = db or db_monodb(req)
200 dbside = module_installed_bypass_session(monodb)
201 dbside = [i for i in dbside if i not in serverside]
202 addons = serverside + dbside
205 def concat_xml(file_list):
206 """Concatenate xml files
208 :param list(str) file_list: list of files to check
209 :returns: (concatenation_result, checksum)
212 checksum = hashlib.new('sha1')
214 return '', checksum.hexdigest()
217 for fname in file_list:
218 with open(fname, 'rb') as fp:
220 checksum.update(contents)
222 xml = ElementTree.parse(fp).getroot()
225 root = ElementTree.Element(xml.tag)
226 #elif root.tag != xml.tag:
227 # raise ValueError("Root tags missmatch: %r != %r" % (root.tag, xml.tag))
229 for child in xml.getchildren():
231 return ElementTree.tostring(root, 'utf-8'), checksum.hexdigest()
233 def concat_files(file_list, reader=None, intersperse=""):
234 """ Concatenates contents of all provided files
236 :param list(str) file_list: list of files to check
237 :param function reader: reading procedure for each file
238 :param str intersperse: string to intersperse between file contents
239 :returns: (concatenation_result, checksum)
242 checksum = hashlib.new('sha1')
244 return '', checksum.hexdigest()
248 with open(f, 'rb') as fp:
252 for fname in file_list:
253 contents = reader(fname)
254 checksum.update(contents)
255 files_content.append(contents)
257 files_concat = intersperse.join(files_content)
258 return files_concat, checksum.hexdigest()
262 def concat_js(file_list):
263 content, checksum = concat_files(file_list, intersperse=';')
264 if checksum in concat_js_cache:
265 content = concat_js_cache[checksum]
267 content = rjsmin(content)
268 concat_js_cache[checksum] = content
269 return content, checksum
272 """convert FS path into web path"""
273 return '/'.join(path.split(os.path.sep))
275 def manifest_glob(req, extension, addons=None, db=None):
277 addons = module_boot(req, db=db)
279 addons = addons.split(',')
282 manifest = openerpweb.addons_manifest.get(addon, None)
285 # ensure does not ends with /
286 addons_path = os.path.join(manifest['addons_path'], '')[:-1]
287 globlist = manifest.get(extension, [])
288 for pattern in globlist:
289 for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
290 r.append((path, fs2web(path[len(addons_path):])))
293 def manifest_list(req, extension, mods=None, db=None):
295 path = '/web/webclient/' + extension
297 path += '?mods=' + mods
301 files = manifest_glob(req, extension, addons=mods, db=db)
302 i_am_diabetic = req.httprequest.environ["QUERY_STRING"].count("no_sugar") >= 1 or \
303 req.httprequest.environ.get('HTTP_REFERER', '').count("no_sugar") >= 1
305 return [wp for _fp, wp in files]
307 return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in files]
309 def get_last_modified(files):
310 """ Returns the modification time of the most recently modified
313 :param list(str) files: names of files to check
314 :return: most recent modification time amongst the fileset
315 :rtype: datetime.datetime
319 return max(datetime.datetime.fromtimestamp(os.path.getmtime(f))
321 return datetime.datetime(1970, 1, 1)
323 def make_conditional(req, response, last_modified=None, etag=None):
324 """ Makes the provided response conditional based upon the request,
325 and mandates revalidation from clients
327 Uses Werkzeug's own :meth:`ETagResponseMixin.make_conditional`, after
328 setting ``last_modified`` and ``etag`` correctly on the response object
330 :param req: OpenERP request
331 :type req: web.common.http.WebRequest
332 :param response: Werkzeug response
333 :type response: werkzeug.wrappers.Response
334 :param datetime.datetime last_modified: last modification date of the response content
335 :param str etag: some sort of checksum of the content (deep etag)
336 :return: the response object provided
337 :rtype: werkzeug.wrappers.Response
339 response.cache_control.must_revalidate = True
340 response.cache_control.max_age = 0
342 response.last_modified = last_modified
344 response.set_etag(etag)
345 return response.make_conditional(req.httprequest)
347 def login_and_redirect(req, db, login, key, redirect_url='/'):
348 wsgienv = req.httprequest.environ
350 base_location=req.httprequest.url_root.rstrip('/'),
351 HTTP_HOST=wsgienv['HTTP_HOST'],
352 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
354 req.session.authenticate(db, login, key, env)
355 return set_cookie_and_redirect(req, redirect_url)
357 def set_cookie_and_redirect(req, redirect_url):
358 redirect = werkzeug.utils.redirect(redirect_url, 303)
359 redirect.autocorrect_location_header = False
360 cookie_val = urllib2.quote(simplejson.dumps(req.session_id))
361 redirect.set_cookie('instance0|session_id', cookie_val)
364 def eval_context_and_domain(session, context, domain=None):
365 e_context = session.eval_context(context)
366 # should we give the evaluated context as an evaluation context to the domain?
367 e_domain = session.eval_domain(domain or [])
369 return e_context, e_domain
371 def load_actions_from_ir_values(req, key, key2, models, meta):
372 context = req.session.eval_context(req.context)
373 Values = req.session.model('ir.values')
374 actions = Values.get(key, key2, models, meta, context)
376 return [(id, name, clean_action(req, action, context))
377 for id, name, action in actions]
379 def clean_action(req, action, context, do_not_eval=False):
380 action.setdefault('flags', {})
382 context = context or {}
383 eval_ctx = req.session.evaluation_context(context)
386 # values come from the server, we can just eval them
387 if action.get('context') and isinstance(action.get('context'), basestring):
388 action['context'] = eval( action['context'], eval_ctx ) or {}
390 if action.get('domain') and isinstance(action.get('domain'), basestring):
391 action['domain'] = eval( action['domain'], eval_ctx ) or []
393 if 'context' in action:
394 action['context'] = parse_context(action['context'], req.session)
395 if 'domain' in action:
396 action['domain'] = parse_domain(action['domain'], req.session)
398 action_type = action.setdefault('type', 'ir.actions.act_window_close')
399 if action_type == 'ir.actions.act_window':
400 return fix_view_modes(action)
403 # I think generate_views,fix_view_modes should go into js ActionManager
404 def generate_views(action):
406 While the server generates a sequence called "views" computing dependencies
407 between a bunch of stuff for views coming directly from the database
408 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
409 to return custom view dictionaries generated on the fly.
411 In that case, there is no ``views`` key available on the action.
413 Since the web client relies on ``action['views']``, generate it here from
414 ``view_mode`` and ``view_id``.
416 Currently handles two different cases:
418 * no view_id, multiple view_mode
419 * single view_id, single view_mode
421 :param dict action: action descriptor dictionary to generate a views key for
423 view_id = action.get('view_id') or False
424 if isinstance(view_id, (list, tuple)):
427 # providing at least one view mode is a requirement, not an option
428 view_modes = action['view_mode'].split(',')
430 if len(view_modes) > 1:
432 raise ValueError('Non-db action dictionaries should provide '
433 'either multiple view modes or a single view '
434 'mode and an optional view id.\n\n Got view '
435 'modes %r and view id %r for action %r' % (
436 view_modes, view_id, action))
437 action['views'] = [(False, mode) for mode in view_modes]
439 action['views'] = [(view_id, view_modes[0])]
441 def fix_view_modes(action):
442 """ For historical reasons, OpenERP has weird dealings in relation to
443 view_mode and the view_type attribute (on window actions):
445 * one of the view modes is ``tree``, which stands for both list views
447 * the choice is made by checking ``view_type``, which is either
448 ``form`` for a list view or ``tree`` for an actual tree view
450 This methods simply folds the view_type into view_mode by adding a
451 new view mode ``list`` which is the result of the ``tree`` view_mode
452 in conjunction with the ``form`` view_type.
454 TODO: this should go into the doc, some kind of "peculiarities" section
456 :param dict action: an action descriptor
457 :returns: nothing, the action is modified in place
459 if not action.get('views'):
460 generate_views(action)
462 if action.pop('view_type', 'form') != 'form':
465 if 'view_mode' in action:
466 action['view_mode'] = ','.join(
467 mode if mode != 'tree' else 'list'
468 for mode in action['view_mode'].split(','))
470 [id, mode if mode != 'tree' else 'list']
471 for id, mode in action['views']
476 def parse_domain(domain, session):
477 """ Parses an arbitrary string containing a domain, transforms it
478 to either a literal domain or a :class:`nonliterals.Domain`
480 :param domain: the domain to parse, if the domain 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(domain, basestring):
488 return ast.literal_eval(domain)
491 return nonliterals.Domain(session, domain)
493 def parse_context(context, session):
494 """ Parses an arbitrary string containing a context, transforms it
495 to either a literal context or a :class:`nonliterals.Context`
497 :param context: the context to parse, if the context is not a string it
498 is assumed to be a literal domain and is returned as-is
499 :param session: Current OpenERP session
500 :type session: openerpweb.OpenERPSession
502 if not isinstance(context, basestring):
505 return ast.literal_eval(context)
507 return nonliterals.Context(session, context)
509 def _local_web_translations(trans_file):
512 with open(trans_file) as t_file:
513 po = babel.messages.pofile.read_po(t_file)
517 if x.id and x.string and "openerp-web" in x.auto_comments:
518 messages.append({'id': x.id, 'string': x.string})
521 def xml2json_from_elementtree(el, preserve_whitespaces=False):
523 Simple and straightforward XML-to-JSON converter in Python
525 http://code.google.com/p/xml2json-direct/
529 ns, name = el.tag.rsplit("}", 1)
531 res["namespace"] = ns[1:]
535 for k, v in el.items():
538 if el.text and (preserve_whitespaces or el.text.strip() != ''):
541 kids.append(xml2json_from_elementtree(kid, preserve_whitespaces))
542 if kid.tail and (preserve_whitespaces or kid.tail.strip() != ''):
543 kids.append(kid.tail)
544 res["children"] = kids
547 def content_disposition(filename, req):
548 filename = filename.encode('utf8')
549 escaped = urllib2.quote(filename)
550 browser = req.httprequest.user_agent.browser
551 version = int((req.httprequest.user_agent.version or '0').split('.')[0])
552 if browser == 'msie' and version < 9:
553 return "attachment; filename=%s" % escaped
554 elif browser == 'safari':
555 return "attachment; filename=%s" % filename
557 return "attachment; filename*=UTF-8''%s" % escaped
560 #----------------------------------------------------------
561 # OpenERP Web web Controllers
562 #----------------------------------------------------------
564 html_template = """<!DOCTYPE html>
565 <html style="height: 100%%">
567 <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
568 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
569 <title>OpenERP</title>
570 <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
571 <link rel="stylesheet" href="/web/static/src/css/full.css" />
574 <script type="text/javascript">
576 var s = new openerp.init(%(modules)s);
583 <script src="http://ajax.googleapis.com/ajax/libs/chrome-frame/1/CFInstall.min.js"></script>
585 var test = function() {
590 if (window.localStorage && false) {
591 if (! localStorage.getItem("hasShownGFramePopup")) {
593 localStorage.setItem("hasShownGFramePopup", true);
604 class Home(openerpweb.Controller):
607 @openerpweb.httprequest
608 def index(self, req, s_action=None, db=None, **kw):
609 js = "\n ".join('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list(req, 'js', db=db))
610 css = "\n ".join('<link rel="stylesheet" href="%s">' % i for i in manifest_list(req, 'css', db=db))
612 r = html_template % {
615 'modules': simplejson.dumps(module_boot(req, db=db)),
616 'init': 'var wc = new s.web.WebClient();wc.appendTo($(document.body));'
620 @openerpweb.httprequest
621 def login(self, req, db, login, key):
622 return login_and_redirect(req, db, login, key)
624 class WebClient(openerpweb.Controller):
625 _cp_path = "/web/webclient"
627 @openerpweb.jsonrequest
628 def csslist(self, req, mods=None):
629 return manifest_list(req, 'css', mods=mods)
631 @openerpweb.jsonrequest
632 def jslist(self, req, mods=None):
633 return manifest_list(req, 'js', mods=mods)
635 @openerpweb.jsonrequest
636 def qweblist(self, req, mods=None):
637 return manifest_list(req, 'qweb', mods=mods)
639 @openerpweb.httprequest
640 def css(self, req, mods=None, db=None):
641 files = list(manifest_glob(req, 'css', addons=mods, db=db))
642 last_modified = get_last_modified(f[0] for f in files)
643 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
644 return werkzeug.wrappers.Response(status=304)
646 file_map = dict(files)
648 rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
649 rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
652 """read the a css file and absolutify all relative uris"""
653 with open(f, 'rb') as fp:
654 data = fp.read().decode('utf-8')
657 web_dir = os.path.dirname(path)
661 r"""@import \1%s/""" % (web_dir,),
667 r"""url(\1%s/""" % (web_dir,),
670 return data.encode('utf-8')
672 content, checksum = concat_files((f[0] for f in files), reader)
674 return make_conditional(
675 req, req.make_response(content, [('Content-Type', 'text/css')]),
676 last_modified, checksum)
678 @openerpweb.httprequest
679 def js(self, req, mods=None, db=None):
680 files = [f[0] for f in manifest_glob(req, 'js', addons=mods, db=db)]
681 last_modified = get_last_modified(files)
682 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
683 return werkzeug.wrappers.Response(status=304)
685 content, checksum = concat_js(files)
687 return make_conditional(
688 req, req.make_response(content, [('Content-Type', 'application/javascript')]),
689 last_modified, checksum)
691 @openerpweb.httprequest
692 def qweb(self, req, mods=None, db=None):
693 files = [f[0] for f in manifest_glob(req, 'qweb', addons=mods, db=db)]
694 last_modified = get_last_modified(files)
695 if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
696 return werkzeug.wrappers.Response(status=304)
698 content, checksum = concat_xml(files)
700 return make_conditional(
701 req, req.make_response(content, [('Content-Type', 'text/xml')]),
702 last_modified, checksum)
704 @openerpweb.jsonrequest
705 def bootstrap_translations(self, req, mods):
706 """ Load local translations from *.po files, as a temporary solution
707 until we have established a valid session. This is meant only
708 for translating the login page and db management chrome, using
709 the browser's language. """
710 # For performance reasons we only load a single translation, so for
711 # sub-languages (that should only be partially translated) we load the
712 # main language PO instead - that should be enough for the login screen.
713 lang = req.lang.split('_')[0]
715 translations_per_module = {}
716 for addon_name in mods:
717 if openerpweb.addons_manifest[addon_name].get('bootstrap'):
718 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
719 f_name = os.path.join(addons_path, addon_name, "i18n", lang + ".po")
720 if not os.path.exists(f_name):
722 translations_per_module[addon_name] = {'messages': _local_web_translations(f_name)}
724 return {"modules": translations_per_module,
725 "lang_parameters": None}
727 @openerpweb.jsonrequest
728 def translations(self, req, mods, lang):
729 res_lang = req.session.model('res.lang')
730 ids = res_lang.search([("code", "=", lang)])
733 lang_params = res_lang.read(ids[0], ["direction", "date_format", "time_format",
734 "grouping", "decimal_point", "thousands_sep"])
736 # Regional languages (ll_CC) must inherit/override their parent lang (ll), but this is
737 # done server-side when the language is loaded, so we only need to load the user's lang.
738 ir_translation = req.session.model('ir.translation')
739 translations_per_module = {}
740 messages = ir_translation.search_read([('module','in',mods),('lang','=',lang),
741 ('comments','like','openerp-web'),('value','!=',False),
743 ['module','src','value','lang'], order='module')
744 for mod, msg_group in itertools.groupby(messages, key=operator.itemgetter('module')):
745 translations_per_module.setdefault(mod,{'messages':[]})
746 translations_per_module[mod]['messages'].extend({'id': m['src'],
747 'string': m['value']} \
749 return {"modules": translations_per_module,
750 "lang_parameters": lang_params}
752 @openerpweb.jsonrequest
753 def version_info(self, req):
754 return req.session.proxy('common').version()['openerp']
756 class Proxy(openerpweb.Controller):
757 _cp_path = '/web/proxy'
759 @openerpweb.jsonrequest
760 def load(self, req, path):
761 """ Proxies an HTTP request through a JSON request.
763 It is strongly recommended to not request binary files through this,
764 as the result will be a binary data blob as well.
766 :param req: OpenERP request
767 :param path: actual request path
768 :return: file content
770 from werkzeug.test import Client
771 from werkzeug.wrappers import BaseResponse
773 return Client(req.httprequest.app, BaseResponse).get(path).data
775 class Database(openerpweb.Controller):
776 _cp_path = "/web/database"
778 @openerpweb.jsonrequest
779 def get_list(self, req):
782 @openerpweb.jsonrequest
783 def create(self, req, fields):
784 params = dict(map(operator.itemgetter('name', 'value'), fields))
785 return req.session.proxy("db").create_database(
786 params['super_admin_pwd'],
788 bool(params.get('demo_data')),
790 params['create_admin_pwd'])
792 @openerpweb.jsonrequest
793 def duplicate(self, req, fields):
794 params = dict(map(operator.itemgetter('name', 'value'), fields))
795 return req.session.proxy("db").duplicate_database(
796 params['super_admin_pwd'],
797 params['db_original_name'],
800 @openerpweb.jsonrequest
801 def duplicate(self, req, fields):
802 params = dict(map(operator.itemgetter('name', 'value'), fields))
804 params['super_admin_pwd'],
805 params['db_original_name'],
809 return req.session.proxy("db").duplicate_database(*duplicate_attrs)
811 @openerpweb.jsonrequest
812 def drop(self, req, fields):
813 password, db = operator.itemgetter(
814 'drop_pwd', 'drop_db')(
815 dict(map(operator.itemgetter('name', 'value'), fields)))
818 return req.session.proxy("db").drop(password, db)
819 except xmlrpclib.Fault, e:
820 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
821 return {'error': e.faultCode, 'title': 'Drop Database'}
822 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
824 @openerpweb.httprequest
825 def backup(self, req, backup_db, backup_pwd, token):
827 db_dump = base64.b64decode(
828 req.session.proxy("db").dump(backup_pwd, backup_db))
829 filename = "%(db)s_%(timestamp)s.dump" % {
831 'timestamp': datetime.datetime.utcnow().strftime(
832 "%Y-%m-%d_%H-%M-%SZ")
834 return req.make_response(db_dump,
835 [('Content-Type', 'application/octet-stream; charset=binary'),
836 ('Content-Disposition', content_disposition(filename, req))],
837 {'fileToken': int(token)}
839 except xmlrpclib.Fault, e:
840 return simplejson.dumps([[],[{'error': e.faultCode, 'title': 'backup Database'}]])
842 @openerpweb.httprequest
843 def restore(self, req, db_file, restore_pwd, new_db):
845 data = base64.b64encode(db_file.read())
846 req.session.proxy("db").restore(restore_pwd, new_db, data)
848 except xmlrpclib.Fault, e:
849 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
850 raise Exception("AccessDenied")
852 @openerpweb.jsonrequest
853 def change_password(self, req, fields):
854 old_password, new_password = operator.itemgetter(
855 'old_pwd', 'new_pwd')(
856 dict(map(operator.itemgetter('name', 'value'), fields)))
858 return req.session.proxy("db").change_admin_password(old_password, new_password)
859 except xmlrpclib.Fault, e:
860 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
861 return {'error': e.faultCode, 'title': 'Change Password'}
862 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
864 class Session(openerpweb.Controller):
865 _cp_path = "/web/session"
867 def session_info(self, req):
868 req.session.ensure_valid()
870 "session_id": req.session_id,
871 "uid": req.session._uid,
872 "context": req.session.get_context() if req.session._uid else {},
873 "db": req.session._db,
874 "login": req.session._login,
877 @openerpweb.jsonrequest
878 def get_session_info(self, req):
879 return self.session_info(req)
881 @openerpweb.jsonrequest
882 def authenticate(self, req, db, login, password, base_location=None):
883 wsgienv = req.httprequest.environ
885 base_location=base_location,
886 HTTP_HOST=wsgienv['HTTP_HOST'],
887 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
889 req.session.authenticate(db, login, password, env)
891 return self.session_info(req)
893 @openerpweb.jsonrequest
894 def change_password (self,req,fields):
895 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
896 dict(map(operator.itemgetter('name', 'value'), fields)))
897 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
898 return {'error':'You cannot leave any password empty.','title': 'Change Password'}
899 if new_password != confirm_password:
900 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
902 if req.session.model('res.users').change_password(
903 old_password, new_password):
904 return {'new_password':new_password}
906 return {'error': 'The old password you provided is incorrect, your password was not changed.', 'title': 'Change Password'}
907 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
909 @openerpweb.jsonrequest
910 def sc_list(self, req):
911 return req.session.model('ir.ui.view_sc').get_sc(
912 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
914 @openerpweb.jsonrequest
915 def get_lang_list(self, req):
917 return req.session.proxy("db").list_lang() or []
919 return {"error": e, "title": "Languages"}
921 @openerpweb.jsonrequest
922 def modules(self, req):
923 # return all installed modules. Web client is smart enough to not load a module twice
924 return module_installed(req)
926 @openerpweb.jsonrequest
927 def eval_domain_and_context(self, req, contexts, domains,
929 """ Evaluates sequences of domains and contexts, composing them into
930 a single context, domain or group_by sequence.
932 :param list contexts: list of contexts to merge together. Contexts are
933 evaluated in sequence, all previous contexts
934 are part of their own evaluation context
935 (starting at the session context).
936 :param list domains: list of domains to merge together. Domains are
937 evaluated in sequence and appended to one another
938 (implicit AND), their evaluation domain is the
939 result of merging all contexts.
940 :param list group_by_seq: list of domains (which may be in a different
941 order than the ``contexts`` parameter),
942 evaluated in sequence, their ``'group_by'``
943 key is extracted if they have one.
948 the global context created by merging all of
952 the concatenation of all domains
955 a list of fields to group by, potentially empty (in which case
956 no group by should be performed)
958 context, domain = eval_context_and_domain(req.session,
959 nonliterals.CompoundContext(*(contexts or [])),
960 nonliterals.CompoundDomain(*(domains or [])))
962 group_by_sequence = []
963 for candidate in (group_by_seq or []):
964 ctx = req.session.eval_context(candidate, context)
965 group_by = ctx.get('group_by')
968 elif isinstance(group_by, basestring):
969 group_by_sequence.append(group_by)
971 group_by_sequence.extend(group_by)
976 'group_by': group_by_sequence
979 @openerpweb.jsonrequest
980 def save_session_action(self, req, the_action):
982 This method store an action object in the session object and returns an integer
983 identifying that action. The method get_session_action() can be used to get
986 :param the_action: The action to save in the session.
987 :type the_action: anything
988 :return: A key identifying the saved action.
991 saved_actions = req.httpsession.get('saved_actions')
992 if not saved_actions:
993 saved_actions = {"next":0, "actions":{}}
994 req.httpsession['saved_actions'] = saved_actions
995 # we don't allow more than 10 stored actions
996 if len(saved_actions["actions"]) >= 10:
997 del saved_actions["actions"][min(saved_actions["actions"])]
998 key = saved_actions["next"]
999 saved_actions["actions"][key] = the_action
1000 saved_actions["next"] = key + 1
1003 @openerpweb.jsonrequest
1004 def get_session_action(self, req, key):
1006 Gets back a previously saved action. This method can return None if the action
1007 was saved since too much time (this case should be handled in a smart way).
1009 :param key: The key given by save_session_action()
1011 :return: The saved action or None.
1014 saved_actions = req.httpsession.get('saved_actions')
1015 if not saved_actions:
1017 return saved_actions["actions"].get(key)
1019 @openerpweb.jsonrequest
1020 def check(self, req):
1021 req.session.assert_valid()
1024 @openerpweb.jsonrequest
1025 def destroy(self, req):
1026 req.session._suicide = True
1028 class Menu(openerpweb.Controller):
1029 _cp_path = "/web/menu"
1031 @openerpweb.jsonrequest
1032 def load(self, req):
1033 return {'data': self.do_load(req)}
1035 @openerpweb.jsonrequest
1036 def get_user_roots(self, req):
1037 return self.do_get_user_roots(req)
1039 def do_get_user_roots(self, req):
1040 """ Return all root menu ids visible for the session user.
1042 :param req: A request object, with an OpenERP session attribute
1043 :type req: < session -> OpenERPSession >
1044 :return: the root menu ids
1048 context = s.eval_context(req.context)
1049 Menus = s.model('ir.ui.menu')
1050 # If a menu action is defined use its domain to get the root menu items
1051 user_menu_id = s.model('res.users').read([s._uid], ['menu_id'], context)[0]['menu_id']
1053 menu_domain = [('parent_id', '=', False)]
1055 domain_string = s.model('ir.actions.act_window').read([user_menu_id[0]], ['domain'], context)[0]['domain']
1057 menu_domain = ast.literal_eval(domain_string)
1059 return Menus.search(menu_domain, 0, False, False, context)
1061 def do_load(self, req):
1062 """ Loads all menu items (all applications and their sub-menus).
1064 :param req: A request object, with an OpenERP session attribute
1065 :type req: < session -> OpenERPSession >
1066 :return: the menu root
1067 :rtype: dict('children': menu_nodes)
1069 context = req.session.eval_context(req.context)
1070 Menus = req.session.model('ir.ui.menu')
1072 menu_roots = Menus.read(self.do_get_user_roots(req), ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1073 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children' : menu_roots}
1075 # menus are loaded fully unlike a regular tree view, cause there are a
1076 # limited number of items (752 when all 6.1 addons are installed)
1077 menu_ids = Menus.search([], 0, False, False, context)
1078 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled', 'needaction_counter'], context)
1079 # adds roots at the end of the sequence, so that they will overwrite
1080 # equivalent menu items from full menu read when put into id:item
1081 # mapping, resulting in children being correctly set on the roots.
1082 menu_items.extend(menu_roots)
1084 # make a tree using parent_id
1085 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
1086 for menu_item in menu_items:
1087 if menu_item['parent_id']:
1088 parent = menu_item['parent_id'][0]
1091 if parent in menu_items_map:
1092 menu_items_map[parent].setdefault(
1093 'children', []).append(menu_item)
1095 # sort by sequence a tree using parent_id
1096 for menu_item in menu_items:
1097 menu_item.setdefault('children', []).sort(
1098 key=operator.itemgetter('sequence'))
1102 @openerpweb.jsonrequest
1103 def action(self, req, menu_id):
1104 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
1105 [('ir.ui.menu', menu_id)], False)
1106 return {"action": actions}
1108 class DataSet(openerpweb.Controller):
1109 _cp_path = "/web/dataset"
1111 @openerpweb.jsonrequest
1112 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
1113 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
1114 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
1116 """ Performs a search() followed by a read() (if needed) using the
1117 provided search criteria
1119 :param req: a JSON-RPC request object
1120 :type req: openerpweb.JsonRequest
1121 :param str model: the name of the model to search on
1122 :param fields: a list of the fields to return in the result records
1124 :param int offset: from which index should the results start being returned
1125 :param int limit: the maximum number of records to return
1126 :param list domain: the search domain for the query
1127 :param list sort: sorting directives
1128 :returns: A structure (dict) with two keys: ids (all the ids matching
1129 the (domain, context) pair) and records (paginated records
1130 matching fields selection set)
1133 Model = req.session.model(model)
1135 context, domain = eval_context_and_domain(
1136 req.session, req.context, domain)
1138 ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
1139 if limit and len(ids) == limit:
1140 length = Model.search_count(domain, context)
1142 length = len(ids) + (offset or 0)
1143 if fields and fields == ['id']:
1144 # shortcut read if we only want the ids
1147 'records': [{'id': id} for id in ids]
1150 records = Model.read(ids, fields or False, context)
1151 records.sort(key=lambda obj: ids.index(obj['id']))
1157 @openerpweb.jsonrequest
1158 def load(self, req, model, id, fields):
1159 m = req.session.model(model)
1161 r = m.read([id], False, req.session.eval_context(req.context))
1164 return {'value': value}
1166 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
1167 has_domain = domain_id is not None and domain_id < len(args)
1168 has_context = context_id is not None and context_id < len(args)
1170 domain = args[domain_id] if has_domain else []
1171 context = args[context_id] if has_context else {}
1172 c, d = eval_context_and_domain(req.session, context, domain)
1176 args[context_id] = c
1178 return self._call_kw(req, model, method, args, {})
1180 def _call_kw(self, req, model, method, args, kwargs):
1181 for i in xrange(len(args)):
1182 if isinstance(args[i], nonliterals.BaseContext):
1183 args[i] = req.session.eval_context(args[i])
1184 elif isinstance(args[i], nonliterals.BaseDomain):
1185 args[i] = req.session.eval_domain(args[i])
1186 for k in kwargs.keys():
1187 if isinstance(kwargs[k], nonliterals.BaseContext):
1188 kwargs[k] = req.session.eval_context(kwargs[k])
1189 elif isinstance(kwargs[k], nonliterals.BaseDomain):
1190 kwargs[k] = req.session.eval_domain(kwargs[k])
1192 # Temporary implements future display_name special field for model#read()
1193 if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
1194 if 'display_name' in args[1]:
1195 names = req.session.model(model).name_get(args[0], **kwargs)
1196 args[1].remove('display_name')
1197 r = getattr(req.session.model(model), method)(*args, **kwargs)
1198 for i in range(len(r)):
1199 r[i]['display_name'] = names[i][1] or "%s#%d" % (model, names[i][0])
1202 return getattr(req.session.model(model), method)(*args, **kwargs)
1204 @openerpweb.jsonrequest
1205 def onchange(self, req, model, method, args, context_id=None):
1206 """ Support method for handling onchange calls: behaves much like call
1207 with the following differences:
1209 * Does not take a domain_id
1210 * Is aware of the return value's structure, and will parse the domains
1211 if needed in order to return either parsed literal domains (in JSON)
1212 or non-literal domain instances, allowing those domains to be used
1216 :type req: web.common.http.JsonRequest
1217 :param str model: object type on which to call the method
1218 :param str method: name of the onchange handler method
1219 :param list args: arguments to call the onchange handler with
1220 :param int context_id: index of the context object in the list of
1222 :return: result of the onchange call with all domains parsed
1224 result = self.call_common(req, model, method, args, context_id=context_id)
1225 if not result or 'domain' not in result:
1228 result['domain'] = dict(
1229 (k, parse_domain(v, req.session))
1230 for k, v in result['domain'].iteritems())
1234 @openerpweb.jsonrequest
1235 def call(self, req, model, method, args, domain_id=None, context_id=None):
1236 return self.call_common(req, model, method, args, domain_id, context_id)
1238 @openerpweb.jsonrequest
1239 def call_kw(self, req, model, method, args, kwargs):
1240 return self._call_kw(req, model, method, args, kwargs)
1242 @openerpweb.jsonrequest
1243 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1244 context = req.session.eval_context(req.context)
1245 action = self.call_common(req, model, method, args, domain_id, context_id)
1246 if isinstance(action, dict) and action.get('type') != '':
1247 return clean_action(req, action, context)
1250 @openerpweb.jsonrequest
1251 def exec_workflow(self, req, model, id, signal):
1252 return req.session.exec_workflow(model, id, signal)
1254 @openerpweb.jsonrequest
1255 def resequence(self, req, model, ids, field='sequence', offset=0):
1256 """ Re-sequences a number of records in the model, by their ids
1258 The re-sequencing starts at the first model of ``ids``, the sequence
1259 number is incremented by one after each record and starts at ``offset``
1261 :param ids: identifiers of the records to resequence, in the new sequence order
1263 :param str field: field used for sequence specification, defaults to
1265 :param int offset: sequence number for first record in ``ids``, allows
1266 starting the resequencing from an arbitrary number,
1269 m = req.session.model(model)
1270 if not m.fields_get([field]):
1272 # python 2.6 has no start parameter
1273 for i, id in enumerate(ids):
1274 m.write(id, { field: i + offset })
1277 class View(openerpweb.Controller):
1278 _cp_path = "/web/view"
1280 def fields_view_get(self, req, model, view_id, view_type,
1281 transform=True, toolbar=False, submenu=False):
1282 Model = req.session.model(model)
1283 context = req.session.eval_context(req.context)
1284 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
1285 # todo fme?: check that we should pass the evaluated context here
1286 self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
1287 if toolbar and transform:
1288 self.process_toolbar(req, fvg['toolbar'])
1291 def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
1292 # depending on how it feels, xmlrpclib.ServerProxy can translate
1293 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
1294 # enjoy unicode strings which can not be trivially converted to
1295 # strings, and it blows up during parsing.
1297 # So ensure we fix this retardation by converting view xml back to
1299 if isinstance(fvg['arch'], unicode):
1300 arch = fvg['arch'].encode('utf-8')
1303 fvg['arch_string'] = arch
1306 evaluation_context = session.evaluation_context(context or {})
1307 xml = self.transform_view(arch, session, evaluation_context)
1309 xml = ElementTree.fromstring(arch)
1310 fvg['arch'] = xml2json_from_elementtree(xml, preserve_whitespaces)
1312 if 'id' in fvg['fields']:
1313 # Special case for id's
1314 id_field = fvg['fields']['id']
1315 id_field['original_type'] = id_field['type']
1316 id_field['type'] = 'id'
1318 for field in fvg['fields'].itervalues():
1319 if field.get('views'):
1320 for view in field["views"].itervalues():
1321 self.process_view(session, view, None, transform)
1322 if field.get('domain'):
1323 field["domain"] = parse_domain(field["domain"], session)
1324 if field.get('context'):
1325 field["context"] = parse_context(field["context"], session)
1327 def process_toolbar(self, req, toolbar):
1329 The toolbar is a mapping of section_key: [action_descriptor]
1331 We need to clean all those actions in order to ensure correct
1334 for actions in toolbar.itervalues():
1335 for action in actions:
1336 if 'context' in action:
1337 action['context'] = parse_context(
1338 action['context'], req.session)
1339 if 'domain' in action:
1340 action['domain'] = parse_domain(
1341 action['domain'], req.session)
1343 @openerpweb.jsonrequest
1344 def add_custom(self, req, view_id, arch):
1345 CustomView = req.session.model('ir.ui.view.custom')
1347 'user_id': req.session._uid,
1350 }, req.session.eval_context(req.context))
1351 return {'result': True}
1353 @openerpweb.jsonrequest
1354 def undo_custom(self, req, view_id, reset=False):
1355 CustomView = req.session.model('ir.ui.view.custom')
1356 context = req.session.eval_context(req.context)
1357 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1358 0, False, False, context)
1361 CustomView.unlink(vcustom, context)
1363 CustomView.unlink([vcustom[0]], context)
1364 return {'result': True}
1365 return {'result': False}
1367 def transform_view(self, view_string, session, context=None):
1368 # transform nodes on the fly via iterparse, instead of
1369 # doing it statically on the parsing result
1370 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
1372 for event, elem in parser:
1373 if event == "start":
1376 self.parse_domains_and_contexts(elem, session)
1379 def parse_domains_and_contexts(self, elem, session):
1380 """ Converts domains and contexts from the view into Python objects,
1381 either literals if they can be parsed by literal_eval or a special
1382 placeholder object if the domain or context refers to free variables.
1384 :param elem: the current node being parsed
1385 :type param: xml.etree.ElementTree.Element
1386 :param session: OpenERP session object, used to store and retrieve
1388 :type session: openerpweb.openerpweb.OpenERPSession
1390 for el in ['domain', 'filter_domain']:
1391 domain = elem.get(el, '').strip()
1393 elem.set(el, parse_domain(domain, session))
1394 elem.set(el + '_string', domain)
1395 for el in ['context', 'default_get']:
1396 context_string = elem.get(el, '').strip()
1398 elem.set(el, parse_context(context_string, session))
1399 elem.set(el + '_string', context_string)
1401 @openerpweb.jsonrequest
1402 def load(self, req, model, view_id, view_type, toolbar=False):
1403 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1405 class TreeView(View):
1406 _cp_path = "/web/treeview"
1408 @openerpweb.jsonrequest
1409 def action(self, req, model, id):
1410 return load_actions_from_ir_values(
1411 req,'action', 'tree_but_open',[(model, id)],
1414 class SearchView(View):
1415 _cp_path = "/web/searchview"
1417 @openerpweb.jsonrequest
1418 def load(self, req, model, view_id):
1419 fields_view = self.fields_view_get(req, model, view_id, 'search')
1420 return {'fields_view': fields_view}
1422 @openerpweb.jsonrequest
1423 def fields_get(self, req, model):
1424 Model = req.session.model(model)
1425 fields = Model.fields_get(False, req.session.eval_context(req.context))
1426 for field in fields.values():
1427 # shouldn't convert the views too?
1428 if field.get('domain'):
1429 field["domain"] = parse_domain(field["domain"], req.session)
1430 if field.get('context'):
1431 field["context"] = parse_context(field["context"], req.session)
1432 return {'fields': fields}
1434 @openerpweb.jsonrequest
1435 def get_filters(self, req, model):
1436 logger = logging.getLogger(__name__ + '.SearchView.get_filters')
1437 Model = req.session.model("ir.filters")
1438 filters = Model.get_filters(model)
1439 for filter in filters:
1441 parsed_context = parse_context(filter["context"], req.session)
1442 filter["context"] = (parsed_context
1443 if not isinstance(parsed_context, nonliterals.BaseContext)
1444 else req.session.eval_context(parsed_context))
1446 parsed_domain = parse_domain(filter["domain"], req.session)
1447 filter["domain"] = (parsed_domain
1448 if not isinstance(parsed_domain, nonliterals.BaseDomain)
1449 else req.session.eval_domain(parsed_domain))
1451 logger.exception("Failed to parse custom filter %s in %s",
1452 filter['name'], model)
1453 filter['disabled'] = True
1454 del filter['context']
1455 del filter['domain']
1458 class Binary(openerpweb.Controller):
1459 _cp_path = "/web/binary"
1461 @openerpweb.httprequest
1462 def image(self, req, model, id, field, **kw):
1463 last_update = '__last_update'
1464 Model = req.session.model(model)
1465 context = req.session.eval_context(req.context)
1466 headers = [('Content-Type', 'image/png')]
1467 etag = req.httprequest.headers.get('If-None-Match')
1468 hashed_session = hashlib.md5(req.session_id).hexdigest()
1469 id = None if not id else simplejson.loads(id)
1470 if type(id) is list:
1473 if not id and hashed_session == etag:
1474 return werkzeug.wrappers.Response(status=304)
1476 date = Model.read([id], [last_update], context)[0].get(last_update)
1477 if hashlib.md5(date).hexdigest() == etag:
1478 return werkzeug.wrappers.Response(status=304)
1480 retag = hashed_session
1483 res = Model.default_get([field], context).get(field)
1486 res = Model.read([id], [last_update, field], context)[0]
1487 retag = hashlib.md5(res.get(last_update)).hexdigest()
1488 image_base64 = res.get(field)
1490 if kw.get('resize'):
1491 resize = kw.get('resize').split(',');
1492 if len(resize) == 2 and int(resize[0]) and int(resize[1]):
1493 width = int(resize[0])
1494 height = int(resize[1])
1495 # resize maximum 500*500
1496 if width > 500: width = 500
1497 if height > 500: height = 500
1498 image_base64 = openerp.tools.image_resize_image(base64_source=image_base64, size=(width, height), encoding='base64', filetype='PNG')
1500 image_data = base64.b64decode(image_base64)
1502 except (TypeError, xmlrpclib.Fault):
1503 image_data = self.placeholder(req)
1504 headers.append(('ETag', retag))
1505 headers.append(('Content-Length', len(image_data)))
1507 ncache = int(kw.get('cache'))
1508 headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1511 return req.make_response(image_data, headers)
1512 def placeholder(self, req):
1513 addons_path = openerpweb.addons_manifest['web']['addons_path']
1514 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1516 @openerpweb.httprequest
1517 def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1518 """ Download link for files stored as binary fields.
1520 If the ``id`` parameter is omitted, fetches the default value for the
1521 binary field (via ``default_get``), otherwise fetches the field for
1522 that precise record.
1524 :param req: OpenERP request
1525 :type req: :class:`web.common.http.HttpRequest`
1526 :param str model: name of the model to fetch the binary from
1527 :param str field: binary field
1528 :param str id: id of the record from which to fetch the binary
1529 :param str filename_field: field holding the file's name, if any
1530 :returns: :class:`werkzeug.wrappers.Response`
1532 Model = req.session.model(model)
1533 context = req.session.eval_context(req.context)
1536 fields.append(filename_field)
1538 res = Model.read([int(id)], fields, context)[0]
1540 res = Model.default_get(fields, context)
1541 filecontent = base64.b64decode(res.get(field, ''))
1543 return req.not_found()
1545 filename = '%s_%s' % (model.replace('.', '_'), id)
1547 filename = res.get(filename_field, '') or filename
1548 return req.make_response(filecontent,
1549 [('Content-Type', 'application/octet-stream'),
1550 ('Content-Disposition', content_disposition(filename, req))])
1552 @openerpweb.httprequest
1553 def saveas_ajax(self, req, data, token):
1554 jdata = simplejson.loads(data)
1555 model = jdata['model']
1556 field = jdata['field']
1557 id = jdata.get('id', None)
1558 filename_field = jdata.get('filename_field', None)
1559 context = jdata.get('context', dict())
1561 context = req.session.eval_context(context)
1562 Model = req.session.model(model)
1565 fields.append(filename_field)
1567 res = Model.read([int(id)], fields, context)[0]
1569 res = Model.default_get(fields, context)
1570 filecontent = base64.b64decode(res.get(field, ''))
1572 raise ValueError("No content found for field '%s' on '%s:%s'" %
1575 filename = '%s_%s' % (model.replace('.', '_'), id)
1577 filename = res.get(filename_field, '') or filename
1578 return req.make_response(filecontent,
1579 headers=[('Content-Type', 'application/octet-stream'),
1580 ('Content-Disposition', content_disposition(filename, req))],
1581 cookies={'fileToken': int(token)})
1583 @openerpweb.httprequest
1584 def upload(self, req, callback, ufile):
1585 # TODO: might be useful to have a configuration flag for max-length file uploads
1587 out = """<script language="javascript" type="text/javascript">
1588 var win = window.top.window;
1589 win.jQuery(win).trigger(%s, %s);
1592 args = [len(data), ufile.filename,
1593 ufile.content_type, base64.b64encode(data)]
1594 except Exception, e:
1595 args = [False, e.message]
1596 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1598 @openerpweb.httprequest
1599 def upload_attachment(self, req, callback, model, id, ufile):
1600 context = req.session.eval_context(req.context)
1601 Model = req.session.model('ir.attachment')
1603 out = """<script language="javascript" type="text/javascript">
1604 var win = window.top.window;
1605 win.jQuery(win).trigger(%s, %s);
1607 attachment_id = Model.create({
1608 'name': ufile.filename,
1609 'datas': base64.encodestring(ufile.read()),
1610 'datas_fname': ufile.filename,
1615 'filename': ufile.filename,
1619 args = {'erorr':e.faultCode.split('--')[1],'title':e.faultCode.split('--')[0]}
1620 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1622 class Action(openerpweb.Controller):
1623 _cp_path = "/web/action"
1625 @openerpweb.jsonrequest
1626 def load(self, req, action_id, do_not_eval=False, eval_context=None):
1627 Actions = req.session.model('ir.actions.actions')
1629 context = req.session.eval_context(req.context)
1630 eval_context = req.session.eval_context(nonliterals.CompoundContext(context, eval_context or {}))
1633 action_id = int(action_id)
1636 module, xmlid = action_id.split('.', 1)
1637 model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
1638 assert model.startswith('ir.actions.')
1640 action_id = 0 # force failed read
1642 base_action = Actions.read([action_id], ['type'], context)
1645 action_type = base_action[0]['type']
1646 if action_type == 'ir.actions.report.xml':
1647 ctx.update({'bin_size': True})
1649 action = req.session.model(action_type).read([action_id], False, ctx)
1651 value = clean_action(req, action[0], eval_context, do_not_eval)
1654 @openerpweb.jsonrequest
1655 def run(self, req, action_id):
1656 context = req.session.eval_context(req.context)
1657 return_action = req.session.model('ir.actions.server').run(
1658 [action_id], req.session.eval_context(req.context))
1660 return clean_action(req, return_action, context)
1665 _cp_path = "/web/export"
1667 @openerpweb.jsonrequest
1668 def formats(self, req):
1669 """ Returns all valid export formats
1671 :returns: for each export format, a pair of identifier and printable name
1672 :rtype: [(str, str)]
1676 for path, controller in openerpweb.controllers_path.iteritems()
1677 if path.startswith(self._cp_path)
1678 if hasattr(controller, 'fmt')
1679 ], key=operator.itemgetter("label"))
1681 def fields_get(self, req, model):
1682 Model = req.session.model(model)
1683 fields = Model.fields_get(False, req.session.eval_context(req.context))
1686 @openerpweb.jsonrequest
1687 def get_fields(self, req, model, prefix='', parent_name= '',
1688 import_compat=True, parent_field_type=None,
1691 if import_compat and parent_field_type == "many2one":
1694 fields = self.fields_get(req, model)
1697 fields.pop('id', None)
1699 fields['.id'] = fields.pop('id', {'string': 'ID'})
1701 fields_sequence = sorted(fields.iteritems(),
1702 key=lambda field: field[1].get('string', ''))
1705 for field_name, field in fields_sequence:
1707 if exclude and field_name in exclude:
1709 if field.get('readonly'):
1710 # If none of the field's states unsets readonly, skip the field
1711 if all(dict(attrs).get('readonly', True)
1712 for attrs in field.get('states', {}).values()):
1715 id = prefix + (prefix and '/'or '') + field_name
1716 name = parent_name + (parent_name and '/' or '') + field['string']
1717 record = {'id': id, 'string': name,
1718 'value': id, 'children': False,
1719 'field_type': field.get('type'),
1720 'required': field.get('required'),
1721 'relation_field': field.get('relation_field')}
1722 records.append(record)
1724 if len(name.split('/')) < 3 and 'relation' in field:
1725 ref = field.pop('relation')
1726 record['value'] += '/id'
1727 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1729 if not import_compat or field['type'] == 'one2many':
1730 # m2m field in import_compat is childless
1731 record['children'] = True
1735 @openerpweb.jsonrequest
1736 def namelist(self,req, model, export_id):
1737 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1738 export = req.session.model("ir.exports").read([export_id])[0]
1739 export_fields_list = req.session.model("ir.exports.line").read(
1740 export['export_fields'])
1742 fields_data = self.fields_info(
1743 req, model, map(operator.itemgetter('name'), export_fields_list))
1746 {'name': field['name'], 'label': fields_data[field['name']]}
1747 for field in export_fields_list
1750 def fields_info(self, req, model, export_fields):
1752 fields = self.fields_get(req, model)
1753 if ".id" in export_fields:
1754 fields['.id'] = fields.pop('id', {'string': 'ID'})
1756 # To make fields retrieval more efficient, fetch all sub-fields of a
1757 # given field at the same time. Because the order in the export list is
1758 # arbitrary, this requires ordering all sub-fields of a given field
1759 # together so they can be fetched at the same time
1761 # Works the following way:
1762 # * sort the list of fields to export, the default sorting order will
1763 # put the field itself (if present, for xmlid) and all of its
1764 # sub-fields right after it
1765 # * then, group on: the first field of the path (which is the same for
1766 # a field and for its subfields and the length of splitting on the
1767 # first '/', which basically means grouping the field on one side and
1768 # all of the subfields on the other. This way, we have the field (for
1769 # the xmlid) with length 1, and all of the subfields with the same
1770 # base but a length "flag" of 2
1771 # * if we have a normal field (length 1), just add it to the info
1772 # mapping (with its string) as-is
1773 # * otherwise, recursively call fields_info via graft_subfields.
1774 # all graft_subfields does is take the result of fields_info (on the
1775 # field's model) and prepend the current base (current field), which
1776 # rebuilds the whole sub-tree for the field
1778 # result: because we're not fetching the fields_get for half the
1779 # database models, fetching a namelist with a dozen fields (including
1780 # relational data) falls from ~6s to ~300ms (on the leads model).
1781 # export lists with no sub-fields (e.g. import_compatible lists with
1782 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1783 # there's a single fields_get to execute)
1784 for (base, length), subfields in itertools.groupby(
1785 sorted(export_fields),
1786 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1787 subfields = list(subfields)
1789 # subfields is a seq of $base/*rest, and not loaded yet
1790 info.update(self.graft_subfields(
1791 req, fields[base]['relation'], base, fields[base]['string'],
1795 info[base] = fields[base]['string']
1799 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1800 export_fields = [field.split('/', 1)[1] for field in fields]
1802 (prefix + '/' + k, prefix_string + '/' + v)
1803 for k, v in self.fields_info(req, model, export_fields).iteritems())
1805 #noinspection PyPropertyDefinition
1807 def content_type(self):
1808 """ Provides the format's content type """
1809 raise NotImplementedError()
1811 def filename(self, base):
1812 """ Creates a valid filename for the format (with extension) from the
1813 provided base name (exension-less)
1815 raise NotImplementedError()
1817 def from_data(self, fields, rows):
1818 """ Conversion method from OpenERP's export data to whatever the
1819 current export class outputs
1821 :params list fields: a list of fields to export
1822 :params list rows: a list of records to export
1826 raise NotImplementedError()
1828 @openerpweb.httprequest
1829 def index(self, req, data, token):
1830 model, fields, ids, domain, import_compat = \
1831 operator.itemgetter('model', 'fields', 'ids', 'domain',
1833 simplejson.loads(data))
1835 context = req.session.eval_context(req.context)
1836 Model = req.session.model(model)
1837 ids = ids or Model.search(domain, 0, False, False, context)
1839 field_names = map(operator.itemgetter('name'), fields)
1840 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1843 columns_headers = field_names
1845 columns_headers = [val['label'].strip() for val in fields]
1848 return req.make_response(self.from_data(columns_headers, import_data),
1849 headers=[('Content-Disposition',
1850 content_disposition(self.filename(model), req)),
1851 ('Content-Type', self.content_type)],
1852 cookies={'fileToken': int(token)})
1854 class CSVExport(Export):
1855 _cp_path = '/web/export/csv'
1856 fmt = {'tag': 'csv', 'label': 'CSV'}
1859 def content_type(self):
1860 return 'text/csv;charset=utf8'
1862 def filename(self, base):
1863 return base + '.csv'
1865 def from_data(self, fields, rows):
1867 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1869 writer.writerow([name.encode('utf-8') for name in fields])
1874 if isinstance(d, basestring):
1875 d = d.replace('\n',' ').replace('\t',' ')
1877 d = d.encode('utf-8')
1878 except UnicodeError:
1880 if d is False: d = None
1882 writer.writerow(row)
1889 class ExcelExport(Export):
1890 _cp_path = '/web/export/xls'
1894 'error': None if xlwt else "XLWT required"
1898 def content_type(self):
1899 return 'application/vnd.ms-excel'
1901 def filename(self, base):
1902 return base + '.xls'
1904 def from_data(self, fields, rows):
1905 workbook = xlwt.Workbook()
1906 worksheet = workbook.add_sheet('Sheet 1')
1908 for i, fieldname in enumerate(fields):
1909 worksheet.write(0, i, fieldname)
1910 worksheet.col(i).width = 8000 # around 220 pixels
1912 style = xlwt.easyxf('align: wrap yes')
1914 for row_index, row in enumerate(rows):
1915 for cell_index, cell_value in enumerate(row):
1916 if isinstance(cell_value, basestring):
1917 cell_value = re.sub("\r", " ", cell_value)
1918 if cell_value is False: cell_value = None
1919 worksheet.write(row_index + 1, cell_index, cell_value, style)
1928 class Reports(View):
1929 _cp_path = "/web/report"
1930 POLLING_DELAY = 0.25
1932 'doc': 'application/vnd.ms-word',
1933 'html': 'text/html',
1934 'odt': 'application/vnd.oasis.opendocument.text',
1935 'pdf': 'application/pdf',
1936 'sxw': 'application/vnd.sun.xml.writer',
1937 'xls': 'application/vnd.ms-excel',
1940 @openerpweb.httprequest
1941 def index(self, req, action, token):
1942 action = simplejson.loads(action)
1944 report_srv = req.session.proxy("report")
1945 context = req.session.eval_context(
1946 nonliterals.CompoundContext(
1947 req.context or {}, action[ "context"]))
1950 report_ids = context["active_ids"]
1951 if 'report_type' in action:
1952 report_data['report_type'] = action['report_type']
1953 if 'datas' in action:
1954 if 'ids' in action['datas']:
1955 report_ids = action['datas'].pop('ids')
1956 report_data.update(action['datas'])
1958 report_id = report_srv.report(
1959 req.session._db, req.session._uid, req.session._password,
1960 action["report_name"], report_ids,
1961 report_data, context)
1963 report_struct = None
1965 report_struct = report_srv.report_get(
1966 req.session._db, req.session._uid, req.session._password, report_id)
1967 if report_struct["state"]:
1970 time.sleep(self.POLLING_DELAY)
1972 report = base64.b64decode(report_struct['result'])
1973 if report_struct.get('code') == 'zlib':
1974 report = zlib.decompress(report)
1975 report_mimetype = self.TYPES_MAPPING.get(
1976 report_struct['format'], 'octet-stream')
1977 file_name = action.get('name', 'report')
1978 if 'name' not in action:
1979 reports = req.session.model('ir.actions.report.xml')
1980 res_id = reports.search([('report_name', '=', action['report_name']),],
1981 0, False, False, context)
1983 file_name = reports.read(res_id[0], ['name'], context)['name']
1985 file_name = action['report_name']
1986 file_name = '%s.%s' % (file_name, report_struct['format'])
1988 return req.make_response(report,
1990 ('Content-Disposition', content_disposition(file_name, req)),
1991 ('Content-Type', report_mimetype),
1992 ('Content-Length', len(report))],
1993 cookies={'fileToken': int(token)})
1995 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: