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):
755 "version": openerp.release.version
758 class Proxy(openerpweb.Controller):
759 _cp_path = '/web/proxy'
761 @openerpweb.jsonrequest
762 def load(self, req, path):
763 """ Proxies an HTTP request through a JSON request.
765 It is strongly recommended to not request binary files through this,
766 as the result will be a binary data blob as well.
768 :param req: OpenERP request
769 :param path: actual request path
770 :return: file content
772 from werkzeug.test import Client
773 from werkzeug.wrappers import BaseResponse
775 return Client(req.httprequest.app, BaseResponse).get(path).data
777 class Database(openerpweb.Controller):
778 _cp_path = "/web/database"
780 @openerpweb.jsonrequest
781 def get_list(self, req):
784 @openerpweb.jsonrequest
785 def create(self, req, fields):
786 params = dict(map(operator.itemgetter('name', 'value'), fields))
787 return req.session.proxy("db").create_database(
788 params['super_admin_pwd'],
790 bool(params.get('demo_data')),
792 params['create_admin_pwd'])
794 @openerpweb.jsonrequest
795 def duplicate(self, req, fields):
796 params = dict(map(operator.itemgetter('name', 'value'), fields))
797 return req.session.proxy("db").duplicate_database(
798 params['super_admin_pwd'],
799 params['db_original_name'],
802 @openerpweb.jsonrequest
803 def duplicate(self, req, fields):
804 params = dict(map(operator.itemgetter('name', 'value'), fields))
806 params['super_admin_pwd'],
807 params['db_original_name'],
811 return req.session.proxy("db").duplicate_database(*duplicate_attrs)
813 @openerpweb.jsonrequest
814 def drop(self, req, fields):
815 password, db = operator.itemgetter(
816 'drop_pwd', 'drop_db')(
817 dict(map(operator.itemgetter('name', 'value'), fields)))
820 return req.session.proxy("db").drop(password, db)
821 except xmlrpclib.Fault, e:
822 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
823 return {'error': e.faultCode, 'title': 'Drop Database'}
824 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
826 @openerpweb.httprequest
827 def backup(self, req, backup_db, backup_pwd, token):
829 db_dump = base64.b64decode(
830 req.session.proxy("db").dump(backup_pwd, backup_db))
831 filename = "%(db)s_%(timestamp)s.dump" % {
833 'timestamp': datetime.datetime.utcnow().strftime(
834 "%Y-%m-%d_%H-%M-%SZ")
836 return req.make_response(db_dump,
837 [('Content-Type', 'application/octet-stream; charset=binary'),
838 ('Content-Disposition', content_disposition(filename, req))],
839 {'fileToken': int(token)}
841 except xmlrpclib.Fault, e:
842 return simplejson.dumps([[],[{'error': e.faultCode, 'title': 'backup Database'}]])
844 @openerpweb.httprequest
845 def restore(self, req, db_file, restore_pwd, new_db):
847 data = base64.b64encode(db_file.read())
848 req.session.proxy("db").restore(restore_pwd, new_db, data)
850 except xmlrpclib.Fault, e:
851 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
852 raise Exception("AccessDenied")
854 @openerpweb.jsonrequest
855 def change_password(self, req, fields):
856 old_password, new_password = operator.itemgetter(
857 'old_pwd', 'new_pwd')(
858 dict(map(operator.itemgetter('name', 'value'), fields)))
860 return req.session.proxy("db").change_admin_password(old_password, new_password)
861 except xmlrpclib.Fault, e:
862 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
863 return {'error': e.faultCode, 'title': 'Change Password'}
864 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
866 class Session(openerpweb.Controller):
867 _cp_path = "/web/session"
869 def session_info(self, req):
870 req.session.ensure_valid()
872 "session_id": req.session_id,
873 "uid": req.session._uid,
874 "context": req.session.get_context() if req.session._uid else {},
875 "db": req.session._db,
876 "login": req.session._login,
879 @openerpweb.jsonrequest
880 def get_session_info(self, req):
881 return self.session_info(req)
883 @openerpweb.jsonrequest
884 def authenticate(self, req, db, login, password, base_location=None):
885 wsgienv = req.httprequest.environ
887 base_location=base_location,
888 HTTP_HOST=wsgienv['HTTP_HOST'],
889 REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
891 req.session.authenticate(db, login, password, env)
893 return self.session_info(req)
895 @openerpweb.jsonrequest
896 def change_password (self,req,fields):
897 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
898 dict(map(operator.itemgetter('name', 'value'), fields)))
899 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
900 return {'error':'You cannot leave any password empty.','title': 'Change Password'}
901 if new_password != confirm_password:
902 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
904 if req.session.model('res.users').change_password(
905 old_password, new_password):
906 return {'new_password':new_password}
908 return {'error': 'The old password you provided is incorrect, your password was not changed.', 'title': 'Change Password'}
909 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
911 @openerpweb.jsonrequest
912 def sc_list(self, req):
913 return req.session.model('ir.ui.view_sc').get_sc(
914 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
916 @openerpweb.jsonrequest
917 def get_lang_list(self, req):
919 return req.session.proxy("db").list_lang() or []
921 return {"error": e, "title": "Languages"}
923 @openerpweb.jsonrequest
924 def modules(self, req):
925 # return all installed modules. Web client is smart enough to not load a module twice
926 return module_installed(req)
928 @openerpweb.jsonrequest
929 def eval_domain_and_context(self, req, contexts, domains,
931 """ Evaluates sequences of domains and contexts, composing them into
932 a single context, domain or group_by sequence.
934 :param list contexts: list of contexts to merge together. Contexts are
935 evaluated in sequence, all previous contexts
936 are part of their own evaluation context
937 (starting at the session context).
938 :param list domains: list of domains to merge together. Domains are
939 evaluated in sequence and appended to one another
940 (implicit AND), their evaluation domain is the
941 result of merging all contexts.
942 :param list group_by_seq: list of domains (which may be in a different
943 order than the ``contexts`` parameter),
944 evaluated in sequence, their ``'group_by'``
945 key is extracted if they have one.
950 the global context created by merging all of
954 the concatenation of all domains
957 a list of fields to group by, potentially empty (in which case
958 no group by should be performed)
960 context, domain = eval_context_and_domain(req.session,
961 nonliterals.CompoundContext(*(contexts or [])),
962 nonliterals.CompoundDomain(*(domains or [])))
964 group_by_sequence = []
965 for candidate in (group_by_seq or []):
966 ctx = req.session.eval_context(candidate, context)
967 group_by = ctx.get('group_by')
970 elif isinstance(group_by, basestring):
971 group_by_sequence.append(group_by)
973 group_by_sequence.extend(group_by)
978 'group_by': group_by_sequence
981 @openerpweb.jsonrequest
982 def save_session_action(self, req, the_action):
984 This method store an action object in the session object and returns an integer
985 identifying that action. The method get_session_action() can be used to get
988 :param the_action: The action to save in the session.
989 :type the_action: anything
990 :return: A key identifying the saved action.
993 saved_actions = req.httpsession.get('saved_actions')
994 if not saved_actions:
995 saved_actions = {"next":0, "actions":{}}
996 req.httpsession['saved_actions'] = saved_actions
997 # we don't allow more than 10 stored actions
998 if len(saved_actions["actions"]) >= 10:
999 del saved_actions["actions"][min(saved_actions["actions"])]
1000 key = saved_actions["next"]
1001 saved_actions["actions"][key] = the_action
1002 saved_actions["next"] = key + 1
1005 @openerpweb.jsonrequest
1006 def get_session_action(self, req, key):
1008 Gets back a previously saved action. This method can return None if the action
1009 was saved since too much time (this case should be handled in a smart way).
1011 :param key: The key given by save_session_action()
1013 :return: The saved action or None.
1016 saved_actions = req.httpsession.get('saved_actions')
1017 if not saved_actions:
1019 return saved_actions["actions"].get(key)
1021 @openerpweb.jsonrequest
1022 def check(self, req):
1023 req.session.assert_valid()
1026 @openerpweb.jsonrequest
1027 def destroy(self, req):
1028 req.session._suicide = True
1030 class Menu(openerpweb.Controller):
1031 _cp_path = "/web/menu"
1033 @openerpweb.jsonrequest
1034 def load(self, req):
1035 return {'data': self.do_load(req)}
1037 @openerpweb.jsonrequest
1038 def load_needaction(self, req, menu_ids):
1039 return {'data': self.do_load_needaction(req, menu_ids)}
1041 @openerpweb.jsonrequest
1042 def get_user_roots(self, req):
1043 return self.do_get_user_roots(req)
1045 def do_get_user_roots(self, req):
1046 """ Return all root menu ids visible for the session user.
1048 :param req: A request object, with an OpenERP session attribute
1049 :type req: < session -> OpenERPSession >
1050 :return: the root menu ids
1054 context = s.eval_context(req.context)
1055 Menus = s.model('ir.ui.menu')
1056 # If a menu action is defined use its domain to get the root menu items
1057 user_menu_id = s.model('res.users').read([s._uid], ['menu_id'], context)[0]['menu_id']
1059 menu_domain = [('parent_id', '=', False)]
1061 domain_string = s.model('ir.actions.act_window').read([user_menu_id[0]], ['domain'], context)[0]['domain']
1063 menu_domain = ast.literal_eval(domain_string)
1065 return Menus.search(menu_domain, 0, False, False, context)
1067 def do_load(self, req):
1068 """ Loads all menu items (all applications and their sub-menus).
1070 :param req: A request object, with an OpenERP session attribute
1071 :type req: < session -> OpenERPSession >
1072 :return: the menu root
1073 :rtype: dict('children': menu_nodes)
1075 context = req.session.eval_context(req.context)
1076 Menus = req.session.model('ir.ui.menu')
1078 menu_roots = Menus.read(self.do_get_user_roots(req), ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled'], context)
1079 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children': menu_roots}
1081 # menus are loaded fully unlike a regular tree view, cause there are a
1082 # limited number of items (752 when all 6.1 addons are installed)
1083 menu_ids = Menus.search([], 0, False, False, context)
1084 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id', 'action', 'needaction_enabled'], context)
1085 # adds roots at the end of the sequence, so that they will overwrite
1086 # equivalent menu items from full menu read when put into id:item
1087 # mapping, resulting in children being correctly set on the roots.
1088 menu_items.extend(menu_roots)
1090 # make a tree using parent_id
1091 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
1092 for menu_item in menu_items:
1093 if menu_item['parent_id']:
1094 parent = menu_item['parent_id'][0]
1097 if parent in menu_items_map:
1098 menu_items_map[parent].setdefault(
1099 'children', []).append(menu_item)
1101 # sort by sequence a tree using parent_id
1102 for menu_item in menu_items:
1103 menu_item.setdefault('children', []).sort(
1104 key=operator.itemgetter('sequence'))
1108 def do_load_needaction(self, req, menu_ids=False):
1109 """ Loads needaction counters for all or some specific menu ids.
1111 :param req: A request object, with an OpenERP session attribute
1112 :type req: < session -> OpenERPSession >
1113 :return: the menu root
1114 :rtype: dict('children': menu_nodes)
1116 context = req.session.eval_context(req.context)
1117 Menus = req.session.model('ir.ui.menu')
1119 if menu_ids == False:
1120 menu_ids = Menus.search([], context=context)
1121 # TDE FIXME: set needaction_enabled column to store, to enable this more limited search
1122 # menu_ids = Menus.search([('needaction_enabled', '=', True)], context=context)
1124 menu_needaction_data = Menus.get_needaction_data(menu_ids, context)
1125 return menu_needaction_data
1127 @openerpweb.jsonrequest
1128 def action(self, req, menu_id):
1129 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
1130 [('ir.ui.menu', menu_id)], False)
1131 return {"action": actions}
1133 class DataSet(openerpweb.Controller):
1134 _cp_path = "/web/dataset"
1136 @openerpweb.jsonrequest
1137 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
1138 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
1139 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
1141 """ Performs a search() followed by a read() (if needed) using the
1142 provided search criteria
1144 :param req: a JSON-RPC request object
1145 :type req: openerpweb.JsonRequest
1146 :param str model: the name of the model to search on
1147 :param fields: a list of the fields to return in the result records
1149 :param int offset: from which index should the results start being returned
1150 :param int limit: the maximum number of records to return
1151 :param list domain: the search domain for the query
1152 :param list sort: sorting directives
1153 :returns: A structure (dict) with two keys: ids (all the ids matching
1154 the (domain, context) pair) and records (paginated records
1155 matching fields selection set)
1158 Model = req.session.model(model)
1160 context, domain = eval_context_and_domain(
1161 req.session, req.context, domain)
1163 ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
1164 if limit and len(ids) == limit:
1165 length = Model.search_count(domain, context)
1167 length = len(ids) + (offset or 0)
1168 if fields and fields == ['id']:
1169 # shortcut read if we only want the ids
1172 'records': [{'id': id} for id in ids]
1175 records = Model.read(ids, fields or False, context)
1176 records.sort(key=lambda obj: ids.index(obj['id']))
1182 @openerpweb.jsonrequest
1183 def load(self, req, model, id, fields):
1184 m = req.session.model(model)
1186 r = m.read([id], False, req.session.eval_context(req.context))
1189 return {'value': value}
1191 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
1192 has_domain = domain_id is not None and domain_id < len(args)
1193 has_context = context_id is not None and context_id < len(args)
1195 domain = args[domain_id] if has_domain else []
1196 context = args[context_id] if has_context else {}
1197 c, d = eval_context_and_domain(req.session, context, domain)
1201 args[context_id] = c
1203 return self._call_kw(req, model, method, args, {})
1205 def _call_kw(self, req, model, method, args, kwargs):
1206 for i in xrange(len(args)):
1207 if isinstance(args[i], nonliterals.BaseContext):
1208 args[i] = req.session.eval_context(args[i])
1209 elif isinstance(args[i], nonliterals.BaseDomain):
1210 args[i] = req.session.eval_domain(args[i])
1211 for k in kwargs.keys():
1212 if isinstance(kwargs[k], nonliterals.BaseContext):
1213 kwargs[k] = req.session.eval_context(kwargs[k])
1214 elif isinstance(kwargs[k], nonliterals.BaseDomain):
1215 kwargs[k] = req.session.eval_domain(kwargs[k])
1217 # Temporary implements future display_name special field for model#read()
1218 if method == 'read' and kwargs.get('context') and kwargs['context'].get('future_display_name'):
1219 if 'display_name' in args[1]:
1220 names = req.session.model(model).name_get(args[0], **kwargs)
1221 args[1].remove('display_name')
1222 r = getattr(req.session.model(model), method)(*args, **kwargs)
1223 for i in range(len(r)):
1224 r[i]['display_name'] = names[i][1] or "%s#%d" % (model, names[i][0])
1227 return getattr(req.session.model(model), method)(*args, **kwargs)
1229 @openerpweb.jsonrequest
1230 def onchange(self, req, model, method, args, context_id=None):
1231 """ Support method for handling onchange calls: behaves much like call
1232 with the following differences:
1234 * Does not take a domain_id
1235 * Is aware of the return value's structure, and will parse the domains
1236 if needed in order to return either parsed literal domains (in JSON)
1237 or non-literal domain instances, allowing those domains to be used
1241 :type req: web.common.http.JsonRequest
1242 :param str model: object type on which to call the method
1243 :param str method: name of the onchange handler method
1244 :param list args: arguments to call the onchange handler with
1245 :param int context_id: index of the context object in the list of
1247 :return: result of the onchange call with all domains parsed
1249 result = self.call_common(req, model, method, args, context_id=context_id)
1250 if not result or 'domain' not in result:
1253 result['domain'] = dict(
1254 (k, parse_domain(v, req.session))
1255 for k, v in result['domain'].iteritems())
1259 @openerpweb.jsonrequest
1260 def call(self, req, model, method, args, domain_id=None, context_id=None):
1261 return self.call_common(req, model, method, args, domain_id, context_id)
1263 @openerpweb.jsonrequest
1264 def call_kw(self, req, model, method, args, kwargs):
1265 return self._call_kw(req, model, method, args, kwargs)
1267 @openerpweb.jsonrequest
1268 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
1269 context = req.session.eval_context(req.context)
1270 action = self.call_common(req, model, method, args, domain_id, context_id)
1271 if isinstance(action, dict) and action.get('type') != '':
1272 return clean_action(req, action, context)
1275 @openerpweb.jsonrequest
1276 def exec_workflow(self, req, model, id, signal):
1277 return req.session.exec_workflow(model, id, signal)
1279 @openerpweb.jsonrequest
1280 def resequence(self, req, model, ids, field='sequence', offset=0):
1281 """ Re-sequences a number of records in the model, by their ids
1283 The re-sequencing starts at the first model of ``ids``, the sequence
1284 number is incremented by one after each record and starts at ``offset``
1286 :param ids: identifiers of the records to resequence, in the new sequence order
1288 :param str field: field used for sequence specification, defaults to
1290 :param int offset: sequence number for first record in ``ids``, allows
1291 starting the resequencing from an arbitrary number,
1294 m = req.session.model(model)
1295 if not m.fields_get([field]):
1297 # python 2.6 has no start parameter
1298 for i, id in enumerate(ids):
1299 m.write(id, { field: i + offset })
1302 class View(openerpweb.Controller):
1303 _cp_path = "/web/view"
1305 def fields_view_get(self, req, model, view_id, view_type,
1306 transform=True, toolbar=False, submenu=False):
1307 Model = req.session.model(model)
1308 context = req.session.eval_context(req.context)
1309 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
1310 # todo fme?: check that we should pass the evaluated context here
1311 self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
1312 if toolbar and transform:
1313 self.process_toolbar(req, fvg['toolbar'])
1316 def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
1317 # depending on how it feels, xmlrpclib.ServerProxy can translate
1318 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
1319 # enjoy unicode strings which can not be trivially converted to
1320 # strings, and it blows up during parsing.
1322 # So ensure we fix this retardation by converting view xml back to
1324 if isinstance(fvg['arch'], unicode):
1325 arch = fvg['arch'].encode('utf-8')
1328 fvg['arch_string'] = arch
1331 evaluation_context = session.evaluation_context(context or {})
1332 xml = self.transform_view(arch, session, evaluation_context)
1334 xml = ElementTree.fromstring(arch)
1335 fvg['arch'] = xml2json_from_elementtree(xml, preserve_whitespaces)
1337 if 'id' in fvg['fields']:
1338 # Special case for id's
1339 id_field = fvg['fields']['id']
1340 id_field['original_type'] = id_field['type']
1341 id_field['type'] = 'id'
1343 for field in fvg['fields'].itervalues():
1344 if field.get('views'):
1345 for view in field["views"].itervalues():
1346 self.process_view(session, view, None, transform)
1347 if field.get('domain'):
1348 field["domain"] = parse_domain(field["domain"], session)
1349 if field.get('context'):
1350 field["context"] = parse_context(field["context"], session)
1352 def process_toolbar(self, req, toolbar):
1354 The toolbar is a mapping of section_key: [action_descriptor]
1356 We need to clean all those actions in order to ensure correct
1359 for actions in toolbar.itervalues():
1360 for action in actions:
1361 if 'context' in action:
1362 action['context'] = parse_context(
1363 action['context'], req.session)
1364 if 'domain' in action:
1365 action['domain'] = parse_domain(
1366 action['domain'], req.session)
1368 @openerpweb.jsonrequest
1369 def add_custom(self, req, view_id, arch):
1370 CustomView = req.session.model('ir.ui.view.custom')
1372 'user_id': req.session._uid,
1375 }, req.session.eval_context(req.context))
1376 return {'result': True}
1378 @openerpweb.jsonrequest
1379 def undo_custom(self, req, view_id, reset=False):
1380 CustomView = req.session.model('ir.ui.view.custom')
1381 context = req.session.eval_context(req.context)
1382 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
1383 0, False, False, context)
1386 CustomView.unlink(vcustom, context)
1388 CustomView.unlink([vcustom[0]], context)
1389 return {'result': True}
1390 return {'result': False}
1392 def transform_view(self, view_string, session, context=None):
1393 # transform nodes on the fly via iterparse, instead of
1394 # doing it statically on the parsing result
1395 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
1397 for event, elem in parser:
1398 if event == "start":
1401 self.parse_domains_and_contexts(elem, session)
1404 def parse_domains_and_contexts(self, elem, session):
1405 """ Converts domains and contexts from the view into Python objects,
1406 either literals if they can be parsed by literal_eval or a special
1407 placeholder object if the domain or context refers to free variables.
1409 :param elem: the current node being parsed
1410 :type param: xml.etree.ElementTree.Element
1411 :param session: OpenERP session object, used to store and retrieve
1413 :type session: openerpweb.openerpweb.OpenERPSession
1415 for el in ['domain', 'filter_domain']:
1416 domain = elem.get(el, '').strip()
1418 elem.set(el, parse_domain(domain, session))
1419 elem.set(el + '_string', domain)
1420 for el in ['context', 'default_get']:
1421 context_string = elem.get(el, '').strip()
1423 elem.set(el, parse_context(context_string, session))
1424 elem.set(el + '_string', context_string)
1426 @openerpweb.jsonrequest
1427 def load(self, req, model, view_id, view_type, toolbar=False):
1428 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1430 class TreeView(View):
1431 _cp_path = "/web/treeview"
1433 @openerpweb.jsonrequest
1434 def action(self, req, model, id):
1435 return load_actions_from_ir_values(
1436 req,'action', 'tree_but_open',[(model, id)],
1439 class SearchView(View):
1440 _cp_path = "/web/searchview"
1442 @openerpweb.jsonrequest
1443 def load(self, req, model, view_id):
1444 fields_view = self.fields_view_get(req, model, view_id, 'search')
1445 return {'fields_view': fields_view}
1447 @openerpweb.jsonrequest
1448 def fields_get(self, req, model):
1449 Model = req.session.model(model)
1450 fields = Model.fields_get(False, req.session.eval_context(req.context))
1451 for field in fields.values():
1452 # shouldn't convert the views too?
1453 if field.get('domain'):
1454 field["domain"] = parse_domain(field["domain"], req.session)
1455 if field.get('context'):
1456 field["context"] = parse_context(field["context"], req.session)
1457 return {'fields': fields}
1459 @openerpweb.jsonrequest
1460 def get_filters(self, req, model):
1461 logger = logging.getLogger(__name__ + '.SearchView.get_filters')
1462 Model = req.session.model("ir.filters")
1463 filters = Model.get_filters(model)
1464 for filter in filters:
1466 parsed_context = parse_context(filter["context"], req.session)
1467 filter["context"] = (parsed_context
1468 if not isinstance(parsed_context, nonliterals.BaseContext)
1469 else req.session.eval_context(parsed_context))
1471 parsed_domain = parse_domain(filter["domain"], req.session)
1472 filter["domain"] = (parsed_domain
1473 if not isinstance(parsed_domain, nonliterals.BaseDomain)
1474 else req.session.eval_domain(parsed_domain))
1476 logger.exception("Failed to parse custom filter %s in %s",
1477 filter['name'], model)
1478 filter['disabled'] = True
1479 del filter['context']
1480 del filter['domain']
1483 class Binary(openerpweb.Controller):
1484 _cp_path = "/web/binary"
1486 @openerpweb.httprequest
1487 def image(self, req, model, id, field, **kw):
1488 last_update = '__last_update'
1489 Model = req.session.model(model)
1490 context = req.session.eval_context(req.context)
1491 headers = [('Content-Type', 'image/png')]
1492 etag = req.httprequest.headers.get('If-None-Match')
1493 hashed_session = hashlib.md5(req.session_id).hexdigest()
1494 id = None if not id else simplejson.loads(id)
1495 if type(id) is list:
1498 if not id and hashed_session == etag:
1499 return werkzeug.wrappers.Response(status=304)
1501 date = Model.read([id], [last_update], context)[0].get(last_update)
1502 if hashlib.md5(date).hexdigest() == etag:
1503 return werkzeug.wrappers.Response(status=304)
1505 retag = hashed_session
1508 res = Model.default_get([field], context).get(field)
1511 res = Model.read([id], [last_update, field], context)[0]
1512 retag = hashlib.md5(res.get(last_update)).hexdigest()
1513 image_base64 = res.get(field)
1515 if kw.get('resize'):
1516 resize = kw.get('resize').split(',');
1517 if len(resize) == 2 and int(resize[0]) and int(resize[1]):
1518 width = int(resize[0])
1519 height = int(resize[1])
1520 # resize maximum 500*500
1521 if width > 500: width = 500
1522 if height > 500: height = 500
1523 image_base64 = openerp.tools.image_resize_image(base64_source=image_base64, size=(width, height), encoding='base64', filetype='PNG')
1525 image_data = base64.b64decode(image_base64)
1527 except (TypeError, xmlrpclib.Fault):
1528 image_data = self.placeholder(req)
1529 headers.append(('ETag', retag))
1530 headers.append(('Content-Length', len(image_data)))
1532 ncache = int(kw.get('cache'))
1533 headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
1536 return req.make_response(image_data, headers)
1537 def placeholder(self, req):
1538 addons_path = openerpweb.addons_manifest['web']['addons_path']
1539 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1541 @openerpweb.httprequest
1542 def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1543 """ Download link for files stored as binary fields.
1545 If the ``id`` parameter is omitted, fetches the default value for the
1546 binary field (via ``default_get``), otherwise fetches the field for
1547 that precise record.
1549 :param req: OpenERP request
1550 :type req: :class:`web.common.http.HttpRequest`
1551 :param str model: name of the model to fetch the binary from
1552 :param str field: binary field
1553 :param str id: id of the record from which to fetch the binary
1554 :param str filename_field: field holding the file's name, if any
1555 :returns: :class:`werkzeug.wrappers.Response`
1557 Model = req.session.model(model)
1558 context = req.session.eval_context(req.context)
1561 fields.append(filename_field)
1563 res = Model.read([int(id)], fields, context)[0]
1565 res = Model.default_get(fields, context)
1566 filecontent = base64.b64decode(res.get(field, ''))
1568 return req.not_found()
1570 filename = '%s_%s' % (model.replace('.', '_'), id)
1572 filename = res.get(filename_field, '') or filename
1573 return req.make_response(filecontent,
1574 [('Content-Type', 'application/octet-stream'),
1575 ('Content-Disposition', content_disposition(filename, req))])
1577 @openerpweb.httprequest
1578 def saveas_ajax(self, req, data, token):
1579 jdata = simplejson.loads(data)
1580 model = jdata['model']
1581 field = jdata['field']
1582 id = jdata.get('id', None)
1583 filename_field = jdata.get('filename_field', None)
1584 context = jdata.get('context', dict())
1586 context = req.session.eval_context(context)
1587 Model = req.session.model(model)
1590 fields.append(filename_field)
1592 res = Model.read([int(id)], fields, context)[0]
1594 res = Model.default_get(fields, context)
1595 filecontent = base64.b64decode(res.get(field, ''))
1597 raise ValueError("No content found for field '%s' on '%s:%s'" %
1600 filename = '%s_%s' % (model.replace('.', '_'), id)
1602 filename = res.get(filename_field, '') or filename
1603 return req.make_response(filecontent,
1604 headers=[('Content-Type', 'application/octet-stream'),
1605 ('Content-Disposition', content_disposition(filename, req))],
1606 cookies={'fileToken': int(token)})
1608 @openerpweb.httprequest
1609 def upload(self, req, callback, ufile):
1610 # TODO: might be useful to have a configuration flag for max-length file uploads
1612 out = """<script language="javascript" type="text/javascript">
1613 var win = window.top.window;
1614 win.jQuery(win).trigger(%s, %s);
1617 args = [len(data), ufile.filename,
1618 ufile.content_type, base64.b64encode(data)]
1619 except Exception, e:
1620 args = [False, e.message]
1621 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1623 @openerpweb.httprequest
1624 def upload_attachment(self, req, callback, model, id, ufile):
1625 context = req.session.eval_context(req.context)
1626 Model = req.session.model('ir.attachment')
1628 out = """<script language="javascript" type="text/javascript">
1629 var win = window.top.window;
1630 win.jQuery(win).trigger(%s, %s);
1632 attachment_id = Model.create({
1633 'name': ufile.filename,
1634 'datas': base64.encodestring(ufile.read()),
1635 'datas_fname': ufile.filename,
1640 'filename': ufile.filename,
1644 args = {'erorr':e.faultCode.split('--')[1],'title':e.faultCode.split('--')[0]}
1645 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1647 class Action(openerpweb.Controller):
1648 _cp_path = "/web/action"
1650 @openerpweb.jsonrequest
1651 def load(self, req, action_id, do_not_eval=False, eval_context=None):
1652 Actions = req.session.model('ir.actions.actions')
1654 context = req.session.eval_context(req.context)
1655 eval_context = req.session.eval_context(nonliterals.CompoundContext(context, eval_context or {}))
1658 action_id = int(action_id)
1661 module, xmlid = action_id.split('.', 1)
1662 model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
1663 assert model.startswith('ir.actions.')
1665 action_id = 0 # force failed read
1667 base_action = Actions.read([action_id], ['type'], context)
1670 action_type = base_action[0]['type']
1671 if action_type == 'ir.actions.report.xml':
1672 ctx.update({'bin_size': True})
1674 action = req.session.model(action_type).read([action_id], False, ctx)
1676 value = clean_action(req, action[0], eval_context, do_not_eval)
1679 @openerpweb.jsonrequest
1680 def run(self, req, action_id):
1681 context = req.session.eval_context(req.context)
1682 return_action = req.session.model('ir.actions.server').run(
1683 [action_id], req.session.eval_context(req.context))
1685 return clean_action(req, return_action, context)
1690 _cp_path = "/web/export"
1692 @openerpweb.jsonrequest
1693 def formats(self, req):
1694 """ Returns all valid export formats
1696 :returns: for each export format, a pair of identifier and printable name
1697 :rtype: [(str, str)]
1701 for path, controller in openerpweb.controllers_path.iteritems()
1702 if path.startswith(self._cp_path)
1703 if hasattr(controller, 'fmt')
1704 ], key=operator.itemgetter("label"))
1706 def fields_get(self, req, model):
1707 Model = req.session.model(model)
1708 fields = Model.fields_get(False, req.session.eval_context(req.context))
1711 @openerpweb.jsonrequest
1712 def get_fields(self, req, model, prefix='', parent_name= '',
1713 import_compat=True, parent_field_type=None,
1716 if import_compat and parent_field_type == "many2one":
1719 fields = self.fields_get(req, model)
1722 fields.pop('id', None)
1724 fields['.id'] = fields.pop('id', {'string': 'ID'})
1726 fields_sequence = sorted(fields.iteritems(),
1727 key=lambda field: field[1].get('string', ''))
1730 for field_name, field in fields_sequence:
1732 if exclude and field_name in exclude:
1734 if field.get('readonly'):
1735 # If none of the field's states unsets readonly, skip the field
1736 if all(dict(attrs).get('readonly', True)
1737 for attrs in field.get('states', {}).values()):
1740 id = prefix + (prefix and '/'or '') + field_name
1741 name = parent_name + (parent_name and '/' or '') + field['string']
1742 record = {'id': id, 'string': name,
1743 'value': id, 'children': False,
1744 'field_type': field.get('type'),
1745 'required': field.get('required'),
1746 'relation_field': field.get('relation_field')}
1747 records.append(record)
1749 if len(name.split('/')) < 3 and 'relation' in field:
1750 ref = field.pop('relation')
1751 record['value'] += '/id'
1752 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1754 if not import_compat or field['type'] == 'one2many':
1755 # m2m field in import_compat is childless
1756 record['children'] = True
1760 @openerpweb.jsonrequest
1761 def namelist(self,req, model, export_id):
1762 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1763 export = req.session.model("ir.exports").read([export_id])[0]
1764 export_fields_list = req.session.model("ir.exports.line").read(
1765 export['export_fields'])
1767 fields_data = self.fields_info(
1768 req, model, map(operator.itemgetter('name'), export_fields_list))
1771 {'name': field['name'], 'label': fields_data[field['name']]}
1772 for field in export_fields_list
1775 def fields_info(self, req, model, export_fields):
1777 fields = self.fields_get(req, model)
1778 if ".id" in export_fields:
1779 fields['.id'] = fields.pop('id', {'string': 'ID'})
1781 # To make fields retrieval more efficient, fetch all sub-fields of a
1782 # given field at the same time. Because the order in the export list is
1783 # arbitrary, this requires ordering all sub-fields of a given field
1784 # together so they can be fetched at the same time
1786 # Works the following way:
1787 # * sort the list of fields to export, the default sorting order will
1788 # put the field itself (if present, for xmlid) and all of its
1789 # sub-fields right after it
1790 # * then, group on: the first field of the path (which is the same for
1791 # a field and for its subfields and the length of splitting on the
1792 # first '/', which basically means grouping the field on one side and
1793 # all of the subfields on the other. This way, we have the field (for
1794 # the xmlid) with length 1, and all of the subfields with the same
1795 # base but a length "flag" of 2
1796 # * if we have a normal field (length 1), just add it to the info
1797 # mapping (with its string) as-is
1798 # * otherwise, recursively call fields_info via graft_subfields.
1799 # all graft_subfields does is take the result of fields_info (on the
1800 # field's model) and prepend the current base (current field), which
1801 # rebuilds the whole sub-tree for the field
1803 # result: because we're not fetching the fields_get for half the
1804 # database models, fetching a namelist with a dozen fields (including
1805 # relational data) falls from ~6s to ~300ms (on the leads model).
1806 # export lists with no sub-fields (e.g. import_compatible lists with
1807 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1808 # there's a single fields_get to execute)
1809 for (base, length), subfields in itertools.groupby(
1810 sorted(export_fields),
1811 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1812 subfields = list(subfields)
1814 # subfields is a seq of $base/*rest, and not loaded yet
1815 info.update(self.graft_subfields(
1816 req, fields[base]['relation'], base, fields[base]['string'],
1820 info[base] = fields[base]['string']
1824 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1825 export_fields = [field.split('/', 1)[1] for field in fields]
1827 (prefix + '/' + k, prefix_string + '/' + v)
1828 for k, v in self.fields_info(req, model, export_fields).iteritems())
1830 #noinspection PyPropertyDefinition
1832 def content_type(self):
1833 """ Provides the format's content type """
1834 raise NotImplementedError()
1836 def filename(self, base):
1837 """ Creates a valid filename for the format (with extension) from the
1838 provided base name (exension-less)
1840 raise NotImplementedError()
1842 def from_data(self, fields, rows):
1843 """ Conversion method from OpenERP's export data to whatever the
1844 current export class outputs
1846 :params list fields: a list of fields to export
1847 :params list rows: a list of records to export
1851 raise NotImplementedError()
1853 @openerpweb.httprequest
1854 def index(self, req, data, token):
1855 model, fields, ids, domain, import_compat = \
1856 operator.itemgetter('model', 'fields', 'ids', 'domain',
1858 simplejson.loads(data))
1860 context = req.session.eval_context(req.context)
1861 Model = req.session.model(model)
1862 ids = ids or Model.search(domain, 0, False, False, context)
1864 field_names = map(operator.itemgetter('name'), fields)
1865 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1868 columns_headers = field_names
1870 columns_headers = [val['label'].strip() for val in fields]
1873 return req.make_response(self.from_data(columns_headers, import_data),
1874 headers=[('Content-Disposition',
1875 content_disposition(self.filename(model), req)),
1876 ('Content-Type', self.content_type)],
1877 cookies={'fileToken': int(token)})
1879 class CSVExport(Export):
1880 _cp_path = '/web/export/csv'
1881 fmt = {'tag': 'csv', 'label': 'CSV'}
1884 def content_type(self):
1885 return 'text/csv;charset=utf8'
1887 def filename(self, base):
1888 return base + '.csv'
1890 def from_data(self, fields, rows):
1892 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1894 writer.writerow([name.encode('utf-8') for name in fields])
1899 if isinstance(d, basestring):
1900 d = d.replace('\n',' ').replace('\t',' ')
1902 d = d.encode('utf-8')
1903 except UnicodeError:
1905 if d is False: d = None
1907 writer.writerow(row)
1914 class ExcelExport(Export):
1915 _cp_path = '/web/export/xls'
1919 'error': None if xlwt else "XLWT required"
1923 def content_type(self):
1924 return 'application/vnd.ms-excel'
1926 def filename(self, base):
1927 return base + '.xls'
1929 def from_data(self, fields, rows):
1930 workbook = xlwt.Workbook()
1931 worksheet = workbook.add_sheet('Sheet 1')
1933 for i, fieldname in enumerate(fields):
1934 worksheet.write(0, i, fieldname)
1935 worksheet.col(i).width = 8000 # around 220 pixels
1937 style = xlwt.easyxf('align: wrap yes')
1939 for row_index, row in enumerate(rows):
1940 for cell_index, cell_value in enumerate(row):
1941 if isinstance(cell_value, basestring):
1942 cell_value = re.sub("\r", " ", cell_value)
1943 if cell_value is False: cell_value = None
1944 worksheet.write(row_index + 1, cell_index, cell_value, style)
1953 class Reports(View):
1954 _cp_path = "/web/report"
1955 POLLING_DELAY = 0.25
1957 'doc': 'application/vnd.ms-word',
1958 'html': 'text/html',
1959 'odt': 'application/vnd.oasis.opendocument.text',
1960 'pdf': 'application/pdf',
1961 'sxw': 'application/vnd.sun.xml.writer',
1962 'xls': 'application/vnd.ms-excel',
1965 @openerpweb.httprequest
1966 def index(self, req, action, token):
1967 action = simplejson.loads(action)
1969 report_srv = req.session.proxy("report")
1970 context = req.session.eval_context(
1971 nonliterals.CompoundContext(
1972 req.context or {}, action[ "context"]))
1975 report_ids = context["active_ids"]
1976 if 'report_type' in action:
1977 report_data['report_type'] = action['report_type']
1978 if 'datas' in action:
1979 if 'ids' in action['datas']:
1980 report_ids = action['datas'].pop('ids')
1981 report_data.update(action['datas'])
1983 report_id = report_srv.report(
1984 req.session._db, req.session._uid, req.session._password,
1985 action["report_name"], report_ids,
1986 report_data, context)
1988 report_struct = None
1990 report_struct = report_srv.report_get(
1991 req.session._db, req.session._uid, req.session._password, report_id)
1992 if report_struct["state"]:
1995 time.sleep(self.POLLING_DELAY)
1997 report = base64.b64decode(report_struct['result'])
1998 if report_struct.get('code') == 'zlib':
1999 report = zlib.decompress(report)
2000 report_mimetype = self.TYPES_MAPPING.get(
2001 report_struct['format'], 'octet-stream')
2002 file_name = action.get('name', 'report')
2003 if 'name' not in action:
2004 reports = req.session.model('ir.actions.report.xml')
2005 res_id = reports.search([('report_name', '=', action['report_name']),],
2006 0, False, False, context)
2008 file_name = reports.read(res_id[0], ['name'], context)['name']
2010 file_name = action['report_name']
2011 file_name = '%s.%s' % (file_name, report_struct['format'])
2013 return req.make_response(report,
2015 ('Content-Disposition', content_disposition(file_name, req)),
2016 ('Content-Type', report_mimetype),
2017 ('Content-Length', len(report))],
2018 cookies={'fileToken': int(token)})
2020 # vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: