except ImportError:
xlwt = None
-from .. import common
-openerpweb = common.http
+import openerp
+import openerp.modules.registry
+from openerp.tools.translate import _
+
+from .. import http
+openerpweb = http
#----------------------------------------------------------
-# OpenERP Web web Controllers
+# OpenERP Web helpers
#----------------------------------------------------------
+def rjsmin(script):
+ """ Minify js with a clever regex.
+ Taken from http://opensource.perlig.de/rjsmin
+ Apache License, Version 2.0 """
+ def subber(match):
+ """ Substitution callback """
+ groups = match.groups()
+ return (
+ groups[0] or
+ groups[1] or
+ groups[2] or
+ groups[3] or
+ (groups[4] and '\n') or
+ (groups[5] and ' ') or
+ (groups[6] and ' ') or
+ (groups[7] and ' ') or
+ ''
+ )
+
+ result = re.sub(
+ r'([^\047"/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]|\r?'
+ r'\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|'
+ r'\r)[^"\\\r\n]*)*"))[^\047"/\000-\040]*)|(?:(?<=[(,=:\[!&|?{};\r\n]'
+ r')(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/'
+ r'))*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*'
+ r'(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*'
+ r'))|(?:(?<=[\000-#%-,./:-@\[-^`{-~-]return)(?:[\000-\011\013\014\01'
+ r'6-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*((?:/(?![\r\n/*])[^/'
+ r'\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]'
+ r'*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*))|(?<=[^\000-!#%&(*,./'
+ r':-@\[\\^`{|~])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/'
+ r'*][^*]*\*+)*/))*(?:((?:(?://[^\r\n]*)?[\r\n]))(?:[\000-\011\013\01'
+ r'4\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040"#'
+ r'%-\047)*,./:-@\\-^`|-~])|(?<=[^\000-#%-,./:-@\[-^`{-~-])((?:[\000-'
+ r'\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=[^'
+ r'\000-#%-,./:-@\[-^`{-~-])|(?<=\+)((?:[\000-\011\013\014\016-\040]|'
+ r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=\+)|(?<=-)((?:[\000-\011\0'
+ r'13\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=-)|(?:[\0'
+ r'00-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))+|(?:'
+ r'(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*'
+ r']*\*+(?:[^/*][^*]*\*+)*/))*)+', subber, '\n%s\n' % script
+ ).strip()
+ return result
+
+def db_list(req):
+ proxy = req.session.proxy("db")
+ dbs = proxy.list()
+ h = req.httprequest.environ['HTTP_HOST'].split(':')[0]
+ d = h.split('.')[0]
+ r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d)
+ dbs = [i for i in dbs if re.match(r, i)]
+ return dbs
+
+def db_monodb(req):
+ # if only one db exists, return it else return False
+ try:
+ dbs = db_list(req)
+ if len(dbs) == 1:
+ return dbs[0]
+ except xmlrpclib.Fault:
+ # ignore access denied
+ pass
+ return False
+
+def module_topological_sort(modules):
+ """ Return a list of module names sorted so that their dependencies of the
+ modules are listed before the module itself
+
+ modules is a dict of {module_name: dependencies}
+
+ :param modules: modules to sort
+ :type modules: dict
+ :returns: list(str)
+ """
+
+ dependencies = set(itertools.chain.from_iterable(modules.itervalues()))
+ # incoming edge: dependency on other module (if a depends on b, a has an
+ # incoming edge from b, aka there's an edge from b to a)
+ # outgoing edge: other module depending on this one
+
+ # [Tarjan 1976], http://en.wikipedia.org/wiki/Topological_sorting#Algorithms
+ #L ← Empty list that will contain the sorted nodes
+ L = []
+ #S ← Set of all nodes with no outgoing edges (modules on which no other
+ # module depends)
+ S = set(module for module in modules if module not in dependencies)
+
+ visited = set()
+ #function visit(node n)
+ def visit(n):
+ #if n has not been visited yet then
+ if n not in visited:
+ #mark n as visited
+ visited.add(n)
+ #change: n not web module, can not be resolved, ignore
+ if n not in modules: return
+ #for each node m with an edge from m to n do (dependencies of n)
+ for m in modules[n]:
+ #visit(m)
+ visit(m)
+ #add n to L
+ L.append(n)
+ #for each node n in S do
+ for n in S:
+ #visit(n)
+ visit(n)
+ return L
+
+def module_installed(req):
+ # Candidates module the current heuristic is the /static dir
+ loadable = openerpweb.addons_manifest.keys()
+ modules = {}
+
+ # Retrieve database installed modules
+ # TODO The following code should move to ir.module.module.list_installed_modules()
+ Modules = req.session.model('ir.module.module')
+ domain = [('state','=','installed'), ('name','in', loadable)]
+ for module in Modules.search_read(domain, ['name', 'dependencies_id']):
+ modules[module['name']] = []
+ deps = module.get('dependencies_id')
+ if deps:
+ deps_read = req.session.model('ir.module.module.dependency').read(deps, ['name'])
+ dependencies = [i['name'] for i in deps_read]
+ modules[module['name']] = dependencies
+
+ sorted_modules = module_topological_sort(modules)
+ return sorted_modules
+
+def module_installed_bypass_session(dbname):
+ loadable = openerpweb.addons_manifest.keys()
+ modules = {}
+ try:
+ registry = openerp.modules.registry.RegistryManager.get(dbname)
+ with registry.cursor() as cr:
+ m = registry.get('ir.module.module')
+ # TODO The following code should move to ir.module.module.list_installed_modules()
+ domain = [('state','=','installed'), ('name','in', loadable)]
+ ids = m.search(cr, 1, [('state','=','installed'), ('name','in', loadable)])
+ for module in m.read(cr, 1, ids, ['name', 'dependencies_id']):
+ modules[module['name']] = []
+ deps = module.get('dependencies_id')
+ if deps:
+ deps_read = registry.get('ir.module.module.dependency').read(cr, 1, deps, ['name'])
+ dependencies = [i['name'] for i in deps_read]
+ modules[module['name']] = dependencies
+ except Exception,e:
+ pass
+ sorted_modules = module_topological_sort(modules)
+ return sorted_modules
+
+def module_boot(req, db=None):
+ server_wide_modules = openerp.conf.server_wide_modules or ['web']
+ serverside = []
+ dbside = []
+ for i in server_wide_modules:
+ if i in openerpweb.addons_manifest:
+ serverside.append(i)
+ monodb = db or db_monodb(req)
+ if monodb:
+ dbside = module_installed_bypass_session(monodb)
+ dbside = [i for i in dbside if i not in serverside]
+ addons = serverside + dbside
+ return addons
def concat_xml(file_list):
"""Concatenate xml files
root.append(child)
return ElementTree.tostring(root, 'utf-8'), checksum.hexdigest()
-
def concat_files(file_list, reader=None, intersperse=""):
""" Concatenates contents of all provided files
if reader is None:
def reader(f):
- with open(f) as fp:
+ with open(f, 'rb') as fp:
return fp.read()
files_content = []
files_concat = intersperse.join(files_content)
return files_concat, checksum.hexdigest()
+concat_js_cache = {}
+
+def concat_js(file_list):
+ content, checksum = concat_files(file_list, intersperse=';')
+ if checksum in concat_js_cache:
+ content = concat_js_cache[checksum]
+ else:
+ content = rjsmin(content)
+ concat_js_cache[checksum] = content
+ return content, checksum
+
+def fs2web(path):
+ """convert FS path into web path"""
+ return '/'.join(path.split(os.path.sep))
+
+def manifest_glob(req, extension, addons=None, db=None):
+ if addons is None:
+ addons = module_boot(req, db=db)
+ else:
+ addons = addons.split(',')
+ r = []
+ for addon in addons:
+ manifest = openerpweb.addons_manifest.get(addon, None)
+ if not manifest:
+ continue
+ # ensure does not ends with /
+ addons_path = os.path.join(manifest['addons_path'], '')[:-1]
+ globlist = manifest.get(extension, [])
+ for pattern in globlist:
+ for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
+ r.append((path, fs2web(path[len(addons_path):])))
+ return r
+
+def manifest_list(req, extension, mods=None, db=None):
+ if not req.debug:
+ path = '/web/webclient/' + extension
+ if mods is not None:
+ path += '?mods=' + mods
+ elif db:
+ path += '?db=' + db
+ return [path]
+ files = manifest_glob(req, extension, addons=mods, db=db)
+ i_am_diabetic = req.httprequest.environ["QUERY_STRING"].count("no_sugar") >= 1 or \
+ req.httprequest.environ.get('HTTP_REFERER', '').count("no_sugar") >= 1
+ if i_am_diabetic:
+ return [wp for _fp, wp in files]
+ else:
+ return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in files]
+
+def get_last_modified(files):
+ """ Returns the modification time of the most recently modified
+ file provided
+
+ :param list(str) files: names of files to check
+ :return: most recent modification time amongst the fileset
+ :rtype: datetime.datetime
+ """
+ files = list(files)
+ if files:
+ return max(datetime.datetime.fromtimestamp(os.path.getmtime(f))
+ for f in files)
+ return datetime.datetime(1970, 1, 1)
+
+def make_conditional(req, response, last_modified=None, etag=None):
+ """ Makes the provided response conditional based upon the request,
+ and mandates revalidation from clients
+
+ Uses Werkzeug's own :meth:`ETagResponseMixin.make_conditional`, after
+ setting ``last_modified`` and ``etag`` correctly on the response object
+
+ :param req: OpenERP request
+ :type req: web.common.http.WebRequest
+ :param response: Werkzeug response
+ :type response: werkzeug.wrappers.Response
+ :param datetime.datetime last_modified: last modification date of the response content
+ :param str etag: some sort of checksum of the content (deep etag)
+ :return: the response object provided
+ :rtype: werkzeug.wrappers.Response
+ """
+ response.cache_control.must_revalidate = True
+ response.cache_control.max_age = 0
+ if last_modified:
+ response.last_modified = last_modified
+ if etag:
+ response.set_etag(etag)
+ return response.make_conditional(req.httprequest)
+
+def login_and_redirect(req, db, login, key, redirect_url='/'):
+ wsgienv = req.httprequest.environ
+ env = dict(
+ base_location=req.httprequest.url_root.rstrip('/'),
+ HTTP_HOST=wsgienv['HTTP_HOST'],
+ REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
+ )
+ req.session.authenticate(db, login, key, env)
+ return set_cookie_and_redirect(req, redirect_url)
+
+def set_cookie_and_redirect(req, redirect_url):
+ redirect = werkzeug.utils.redirect(redirect_url, 303)
+ redirect.autocorrect_location_header = False
+ cookie_val = urllib2.quote(simplejson.dumps(req.session_id))
+ redirect.set_cookie('instance0|session_id', cookie_val)
+ return redirect
+
+def load_actions_from_ir_values(req, key, key2, models, meta):
+ Values = req.session.model('ir.values')
+ actions = Values.get(key, key2, models, meta, req.context)
+
+ return [(id, name, clean_action(req, action))
+ for id, name, action in actions]
+
+def clean_action(req, action):
+ action.setdefault('flags', {})
+ action_type = action.setdefault('type', 'ir.actions.act_window_close')
+ if action_type == 'ir.actions.act_window':
+ return fix_view_modes(action)
+ return action
+
+# I think generate_views,fix_view_modes should go into js ActionManager
+def generate_views(action):
+ """
+ While the server generates a sequence called "views" computing dependencies
+ between a bunch of stuff for views coming directly from the database
+ (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
+ to return custom view dictionaries generated on the fly.
+
+ In that case, there is no ``views`` key available on the action.
+
+ Since the web client relies on ``action['views']``, generate it here from
+ ``view_mode`` and ``view_id``.
+
+ Currently handles two different cases:
+
+ * no view_id, multiple view_mode
+ * single view_id, single view_mode
+
+ :param dict action: action descriptor dictionary to generate a views key for
+ """
+ view_id = action.get('view_id') or False
+ if isinstance(view_id, (list, tuple)):
+ view_id = view_id[0]
+
+ # providing at least one view mode is a requirement, not an option
+ view_modes = action['view_mode'].split(',')
+
+ if len(view_modes) > 1:
+ if view_id:
+ raise ValueError('Non-db action dictionaries should provide '
+ 'either multiple view modes or a single view '
+ 'mode and an optional view id.\n\n Got view '
+ 'modes %r and view id %r for action %r' % (
+ view_modes, view_id, action))
+ action['views'] = [(False, mode) for mode in view_modes]
+ return
+ action['views'] = [(view_id, view_modes[0])]
+
+def fix_view_modes(action):
+ """ For historical reasons, OpenERP has weird dealings in relation to
+ view_mode and the view_type attribute (on window actions):
+
+ * one of the view modes is ``tree``, which stands for both list views
+ and tree views
+ * the choice is made by checking ``view_type``, which is either
+ ``form`` for a list view or ``tree`` for an actual tree view
+
+ This methods simply folds the view_type into view_mode by adding a
+ new view mode ``list`` which is the result of the ``tree`` view_mode
+ in conjunction with the ``form`` view_type.
+
+ TODO: this should go into the doc, some kind of "peculiarities" section
+
+ :param dict action: an action descriptor
+ :returns: nothing, the action is modified in place
+ """
+ if not action.get('views'):
+ generate_views(action)
+
+ if action.pop('view_type', 'form') != 'form':
+ return action
+
+ if 'view_mode' in action:
+ action['view_mode'] = ','.join(
+ mode if mode != 'tree' else 'list'
+ for mode in action['view_mode'].split(','))
+ action['views'] = [
+ [id, mode if mode != 'tree' else 'list']
+ for id, mode in action['views']
+ ]
+
+ return action
+
+def _local_web_translations(trans_file):
+ messages = []
+ try:
+ with open(trans_file) as t_file:
+ po = babel.messages.pofile.read_po(t_file)
+ except Exception:
+ return
+ for x in po:
+ if x.id and x.string and "openerp-web" in x.auto_comments:
+ messages.append({'id': x.id, 'string': x.string})
+ return messages
+
+def xml2json_from_elementtree(el, preserve_whitespaces=False):
+ """ xml2json-direct
+ Simple and straightforward XML-to-JSON converter in Python
+ New BSD Licensed
+ http://code.google.com/p/xml2json-direct/
+ """
+ res = {}
+ if el.tag[0] == "{":
+ ns, name = el.tag.rsplit("}", 1)
+ res["tag"] = name
+ res["namespace"] = ns[1:]
+ else:
+ res["tag"] = el.tag
+ res["attrs"] = {}
+ for k, v in el.items():
+ res["attrs"][k] = v
+ kids = []
+ if el.text and (preserve_whitespaces or el.text.strip() != ''):
+ kids.append(el.text)
+ for kid in el:
+ kids.append(xml2json_from_elementtree(kid, preserve_whitespaces))
+ if kid.tail and (preserve_whitespaces or kid.tail.strip() != ''):
+ kids.append(kid.tail)
+ res["children"] = kids
+ return res
+
+def content_disposition(filename, req):
+ filename = filename.encode('utf8')
+ escaped = urllib2.quote(filename)
+ browser = req.httprequest.user_agent.browser
+ version = int((req.httprequest.user_agent.version or '0').split('.')[0])
+ if browser == 'msie' and version < 9:
+ return "attachment; filename=%s" % escaped
+ elif browser == 'safari':
+ return "attachment; filename=%s" % filename
+ else:
+ return "attachment; filename*=UTF-8''%s" % escaped
+
+
+#----------------------------------------------------------
+# OpenERP Web web Controllers
+#----------------------------------------------------------
+
html_template = """<!DOCTYPE html>
<html style="height: 100%%">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>OpenERP</title>
<link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
+ <link rel="stylesheet" href="/web/static/src/css/full.css" />
%(css)s
%(js)s
<script type="text/javascript">
});
</script>
</head>
- <body class="openerp" id="oe"></body>
+ <body>
+ <!--[if lte IE 8]>
+ <script src="http://ajax.googleapis.com/ajax/libs/chrome-frame/1/CFInstall.min.js"></script>
+ <script>CFInstall.check({mode: "overlay"});</script>
+ <![endif]-->
+
+ <!--[if lte IE 9]>
+ <script src="/web/static/lib/jquery.placeholder/jquery.placeholder.min.js"></script>
+ <script>
+ document.addEventListener("DOMNodeInserted",function(event){
+ if ( $(event.target).is("input") || $(event.target).is("textarea") ) {
+ $(event.target).placeholder();
+ }
+ });
+ </script>
+ <![endif]-->
+ </body>
</html>
"""
-class WebClient(openerpweb.Controller):
- _cp_path = "/web/webclient"
+class Home(openerpweb.Controller):
+ _cp_path = '/'
- def server_wide_modules(self, req):
- addons = [i for i in req.config.server_wide_modules if i in openerpweb.addons_manifest]
- return addons
+ @openerpweb.httprequest
+ def index(self, req, s_action=None, db=None, **kw):
+ js = "\n ".join('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list(req, 'js', db=db))
+ css = "\n ".join('<link rel="stylesheet" href="%s">' % i for i in manifest_list(req, 'css', db=db))
- def manifest_glob(self, req, addons, key):
- if addons is None:
- addons = self.server_wide_modules(req)
- else:
- addons = addons.split(',')
- r = []
- for addon in addons:
- manifest = openerpweb.addons_manifest.get(addon, None)
- if not manifest:
- continue
- # ensure does not ends with /
- addons_path = os.path.join(manifest['addons_path'], '')[:-1]
- globlist = manifest.get(key, [])
- for pattern in globlist:
- for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
- r.append( (path, path[len(addons_path):]))
+ r = html_template % {
+ 'js': js,
+ 'css': css,
+ 'modules': simplejson.dumps(module_boot(req, db=db)),
+ 'init': 'var wc = new s.web.WebClient();wc.appendTo($(document.body));'
+ }
return r
- def manifest_list(self, req, mods, extension):
- if not req.debug:
- path = '/web/webclient/' + extension
- if mods is not None:
- path += '?mods=' + mods
- return [path]
- # re-normalize fs paths to URLs: split on fs path separator
- # ('/' or '\\' usually) and join on url path separator ('/')
- return ['%s?debug=%s' % ('/'.join(wp.split(os.path.sep)), os.path.getmtime(fp))
- for fp, wp in self.manifest_glob(req, mods, extension)]
+ @openerpweb.httprequest
+ def login(self, req, db, login, key):
+ return login_and_redirect(req, db, login, key)
+
+class WebClient(openerpweb.Controller):
+ _cp_path = "/web/webclient"
@openerpweb.jsonrequest
def csslist(self, req, mods=None):
- return self.manifest_list(req, mods, 'css')
+ return manifest_list(req, 'css', mods=mods)
@openerpweb.jsonrequest
def jslist(self, req, mods=None):
- return self.manifest_list(req, mods, 'js')
+ return manifest_list(req, 'js', mods=mods)
@openerpweb.jsonrequest
def qweblist(self, req, mods=None):
- return self.manifest_list(req, mods, 'qweb')
-
- def get_last_modified(self, files):
- """ Returns the modification time of the most recently modified
- file provided
-
- :param list(str) files: names of files to check
- :return: most recent modification time amongst the fileset
- :rtype: datetime.datetime
- """
- files = list(files)
- if files:
- return max(datetime.datetime.fromtimestamp(os.path.getmtime(f))
- for f in files)
- return datetime.datetime(1970, 1, 1)
-
- def make_conditional(self, req, response, last_modified=None, etag=None):
- """ Makes the provided response conditional based upon the request,
- and mandates revalidation from clients
-
- Uses Werkzeug's own :meth:`ETagResponseMixin.make_conditional`, after
- setting ``last_modified`` and ``etag`` correctly on the response object
-
- :param req: OpenERP request
- :type req: web.common.http.WebRequest
- :param response: Werkzeug response
- :type response: werkzeug.wrappers.Response
- :param datetime.datetime last_modified: last modification date of the response content
- :param str etag: some sort of checksum of the content (deep etag)
- :return: the response object provided
- :rtype: werkzeug.wrappers.Response
- """
- response.cache_control.must_revalidate = True
- response.cache_control.max_age = 0
- if last_modified:
- response.last_modified = last_modified
- if etag:
- response.set_etag(etag)
- return response.make_conditional(req.httprequest)
+ return manifest_list(req, 'qweb', mods=mods)
@openerpweb.httprequest
- def css(self, req, mods=None):
- files = list(self.manifest_glob(req, mods, 'css'))
- last_modified = self.get_last_modified(f[0] for f in files)
+ def css(self, req, mods=None, db=None):
+ files = list(manifest_glob(req, 'css', addons=mods, db=db))
+ last_modified = get_last_modified(f[0] for f in files)
if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
return werkzeug.wrappers.Response(status=304)
file_map = dict(files)
rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
- rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://)""", re.U)
-
+ rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
def reader(f):
"""read the a css file and absolutify all relative uris"""
- with open(f) as fp:
- data = fp.read()
+ with open(f, 'rb') as fp:
+ data = fp.read().decode('utf-8')
path = file_map[f]
- # convert FS path into web path
- web_dir = '/'.join(os.path.dirname(path).split(os.path.sep))
+ web_dir = os.path.dirname(path)
data = re.sub(
rx_import,
r"""url(\1%s/""" % (web_dir,),
data,
)
- return data
+ return data.encode('utf-8')
content, checksum = concat_files((f[0] for f in files), reader)
- return self.make_conditional(
+ return make_conditional(
req, req.make_response(content, [('Content-Type', 'text/css')]),
last_modified, checksum)
@openerpweb.httprequest
- def js(self, req, mods=None):
- files = [f[0] for f in self.manifest_glob(req, mods, 'js')]
- last_modified = self.get_last_modified(files)
+ def js(self, req, mods=None, db=None):
+ files = [f[0] for f in manifest_glob(req, 'js', addons=mods, db=db)]
+ last_modified = get_last_modified(files)
if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
return werkzeug.wrappers.Response(status=304)
- content, checksum = concat_files(files, intersperse=';')
+ content, checksum = concat_js(files)
- return self.make_conditional(
+ return make_conditional(
req, req.make_response(content, [('Content-Type', 'application/javascript')]),
last_modified, checksum)
@openerpweb.httprequest
- def qweb(self, req, mods=None):
- files = [f[0] for f in self.manifest_glob(req, mods, 'qweb')]
- last_modified = self.get_last_modified(files)
+ def qweb(self, req, mods=None, db=None):
+ files = [f[0] for f in manifest_glob(req, 'qweb', addons=mods, db=db)]
+ last_modified = get_last_modified(files)
if req.httprequest.if_modified_since and req.httprequest.if_modified_since >= last_modified:
return werkzeug.wrappers.Response(status=304)
- content,checksum = concat_xml(files)
+ content, checksum = concat_xml(files)
- return self.make_conditional(
+ return make_conditional(
req, req.make_response(content, [('Content-Type', 'text/xml')]),
last_modified, checksum)
- @openerpweb.httprequest
- def home(self, req, s_action=None, **kw):
- js = "\n ".join('<script type="text/javascript" src="%s"></script>'%i for i in self.manifest_list(req, None, 'js'))
- css = "\n ".join('<link rel="stylesheet" href="%s">'%i for i in self.manifest_list(req, None, 'css'))
-
- r = html_template % {
- 'js': js,
- 'css': css,
- 'modules': simplejson.dumps(self.server_wide_modules(req)),
- 'init': 'new s.web.WebClient().start();',
- }
- return r
+ @openerpweb.jsonrequest
+ def bootstrap_translations(self, req, mods):
+ """ Load local translations from *.po files, as a temporary solution
+ until we have established a valid session. This is meant only
+ for translating the login page and db management chrome, using
+ the browser's language. """
+ # For performance reasons we only load a single translation, so for
+ # sub-languages (that should only be partially translated) we load the
+ # main language PO instead - that should be enough for the login screen.
+ lang = req.lang.split('_')[0]
+
+ translations_per_module = {}
+ for addon_name in mods:
+ if openerpweb.addons_manifest[addon_name].get('bootstrap'):
+ addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
+ f_name = os.path.join(addons_path, addon_name, "i18n", lang + ".po")
+ if not os.path.exists(f_name):
+ continue
+ translations_per_module[addon_name] = {'messages': _local_web_translations(f_name)}
- @openerpweb.httprequest
- def login(self, req, db, login, key):
- req.session.authenticate(db, login, key, {})
- redirect = werkzeug.utils.redirect('/web/webclient/home', 303)
- cookie_val = urllib2.quote(simplejson.dumps(req.session_id))
- redirect.set_cookie('session0|session_id', cookie_val)
- return redirect
+ return {"modules": translations_per_module,
+ "lang_parameters": None}
@openerpweb.jsonrequest
def translations(self, req, mods, lang):
- lang_model = req.session.model('res.lang')
- ids = lang_model.search([("code", "=", lang)])
+ res_lang = req.session.model('res.lang')
+ ids = res_lang.search([("code", "=", lang)])
+ lang_params = None
if ids:
- lang_obj = lang_model.read(ids[0], ["direction", "date_format", "time_format",
+ lang_params = res_lang.read(ids[0], ["direction", "date_format", "time_format",
"grouping", "decimal_point", "thousands_sep"])
- else:
- lang_obj = None
-
- if "_" in lang:
- separator = "_"
- else:
- separator = "@"
- langs = lang.split(separator)
- langs = [separator.join(langs[:x]) for x in range(1, len(langs) + 1)]
- transs = {}
- for addon_name in mods:
- transl = {"messages":[]}
- transs[addon_name] = transl
- addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
- for l in langs:
- f_name = os.path.join(addons_path, addon_name, "i18n", l + ".po")
- if not os.path.exists(f_name):
- continue
- try:
- with open(f_name) as t_file:
- po = babel.messages.pofile.read_po(t_file)
- except Exception:
- continue
- for x in po:
- if x.id and x.string and "openerp-web" in x.auto_comments:
- transl["messages"].append({'id': x.id, 'string': x.string})
- return {"modules": transs,
- "lang_parameters": lang_obj}
+ # Regional languages (ll_CC) must inherit/override their parent lang (ll), but this is
+ # done server-side when the language is loaded, so we only need to load the user's lang.
+ ir_translation = req.session.model('ir.translation')
+ translations_per_module = {}
+ messages = ir_translation.search_read([('module','in',mods),('lang','=',lang),
+ ('comments','like','openerp-web'),('value','!=',False),
+ ('value','!=','')],
+ ['module','src','value','lang'], order='module')
+ for mod, msg_group in itertools.groupby(messages, key=operator.itemgetter('module')):
+ translations_per_module.setdefault(mod,{'messages':[]})
+ translations_per_module[mod]['messages'].extend({'id': m['src'],
+ 'string': m['value']} \
+ for m in msg_group)
+ return {"modules": translations_per_module,
+ "lang_parameters": lang_params}
@openerpweb.jsonrequest
def version_info(self, req):
- return {
- "version": common.release.version
- }
+ return openerp.service.web_services.RPC_VERSION_1
class Proxy(openerpweb.Controller):
_cp_path = '/web/proxy'
@openerpweb.jsonrequest
def get_list(self, req):
- proxy = req.session.proxy("db")
- dbs = proxy.list()
- h = req.httprequest.environ['HTTP_HOST'].split(':')[0]
- d = h.split('.')[0]
- r = req.config.dbfilter.replace('%h', h).replace('%d', d)
- dbs = [i for i in dbs if re.match(r, i)]
- return {"db_list": dbs}
+ return db_list(req)
@openerpweb.jsonrequest
def create(self, req, fields):
params = dict(map(operator.itemgetter('name', 'value'), fields))
- create_attrs = (
+ return req.session.proxy("db").create_database(
params['super_admin_pwd'],
params['db_name'],
bool(params.get('demo_data')),
params['db_lang'],
- params['create_admin_pwd']
+ params['create_admin_pwd'])
+
+ @openerpweb.jsonrequest
+ def duplicate(self, req, fields):
+ params = dict(map(operator.itemgetter('name', 'value'), fields))
+ return req.session.proxy("db").duplicate_database(
+ params['super_admin_pwd'],
+ params['db_original_name'],
+ params['db_name'])
+
+ @openerpweb.jsonrequest
+ def duplicate(self, req, fields):
+ params = dict(map(operator.itemgetter('name', 'value'), fields))
+ duplicate_attrs = (
+ params['super_admin_pwd'],
+ params['db_original_name'],
+ params['db_name'],
)
- return req.session.proxy("db").create_database(*create_attrs)
+ return req.session.proxy("db").duplicate_database(*duplicate_attrs)
@openerpweb.jsonrequest
def drop(self, req, fields):
except xmlrpclib.Fault, e:
if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
return {'error': e.faultCode, 'title': 'Drop Database'}
- return {'error': 'Could not drop database !', 'title': 'Drop Database'}
+ return {'error': _('Could not drop database !'), 'title': _('Drop Database')}
@openerpweb.httprequest
def backup(self, req, backup_db, backup_pwd, token):
- db_dump = base64.b64decode(
- req.session.proxy("db").dump(backup_pwd, backup_db))
- filename = "%(db)s_%(timestamp)s.dump" % {
- 'db': backup_db,
- 'timestamp': datetime.datetime.utcnow().strftime(
- "%Y-%m-%d_%H-%M-%SZ")
- }
- return req.make_response(db_dump,
- [('Content-Type', 'application/octet-stream; charset=binary'),
- ('Content-Disposition', 'attachment; filename="' + filename + '"')],
- {'fileToken': int(token)}
- )
+ try:
+ db_dump = base64.b64decode(
+ req.session.proxy("db").dump(backup_pwd, backup_db))
+ filename = "%(db)s_%(timestamp)s.dump" % {
+ 'db': backup_db,
+ 'timestamp': datetime.datetime.utcnow().strftime(
+ "%Y-%m-%d_%H-%M-%SZ")
+ }
+ return req.make_response(db_dump,
+ [('Content-Type', 'application/octet-stream; charset=binary'),
+ ('Content-Disposition', content_disposition(filename, req))],
+ {'fileToken': int(token)}
+ )
+ except xmlrpclib.Fault, e:
+ return simplejson.dumps([[],[{'error': e.faultCode, 'title': _('Backup Database')}]])
@openerpweb.httprequest
def restore(self, req, db_file, restore_pwd, new_db):
return req.session.proxy("db").change_admin_password(old_password, new_password)
except xmlrpclib.Fault, e:
if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
- return {'error': e.faultCode, 'title': 'Change Password'}
- return {'error': 'Error, password not changed !', 'title': 'Change Password'}
-
-def topological_sort(modules):
- """ Return a list of module names sorted so that their dependencies of the
- modules are listed before the module itself
-
- modules is a dict of {module_name: dependencies}
-
- :param modules: modules to sort
- :type modules: dict
- :returns: list(str)
- """
-
- dependencies = set(itertools.chain.from_iterable(modules.itervalues()))
- # incoming edge: dependency on other module (if a depends on b, a has an
- # incoming edge from b, aka there's an edge from b to a)
- # outgoing edge: other module depending on this one
-
- # [Tarjan 1976], http://en.wikipedia.org/wiki/Topological_sorting#Algorithms
- #L ← Empty list that will contain the sorted nodes
- L = []
- #S ← Set of all nodes with no outgoing edges (modules on which no other
- # module depends)
- S = set(module for module in modules if module not in dependencies)
-
- visited = set()
- #function visit(node n)
- def visit(n):
- #if n has not been visited yet then
- if n not in visited:
- #mark n as visited
- visited.add(n)
- #change: n not web module, can not be resolved, ignore
- if n not in modules: return
- #for each node m with an edge from m to n do (dependencies of n)
- for m in modules[n]:
- #visit(m)
- visit(m)
- #add n to L
- L.append(n)
- #for each node n in S do
- for n in S:
- #visit(n)
- visit(n)
- return L
+ return {'error': e.faultCode, 'title': _('Change Password')}
+ return {'error': _('Error, password not changed !'), 'title': _('Change Password')}
class Session(openerpweb.Controller):
_cp_path = "/web/session"
return {
"session_id": req.session_id,
"uid": req.session._uid,
- "context": req.session.get_context() if req.session._uid else {},
+ "user_context": req.session.get_context() if req.session._uid else {},
"db": req.session._db,
- "login": req.session._login,
- "openerp_entreprise": req.session.openerp_entreprise(),
+ "username": req.session._login,
}
@openerpweb.jsonrequest
@openerpweb.jsonrequest
def authenticate(self, req, db, login, password, base_location=None):
wsgienv = req.httprequest.environ
- release = common.release
env = dict(
base_location=base_location,
HTTP_HOST=wsgienv['HTTP_HOST'],
REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
- user_agent="%s / %s" % (release.name, release.version),
)
req.session.authenticate(db, login, password, env)
old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
dict(map(operator.itemgetter('name', 'value'), fields)))
if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
- return {'error':'All passwords have to be filled.','title': 'Change Password'}
+ return {'error':_('You cannot leave any password empty.'),'title': _('Change Password')}
if new_password != confirm_password:
- return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
+ return {'error': _('The new password and its confirmation must be identical.'),'title': _('Change Password')}
try:
if req.session.model('res.users').change_password(
old_password, new_password):
return {'new_password':new_password}
except Exception:
- return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
- return {'error': 'Error, password not changed !', 'title': 'Change Password'}
+ return {'error': _('The old password you provided is incorrect, your password was not changed.'), 'title': _('Change Password')}
+ return {'error': _('Error, password not changed !'), 'title': _('Change Password')}
@openerpweb.jsonrequest
def sc_list(self, req):
return req.session.model('ir.ui.view_sc').get_sc(
- req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
+ req.session._uid, "ir.ui.menu", req.context)
@openerpweb.jsonrequest
def get_lang_list(self, req):
try:
- return {
- 'lang_list': (req.session.proxy("db").list_lang() or []),
- 'error': ""
- }
+ return req.session.proxy("db").list_lang() or []
except Exception, e:
- return {"error": e, "title": "Languages"}
+ return {"error": e, "title": _("Languages")}
@openerpweb.jsonrequest
def modules(self, req):
- # Compute available candidates module
- loadable = openerpweb.addons_manifest
- loaded = set(req.config.server_wide_modules)
- candidates = [mod for mod in loadable if mod not in loaded]
-
- # already installed modules have no dependencies
- modules = dict.fromkeys(loaded, [])
-
- # Compute auto_install modules that might be on the web side only
- modules.update((name, openerpweb.addons_manifest[name].get('depends', []))
- for name in candidates
- if openerpweb.addons_manifest[name].get('auto_install'))
-
- # Retrieve database installed modules
- Modules = req.session.model('ir.module.module')
- for module in Modules.search_read(
- [('state','=','installed'), ('name','in', candidates)],
- ['name', 'dependencies_id']):
- deps = module.get('dependencies_id')
- if deps:
- dependencies = map(
- operator.itemgetter('name'),
- req.session.model('ir.module.module.dependency').read(deps, ['name']))
- modules[module['name']] = list(
- set(modules.get(module['name'], []) + dependencies))
-
- sorted_modules = topological_sort(modules)
- return [module for module in sorted_modules if module not in loaded]
-
- @openerpweb.jsonrequest
- def eval_domain_and_context(self, req, contexts, domains,
- group_by_seq=None):
- """ Evaluates sequences of domains and contexts, composing them into
- a single context, domain or group_by sequence.
-
- :param list contexts: list of contexts to merge together. Contexts are
- evaluated in sequence, all previous contexts
- are part of their own evaluation context
- (starting at the session context).
- :param list domains: list of domains to merge together. Domains are
- evaluated in sequence and appended to one another
- (implicit AND), their evaluation domain is the
- result of merging all contexts.
- :param list group_by_seq: list of domains (which may be in a different
- order than the ``contexts`` parameter),
- evaluated in sequence, their ``'group_by'``
- key is extracted if they have one.
- :returns:
- a 3-dict of:
-
- context (``dict``)
- the global context created by merging all of
- ``contexts``
-
- domain (``list``)
- the concatenation of all domains
-
- group_by (``list``)
- a list of fields to group by, potentially empty (in which case
- no group by should be performed)
- """
- context, domain = eval_context_and_domain(req.session,
- common.nonliterals.CompoundContext(*(contexts or [])),
- common.nonliterals.CompoundDomain(*(domains or [])))
-
- group_by_sequence = []
- for candidate in (group_by_seq or []):
- ctx = req.session.eval_context(candidate, context)
- group_by = ctx.get('group_by')
- if not group_by:
- continue
- elif isinstance(group_by, basestring):
- group_by_sequence.append(group_by)
- else:
- group_by_sequence.extend(group_by)
-
- return {
- 'context': context,
- 'domain': domain,
- 'group_by': group_by_sequence
- }
+ # return all installed modules. Web client is smart enough to not load a module twice
+ return module_installed(req)
@openerpweb.jsonrequest
def save_session_action(self, req, the_action):
def destroy(self, req):
req.session._suicide = True
-def eval_context_and_domain(session, context, domain=None):
- e_context = session.eval_context(context)
- # should we give the evaluated context as an evaluation context to the domain?
- e_domain = session.eval_domain(domain or [])
-
- return e_context, e_domain
-
-def load_actions_from_ir_values(req, key, key2, models, meta):
- context = req.session.eval_context(req.context)
- Values = req.session.model('ir.values')
- actions = Values.get(key, key2, models, meta, context)
-
- return [(id, name, clean_action(req, action))
- for id, name, action in actions]
-
-def clean_action(req, action, do_not_eval=False):
- if action is False:
- action = {}
- action.setdefault('flags', {})
-
- context = req.session.eval_context(req.context)
- eval_ctx = req.session.evaluation_context(context)
-
- if not do_not_eval:
- # values come from the server, we can just eval them
- if action.get('context') and isinstance(action.get('context'), basestring):
- action['context'] = eval( action['context'], eval_ctx ) or {}
-
- if action.get('domain') and isinstance(action.get('domain'), basestring):
- action['domain'] = eval( action['domain'], eval_ctx ) or []
- else:
- if 'context' in action:
- action['context'] = parse_context(action['context'], req.session)
- if 'domain' in action:
- action['domain'] = parse_domain(action['domain'], req.session)
-
- action_type = action.setdefault('type', 'ir.actions.act_window_close')
- if action_type == 'ir.actions.act_window':
- return fix_view_modes(action)
- return action
-
-# I think generate_views,fix_view_modes should go into js ActionManager
-def generate_views(action):
- """
- While the server generates a sequence called "views" computing dependencies
- between a bunch of stuff for views coming directly from the database
- (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
- to return custom view dictionaries generated on the fly.
-
- In that case, there is no ``views`` key available on the action.
-
- Since the web client relies on ``action['views']``, generate it here from
- ``view_mode`` and ``view_id``.
-
- Currently handles two different cases:
-
- * no view_id, multiple view_mode
- * single view_id, single view_mode
-
- :param dict action: action descriptor dictionary to generate a views key for
- """
- view_id = action.get('view_id') or False
- if isinstance(view_id, (list, tuple)):
- view_id = view_id[0]
-
- # providing at least one view mode is a requirement, not an option
- view_modes = action['view_mode'].split(',')
-
- if len(view_modes) > 1:
- if view_id:
- raise ValueError('Non-db action dictionaries should provide '
- 'either multiple view modes or a single view '
- 'mode and an optional view id.\n\n Got view '
- 'modes %r and view id %r for action %r' % (
- view_modes, view_id, action))
- action['views'] = [(False, mode) for mode in view_modes]
- return
- action['views'] = [(view_id, view_modes[0])]
-
-def fix_view_modes(action):
- """ For historical reasons, OpenERP has weird dealings in relation to
- view_mode and the view_type attribute (on window actions):
-
- * one of the view modes is ``tree``, which stands for both list views
- and tree views
- * the choice is made by checking ``view_type``, which is either
- ``form`` for a list view or ``tree`` for an actual tree view
-
- This methods simply folds the view_type into view_mode by adding a
- new view mode ``list`` which is the result of the ``tree`` view_mode
- in conjunction with the ``form`` view_type.
-
- This method also adds a ``page`` view mode in case there is a ``form`` in
- the input action.
-
- TODO: this should go into the doc, some kind of "peculiarities" section
-
- :param dict action: an action descriptor
- :returns: nothing, the action is modified in place
- """
- if not action.get('views'):
- generate_views(action)
-
- id_form = None
- for index, (id, mode) in enumerate(action['views']):
- if mode == 'form':
- id_form = id
- break
- if id_form is not None:
- action['views'].insert(index + 1, (id_form, 'page'))
-
- if action.pop('view_type', 'form') != 'form':
- return action
-
- action['views'] = [
- [id, mode if mode != 'tree' else 'list']
- for id, mode in action['views']
- ]
-
- return action
-
class Menu(openerpweb.Controller):
_cp_path = "/web/menu"
@openerpweb.jsonrequest
- def load(self, req):
- return {'data': self.do_load(req)}
-
- @openerpweb.jsonrequest
def get_user_roots(self, req):
- return self.do_get_user_roots(req)
-
- def do_get_user_roots(self, req):
""" Return all root menu ids visible for the session user.
:param req: A request object, with an OpenERP session attribute
:rtype: list(int)
"""
s = req.session
- context = s.eval_context(req.context)
Menus = s.model('ir.ui.menu')
# If a menu action is defined use its domain to get the root menu items
- user_menu_id = s.model('res.users').read([s._uid], ['menu_id'], context)[0]['menu_id']
+ user_menu_id = s.model('res.users').read([s._uid], ['menu_id'],
+ req.context)[0]['menu_id']
menu_domain = [('parent_id', '=', False)]
if user_menu_id:
- domain_string = s.model('ir.actions.act_window').read([user_menu_id[0]], ['domain'], context)[0]['domain']
+ domain_string = s.model('ir.actions.act_window').read(
+ [user_menu_id[0]], ['domain'],req.context)[0]['domain']
if domain_string:
menu_domain = ast.literal_eval(domain_string)
- return Menus.search(menu_domain, 0, False, False, context)
+ return Menus.search(menu_domain, 0, False, False, req.context)
- def do_load(self, req):
+ @openerpweb.jsonrequest
+ def load(self, req):
""" Loads all menu items (all applications and their sub-menus).
:param req: A request object, with an OpenERP session attribute
:return: the menu root
:rtype: dict('children': menu_nodes)
"""
- context = req.session.eval_context(req.context)
Menus = req.session.model('ir.ui.menu')
- menu_roots = Menus.read(self.do_get_user_roots(req), ['name', 'sequence', 'parent_id'], context)
- menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, ''], 'children' : menu_roots}
+ fields = ['name', 'sequence', 'parent_id', 'action']
+ menu_root_ids = self.get_user_roots(req)
+ menu_roots = Menus.read(menu_root_ids, fields, req.context) if menu_root_ids else []
+ menu_root = {
+ 'id': False,
+ 'name': 'root',
+ 'parent_id': [-1, ''],
+ 'children': menu_roots,
+ 'all_menu_ids': menu_root_ids,
+ }
+ if not menu_roots:
+ return menu_root
# menus are loaded fully unlike a regular tree view, cause there are a
# limited number of items (752 when all 6.1 addons are installed)
- menu_ids = Menus.search([], 0, False, False, context)
- menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
+ menu_ids = Menus.search([('id', 'child_of', menu_root_ids)], 0, False, False, req.context)
+ menu_items = Menus.read(menu_ids, fields, req.context)
# adds roots at the end of the sequence, so that they will overwrite
# equivalent menu items from full menu read when put into id:item
# mapping, resulting in children being correctly set on the roots.
menu_items.extend(menu_roots)
+ menu_root['all_menu_ids'] = menu_ids # includes menu_root_ids!
# make a tree using parent_id
- menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
+ menu_items_map = dict(
+ (menu_item["id"], menu_item) for menu_item in menu_items)
for menu_item in menu_items:
if menu_item['parent_id']:
parent = menu_item['parent_id'][0]
return menu_root
@openerpweb.jsonrequest
+ def load_needaction(self, req, menu_ids):
+ """ Loads needaction counters for specific menu ids.
+
+ :return: needaction data
+ :rtype: dict(menu_id: {'needaction_enabled': boolean, 'needaction_counter': int})
+ """
+ return req.session.model('ir.ui.menu').get_needaction_data(menu_ids, req.context)
+
+ @openerpweb.jsonrequest
def action(self, req, menu_id):
+ # still used by web_shortcut
actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
[('ir.ui.menu', menu_id)], False)
return {"action": actions}
_cp_path = "/web/dataset"
@openerpweb.jsonrequest
- def fields(self, req, model):
- return {'fields': req.session.model(model).fields_get(False,
- req.session.eval_context(req.context))}
-
- @openerpweb.jsonrequest
def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
return self.do_search_read(req, model, fields, offset, limit, domain, sort)
def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
"""
Model = req.session.model(model)
- context, domain = eval_context_and_domain(
- req.session, req.context, domain)
-
- ids = Model.search(domain, offset or 0, limit or False, sort or False, context)
+ ids = Model.search(domain, offset or 0, limit or False, sort or False,
+ req.context)
if limit and len(ids) == limit:
- length = Model.search_count(domain, context)
+ length = Model.search_count(domain, req.context)
else:
length = len(ids) + (offset or 0)
if fields and fields == ['id']:
# shortcut read if we only want the ids
return {
- 'ids': ids,
'length': length,
'records': [{'id': id} for id in ids]
}
- records = Model.read(ids, fields or False, context)
+ records = Model.read(ids, fields or False, req.context)
records.sort(key=lambda obj: ids.index(obj['id']))
return {
- 'ids': ids,
'length': length,
'records': records
}
-
- @openerpweb.jsonrequest
- def read(self, req, model, ids, fields=False):
- return self.do_search_read(req, model, ids, fields)
-
- @openerpweb.jsonrequest
- def get(self, req, model, ids, fields=False):
- return self.do_get(req, model, ids, fields)
-
- def do_get(self, req, model, ids, fields=False):
- """ Fetches and returns the records of the model ``model`` whose ids
- are in ``ids``.
-
- The results are in the same order as the inputs, but elements may be
- missing (if there is no record left for the id)
-
- :param req: the JSON-RPC2 request object
- :type req: openerpweb.JsonRequest
- :param model: the model to read from
- :type model: str
- :param ids: a list of identifiers
- :type ids: list
- :param fields: a list of fields to fetch, ``False`` or empty to fetch
- all fields in the model
- :type fields: list | False
- :returns: a list of records, in the same order as the list of ids
- :rtype: list
- """
- Model = req.session.model(model)
- records = Model.read(ids, fields, req.session.eval_context(req.context))
-
- record_map = dict((record['id'], record) for record in records)
-
- return [record_map[id] for id in ids if record_map.get(id)]
-
@openerpweb.jsonrequest
def load(self, req, model, id, fields):
m = req.session.model(model)
value = {}
- r = m.read([id], False, req.session.eval_context(req.context))
+ r = m.read([id], False, req.context)
if r:
value = r[0]
return {'value': value}
- @openerpweb.jsonrequest
- def create(self, req, model, data):
- m = req.session.model(model)
- r = m.create(data, req.session.eval_context(req.context))
- return {'result': r}
-
- @openerpweb.jsonrequest
- def save(self, req, model, id, data):
- m = req.session.model(model)
- r = m.write([id], data, req.session.eval_context(req.context))
- return {'result': r}
-
- @openerpweb.jsonrequest
- def unlink(self, req, model, ids=()):
- Model = req.session.model(model)
- return Model.unlink(ids, req.session.eval_context(req.context))
-
def call_common(self, req, model, method, args, domain_id=None, context_id=None):
- has_domain = domain_id is not None and domain_id < len(args)
- has_context = context_id is not None and context_id < len(args)
-
- domain = args[domain_id] if has_domain else []
- context = args[context_id] if has_context else {}
- c, d = eval_context_and_domain(req.session, context, domain)
- if has_domain:
- args[domain_id] = d
- if has_context:
- args[context_id] = c
-
return self._call_kw(req, model, method, args, {})
-
+
def _call_kw(self, req, model, method, args, kwargs):
- for i in xrange(len(args)):
- if isinstance(args[i], common.nonliterals.BaseContext):
- args[i] = req.session.eval_context(args[i])
- elif isinstance(args[i], common.nonliterals.BaseDomain):
- args[i] = req.session.eval_domain(args[i])
- for k in kwargs.keys():
- if isinstance(kwargs[k], common.nonliterals.BaseContext):
- kwargs[k] = req.session.eval_context(kwargs[k])
- elif isinstance(kwargs[k], common.nonliterals.BaseDomain):
- kwargs[k] = req.session.eval_domain(kwargs[k])
+ # Temporary implements future display_name special field for model#read()
+ if method == 'read' and kwargs.get('context', {}).get('future_display_name'):
+ if 'display_name' in args[1]:
+ names = dict(req.session.model(model).name_get(args[0], **kwargs))
+ args[1].remove('display_name')
+ records = req.session.model(model).read(*args, **kwargs)
+ for record in records:
+ record['display_name'] = \
+ names.get(record['id']) or "%s#%d" % (model, (record['id']))
+ return records
return getattr(req.session.model(model), method)(*args, **kwargs)
@openerpweb.jsonrequest
- def onchange(self, req, model, method, args, context_id=None):
- """ Support method for handling onchange calls: behaves much like call
- with the following differences:
-
- * Does not take a domain_id
- * Is aware of the return value's structure, and will parse the domains
- if needed in order to return either parsed literal domains (in JSON)
- or non-literal domain instances, allowing those domains to be used
- from JS
-
- :param req:
- :type req: web.common.http.JsonRequest
- :param str model: object type on which to call the method
- :param str method: name of the onchange handler method
- :param list args: arguments to call the onchange handler with
- :param int context_id: index of the context object in the list of
- arguments
- :return: result of the onchange call with all domains parsed
- """
- result = self.call_common(req, model, method, args, context_id=context_id)
- if not result or 'domain' not in result:
- return result
-
- result['domain'] = dict(
- (k, parse_domain(v, req.session))
- for k, v in result['domain'].iteritems())
-
- return result
-
- @openerpweb.jsonrequest
def call(self, req, model, method, args, domain_id=None, context_id=None):
- return self.call_common(req, model, method, args, domain_id, context_id)
-
+ return self._call_kw(req, model, method, args, {})
+
@openerpweb.jsonrequest
def call_kw(self, req, model, method, args, kwargs):
return self._call_kw(req, model, method, args, kwargs)
@openerpweb.jsonrequest
def call_button(self, req, model, method, args, domain_id=None, context_id=None):
- action = self.call_common(req, model, method, args, domain_id, context_id)
+ action = self._call_kw(req, model, method, args, {})
if isinstance(action, dict) and action.get('type') != '':
- return {'result': clean_action(req, action)}
- return {'result': False}
+ return clean_action(req, action)
+ return False
@openerpweb.jsonrequest
def exec_workflow(self, req, model, id, signal):
- r = req.session.exec_workflow(model, id, signal)
- return {'result': r}
+ return req.session.exec_workflow(model, id, signal)
@openerpweb.jsonrequest
- def default_get(self, req, model, fields):
- Model = req.session.model(model)
- return Model.default_get(fields, req.session.eval_context(req.context))
-
- @openerpweb.jsonrequest
- def name_search(self, req, model, search_str, domain=[], context={}):
+ def resequence(self, req, model, ids, field='sequence', offset=0):
+ """ Re-sequences a number of records in the model, by their ids
+
+ The re-sequencing starts at the first model of ``ids``, the sequence
+ number is incremented by one after each record and starts at ``offset``
+
+ :param ids: identifiers of the records to resequence, in the new sequence order
+ :type ids: list(id)
+ :param str field: field used for sequence specification, defaults to
+ "sequence"
+ :param int offset: sequence number for first record in ``ids``, allows
+ starting the resequencing from an arbitrary number,
+ defaults to ``0``
+ """
m = req.session.model(model)
- r = m.name_search(search_str+'%', domain, '=ilike', context)
- return {'result': r}
-
-class DataGroup(openerpweb.Controller):
- _cp_path = "/web/group"
- @openerpweb.jsonrequest
- def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
- Model = req.session.model(model)
- context, domain = eval_context_and_domain(req.session, req.context, domain)
-
- return Model.read_group(
- domain or [], fields, group_by_fields, 0, False,
- dict(context, group_by=group_by_fields), sort or False)
+ if not m.fields_get([field]):
+ return False
+ # python 2.6 has no start parameter
+ for i, id in enumerate(ids):
+ m.write(id, { field: i + offset })
+ return True
class View(openerpweb.Controller):
_cp_path = "/web/view"
- def fields_view_get(self, req, model, view_id, view_type,
- transform=True, toolbar=False, submenu=False):
- Model = req.session.model(model)
- context = req.session.eval_context(req.context)
- fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
- # todo fme?: check that we should pass the evaluated context here
- self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
- if toolbar and transform:
- self.process_toolbar(req, fvg['toolbar'])
- return fvg
-
- def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
- # depending on how it feels, xmlrpclib.ServerProxy can translate
- # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
- # enjoy unicode strings which can not be trivially converted to
- # strings, and it blows up during parsing.
-
- # So ensure we fix this retardation by converting view xml back to
- # bit strings.
- if isinstance(fvg['arch'], unicode):
- arch = fvg['arch'].encode('utf-8')
- else:
- arch = fvg['arch']
-
- if transform:
- evaluation_context = session.evaluation_context(context or {})
- xml = self.transform_view(arch, session, evaluation_context)
- else:
- xml = ElementTree.fromstring(arch)
- fvg['arch'] = common.xml2json.from_elementtree(xml, preserve_whitespaces)
-
- if 'id' in fvg['fields']:
- # Special case for id's
- id_field = fvg['fields']['id']
- id_field['original_type'] = id_field['type']
- id_field['type'] = 'id'
-
- for field in fvg['fields'].itervalues():
- if field.get('views'):
- for view in field["views"].itervalues():
- self.process_view(session, view, None, transform)
- if field.get('domain'):
- field["domain"] = parse_domain(field["domain"], session)
- if field.get('context'):
- field["context"] = parse_context(field["context"], session)
-
- def process_toolbar(self, req, toolbar):
- """
- The toolbar is a mapping of section_key: [action_descriptor]
-
- We need to clean all those actions in order to ensure correct
- round-tripping
- """
- for actions in toolbar.itervalues():
- for action in actions:
- if 'context' in action:
- action['context'] = parse_context(
- action['context'], req.session)
- if 'domain' in action:
- action['domain'] = parse_domain(
- action['domain'], req.session)
-
@openerpweb.jsonrequest
def add_custom(self, req, view_id, arch):
CustomView = req.session.model('ir.ui.view.custom')
'user_id': req.session._uid,
'ref_id': view_id,
'arch': arch
- }, req.session.eval_context(req.context))
+ }, req.context)
return {'result': True}
@openerpweb.jsonrequest
def undo_custom(self, req, view_id, reset=False):
CustomView = req.session.model('ir.ui.view.custom')
- context = req.session.eval_context(req.context)
vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
- 0, False, False, context)
+ 0, False, False, req.context)
if vcustom:
if reset:
- CustomView.unlink(vcustom, context)
+ CustomView.unlink(vcustom, req.context)
else:
- CustomView.unlink([vcustom[0]], context)
+ CustomView.unlink([vcustom[0]], req.context)
return {'result': True}
return {'result': False}
- def transform_view(self, view_string, session, context=None):
- # transform nodes on the fly via iterparse, instead of
- # doing it statically on the parsing result
- parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
- root = None
- for event, elem in parser:
- if event == "start":
- if root is None:
- root = elem
- self.parse_domains_and_contexts(elem, session)
- return root
-
- def parse_domains_and_contexts(self, elem, session):
- """ Converts domains and contexts from the view into Python objects,
- either literals if they can be parsed by literal_eval or a special
- placeholder object if the domain or context refers to free variables.
-
- :param elem: the current node being parsed
- :type param: xml.etree.ElementTree.Element
- :param session: OpenERP session object, used to store and retrieve
- non-literal objects
- :type session: openerpweb.openerpweb.OpenERPSession
- """
- for el in ['domain', 'filter_domain']:
- domain = elem.get(el, '').strip()
- if domain:
- elem.set(el, parse_domain(domain, session))
- elem.set(el + '_string', domain)
- for el in ['context', 'default_get']:
- context_string = elem.get(el, '').strip()
- if context_string:
- elem.set(el, parse_context(context_string, session))
- elem.set(el + '_string', context_string)
-
- @openerpweb.jsonrequest
- def load(self, req, model, view_id, view_type, toolbar=False):
- return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
-
-def parse_domain(domain, session):
- """ Parses an arbitrary string containing a domain, transforms it
- to either a literal domain or a :class:`common.nonliterals.Domain`
-
- :param domain: the domain to parse, if the domain is not a string it
- is assumed to be a literal domain and is returned as-is
- :param session: Current OpenERP session
- :type session: openerpweb.openerpweb.OpenERPSession
- """
- if not isinstance(domain, basestring):
- return domain
- try:
- return ast.literal_eval(domain)
- except ValueError:
- # not a literal
- return common.nonliterals.Domain(session, domain)
-
-def parse_context(context, session):
- """ Parses an arbitrary string containing a context, transforms it
- to either a literal context or a :class:`common.nonliterals.Context`
-
- :param context: the context to parse, if the context is not a string it
- is assumed to be a literal domain and is returned as-is
- :param session: Current OpenERP session
- :type session: openerpweb.openerpweb.OpenERPSession
- """
- if not isinstance(context, basestring):
- return context
- try:
- return ast.literal_eval(context)
- except ValueError:
- return common.nonliterals.Context(session, context)
-
-class ListView(View):
- _cp_path = "/web/listview"
-
- def process_colors(self, view, row, context):
- colors = view['arch']['attrs'].get('colors')
-
- if not colors:
- return None
-
- color = [
- pair.split(':')[0]
- for pair in colors.split(';')
- if eval(pair.split(':')[1], dict(context, **row))
- ]
-
- if not color:
- return None
- elif len(color) == 1:
- return color[0]
- return 'maroon'
-
class TreeView(View):
_cp_path = "/web/treeview"
req,'action', 'tree_but_open',[(model, id)],
False)
-class SearchView(View):
- _cp_path = "/web/searchview"
-
- @openerpweb.jsonrequest
- def load(self, req, model, view_id):
- fields_view = self.fields_view_get(req, model, view_id, 'search')
- return {'fields_view': fields_view}
-
- @openerpweb.jsonrequest
- def fields_get(self, req, model):
- Model = req.session.model(model)
- fields = Model.fields_get(False, req.session.eval_context(req.context))
- for field in fields.values():
- # shouldn't convert the views too?
- if field.get('domain'):
- field["domain"] = parse_domain(field["domain"], req.session)
- if field.get('context'):
- field["context"] = parse_context(field["context"], req.session)
- return {'fields': fields}
-
- @openerpweb.jsonrequest
- def get_filters(self, req, model):
- logger = logging.getLogger(__name__ + '.SearchView.get_filters')
- Model = req.session.model("ir.filters")
- filters = Model.get_filters(model)
- for filter in filters:
- try:
- parsed_context = parse_context(filter["context"], req.session)
- filter["context"] = (parsed_context
- if not isinstance(parsed_context, common.nonliterals.BaseContext)
- else req.session.eval_context(parsed_context))
-
- parsed_domain = parse_domain(filter["domain"], req.session)
- filter["domain"] = (parsed_domain
- if not isinstance(parsed_domain, common.nonliterals.BaseDomain)
- else req.session.eval_domain(parsed_domain))
- except Exception:
- logger.exception("Failed to parse custom filter %s in %s",
- filter['name'], model)
- filter['disabled'] = True
- del filter['context']
- del filter['domain']
- return filters
-
- @openerpweb.jsonrequest
- def save_filter(self, req, model, name, context_to_save, domain):
- Model = req.session.model("ir.filters")
- ctx = common.nonliterals.CompoundContext(context_to_save)
- ctx.session = req.session
- ctx = ctx.evaluate()
- domain = common.nonliterals.CompoundDomain(domain)
- domain.session = req.session
- domain = domain.evaluate()
- uid = req.session._uid
- context = req.session.eval_context(req.context)
- to_return = Model.create_or_replace({"context": ctx,
- "domain": domain,
- "model_id": model,
- "name": name,
- "user_id": uid
- }, context)
- return to_return
-
- @openerpweb.jsonrequest
- def add_to_dashboard(self, req, menu_id, action_id, context_to_save, domain, view_mode, name=''):
- to_eval = common.nonliterals.CompoundContext(context_to_save)
- to_eval.session = req.session
- ctx = dict((k, v) for k, v in to_eval.evaluate().iteritems()
- if not k.startswith('search_default_')
- if k != 'lang')
- ctx['dashboard_merge_domains_contexts'] = False # TODO: replace this 6.1 workaround by attribute on <action/>
- domain = common.nonliterals.CompoundDomain(domain)
- domain.session = req.session
- domain = domain.evaluate()
-
- dashboard_action = load_actions_from_ir_values(req, 'action', 'tree_but_open',
- [('ir.ui.menu', menu_id)], False)
- if dashboard_action:
- action = dashboard_action[0][2]
- if action['res_model'] == 'board.board' and action['views'][0][1] == 'form':
- # Maybe should check the content instead of model board.board ?
- view_id = action['views'][0][0]
- board = req.session.model(action['res_model']).fields_view_get(view_id, 'form')
- if board and 'arch' in board:
- xml = ElementTree.fromstring(board['arch'])
- column = xml.find('./board/column')
- if column is not None:
- new_action = ElementTree.Element('action', {
- 'name' : str(action_id),
- 'string' : name,
- 'view_mode' : view_mode,
- 'context' : str(ctx),
- 'domain' : str(domain)
- })
- column.insert(0, new_action)
- arch = ElementTree.tostring(xml, 'utf-8')
- return req.session.model('ir.ui.view.custom').create({
- 'user_id': req.session._uid,
- 'ref_id': view_id,
- 'arch': arch
- }, req.session.eval_context(req.context))
-
- return False
-
class Binary(openerpweb.Controller):
_cp_path = "/web/binary"
@openerpweb.httprequest
def image(self, req, model, id, field, **kw):
+ last_update = '__last_update'
Model = req.session.model(model)
- context = req.session.eval_context(req.context)
+ headers = [('Content-Type', 'image/png')]
+ etag = req.httprequest.headers.get('If-None-Match')
+ hashed_session = hashlib.md5(req.session_id).hexdigest()
+ id = None if not id else simplejson.loads(id)
+ if type(id) is list:
+ id = id[0] # m2o
+ if etag:
+ if not id and hashed_session == etag:
+ return werkzeug.wrappers.Response(status=304)
+ else:
+ date = Model.read([id], [last_update], req.context)[0].get(last_update)
+ if hashlib.md5(date).hexdigest() == etag:
+ return werkzeug.wrappers.Response(status=304)
+ retag = hashed_session
try:
if not id:
- res = Model.default_get([field], context).get(field)
+ res = Model.default_get([field], req.context).get(field)
+ image_base64 = res
else:
- res = Model.read([int(id)], [field], context)[0].get(field)
- image_data = base64.b64decode(res)
+ res = Model.read([id], [last_update, field], req.context)[0]
+ retag = hashlib.md5(res.get(last_update)).hexdigest()
+ image_base64 = res.get(field)
+
+ if kw.get('resize'):
+ resize = kw.get('resize').split(',')
+ if len(resize) == 2 and int(resize[0]) and int(resize[1]):
+ width = int(resize[0])
+ height = int(resize[1])
+ # resize maximum 500*500
+ if width > 500: width = 500
+ if height > 500: height = 500
+ image_base64 = openerp.tools.image_resize_image(base64_source=image_base64, size=(width, height), encoding='base64', filetype='PNG')
+
+ image_data = base64.b64decode(image_base64)
+
except (TypeError, xmlrpclib.Fault):
image_data = self.placeholder(req)
- return req.make_response(image_data, [
- ('Content-Type', 'image/png'), ('Content-Length', len(image_data))])
- def placeholder(self, req):
+ headers.append(('ETag', retag))
+ headers.append(('Content-Length', len(image_data)))
+ try:
+ ncache = int(kw.get('cache'))
+ headers.append(('Cache-Control', 'no-cache' if ncache == 0 else 'max-age=%s' % (ncache)))
+ except:
+ pass
+ return req.make_response(image_data, headers)
+
+ def placeholder(self, req, image='placeholder.png'):
addons_path = openerpweb.addons_manifest['web']['addons_path']
- return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
- def content_disposition(self, filename, req):
- filename = filename.encode('utf8')
- escaped = urllib2.quote(filename)
- browser = req.httprequest.user_agent.browser
- version = int((req.httprequest.user_agent.version or '0').split('.')[0])
- if browser == 'msie' and version < 9:
- return "attachment; filename=%s" % escaped
- elif browser == 'safari':
- return "attachment; filename=%s" % filename
- else:
- return "attachment; filename*=UTF-8''%s" % escaped
+ return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', image), 'rb').read()
@openerpweb.httprequest
def saveas(self, req, model, field, id=None, filename_field=None, **kw):
:returns: :class:`werkzeug.wrappers.Response`
"""
Model = req.session.model(model)
- context = req.session.eval_context(req.context)
fields = [field]
if filename_field:
fields.append(filename_field)
if id:
- res = Model.read([int(id)], fields, context)[0]
+ res = Model.read([int(id)], fields, req.context)[0]
else:
- res = Model.default_get(fields, context)
+ res = Model.default_get(fields, req.context)
filecontent = base64.b64decode(res.get(field, ''))
if not filecontent:
return req.not_found()
filename = res.get(filename_field, '') or filename
return req.make_response(filecontent,
[('Content-Type', 'application/octet-stream'),
- ('Content-Disposition', self.content_disposition(filename, req))])
+ ('Content-Disposition', content_disposition(filename, req))])
@openerpweb.httprequest
def saveas_ajax(self, req, data, token):
jdata = simplejson.loads(data)
model = jdata['model']
field = jdata['field']
+ data = jdata['data']
id = jdata.get('id', None)
filename_field = jdata.get('filename_field', None)
- context = jdata.get('context', dict())
+ context = jdata.get('context', {})
- context = req.session.eval_context(context)
Model = req.session.model(model)
fields = [field]
if filename_field:
fields.append(filename_field)
- if id:
+ if data:
+ res = { field: data }
+ elif id:
res = Model.read([int(id)], fields, context)[0]
else:
res = Model.default_get(fields, context)
filecontent = base64.b64decode(res.get(field, ''))
if not filecontent:
- raise ValueError("No content found for field '%s' on '%s:%s'" %
+ raise ValueError(_("No content found for field '%s' on '%s:%s'") %
(field, model, id))
else:
filename = '%s_%s' % (model.replace('.', '_'), id)
filename = res.get(filename_field, '') or filename
return req.make_response(filecontent,
headers=[('Content-Type', 'application/octet-stream'),
- ('Content-Disposition', self.content_disposition(filename, req))],
+ ('Content-Disposition', content_disposition(filename, req))],
cookies={'fileToken': int(token)})
@openerpweb.httprequest
def upload(self, req, callback, ufile):
# TODO: might be useful to have a configuration flag for max-length file uploads
+ out = """<script language="javascript" type="text/javascript">
+ var win = window.top.window;
+ win.jQuery(win).trigger(%s, %s);
+ </script>"""
try:
- out = """<script language="javascript" type="text/javascript">
- var win = window.top.window,
- callback = win[%s];
- if (typeof(callback) === 'function') {
- callback.apply(this, %s);
- } else {
- win.jQuery('#oe_notification', win.document).notify('create', {
- title: "Ajax File Upload",
- text: "Could not find callback"
- });
- }
- </script>"""
data = ufile.read()
args = [len(data), ufile.filename,
ufile.content_type, base64.b64encode(data)]
@openerpweb.httprequest
def upload_attachment(self, req, callback, model, id, ufile):
- context = req.session.eval_context(req.context)
Model = req.session.model('ir.attachment')
+ out = """<script language="javascript" type="text/javascript">
+ var win = window.top.window;
+ win.jQuery(win).trigger(%s, %s);
+ </script>"""
try:
- out = """<script language="javascript" type="text/javascript">
- var win = window.top.window,
- callback = win[%s];
- if (typeof(callback) === 'function') {
- callback.call(this, %s);
- }
- </script>"""
attachment_id = Model.create({
'name': ufile.filename,
'datas': base64.encodestring(ufile.read()),
'datas_fname': ufile.filename,
'res_model': model,
'res_id': int(id)
- }, context)
+ }, req.context)
args = {
'filename': ufile.filename,
'id': attachment_id
}
- except Exception, e:
- args = { 'error': e.message }
+ except xmlrpclib.Fault, e:
+ args = {'error':e.faultCode }
return out % (simplejson.dumps(callback), simplejson.dumps(args))
+ @openerpweb.httprequest
+ def company_logo(self, req, dbname=None):
+ # TODO add etag, refactor to use /image code for etag
+ uid = None
+ if req.session._db:
+ dbname = req.session._db
+ uid = req.session._uid
+ elif dbname is None:
+ dbname = db_monodb(req)
+
+ if uid is None:
+ uid = openerp.SUPERUSER_ID
+
+ if not dbname:
+ image_data = self.placeholder(req, 'logo.png')
+ else:
+ registry = openerp.modules.registry.RegistryManager.get(dbname)
+ with registry.cursor() as cr:
+ user = registry.get('res.users').browse(cr, uid, uid)
+ if user.company_id.logo_web:
+ image_data = user.company_id.logo_web.decode('base64')
+ else:
+ image_data = self.placeholder(req, 'nologo.png')
+ headers = [
+ ('Content-Type', 'image/png'),
+ ('Content-Length', len(image_data)),
+ ]
+ return req.make_response(image_data, headers)
+
class Action(openerpweb.Controller):
_cp_path = "/web/action"
- # For most actions, the type attribute and the model name are the same, but
- # there are exceptions. This dict is used to remap action type attributes
- # to the "real" model name when they differ.
- action_mapping = {
- "ir.actions.act_url": "ir.actions.url",
- }
-
@openerpweb.jsonrequest
def load(self, req, action_id, do_not_eval=False):
Actions = req.session.model('ir.actions.actions')
value = False
- context = req.session.eval_context(req.context)
- base_action = Actions.read([action_id], ['type'], context)
+ try:
+ action_id = int(action_id)
+ except ValueError:
+ try:
+ module, xmlid = action_id.split('.', 1)
+ model, action_id = req.session.model('ir.model.data').get_object_reference(module, xmlid)
+ assert model.startswith('ir.actions.')
+ except Exception:
+ action_id = 0 # force failed read
+
+ base_action = Actions.read([action_id], ['type'], req.context)
if base_action:
ctx = {}
action_type = base_action[0]['type']
if action_type == 'ir.actions.report.xml':
ctx.update({'bin_size': True})
- ctx.update(context)
- action_model = self.action_mapping.get(action_type, action_type)
- action = req.session.model(action_model).read([action_id], False, ctx)
+ ctx.update(req.context)
+ action = req.session.model(action_type).read([action_id], False, ctx)
if action:
- value = clean_action(req, action[0], do_not_eval)
- return {'result': value}
+ value = clean_action(req, action[0])
+ return value
@openerpweb.jsonrequest
def run(self, req, action_id):
- return clean_action(req, req.session.model('ir.actions.server').run(
- [action_id], req.session.eval_context(req.context)))
+ return_action = req.session.model('ir.actions.server').run(
+ [action_id], req.context)
+ if return_action:
+ return clean_action(req, return_action)
+ else:
+ return False
class Export(View):
_cp_path = "/web/export"
def fields_get(self, req, model):
Model = req.session.model(model)
- fields = Model.fields_get(False, req.session.eval_context(req.context))
+ fields = Model.fields_get(False, req.context)
return fields
@openerpweb.jsonrequest
def fields_info(self, req, model, export_fields):
info = {}
fields = self.fields_get(req, model)
+ if ".id" in export_fields:
+ fields['.id'] = fields.pop('id', {'string': 'ID'})
# To make fields retrieval more efficient, fetch all sub-fields of a
# given field at the same time. Because the order in the export list is
'import_compat')(
simplejson.loads(data))
- context = req.session.eval_context(req.context)
Model = req.session.model(model)
- ids = ids or Model.search(domain, 0, False, False, context)
+ ids = ids or Model.search(domain, 0, False, False, req.context)
field_names = map(operator.itemgetter('name'), fields)
- import_data = Model.export_data(ids, field_names, context).get('datas',[])
+ import_data = Model.export_data(ids, field_names, req.context).get('datas',[])
if import_compat:
columns_headers = field_names
return req.make_response(self.from_data(columns_headers, import_data),
- headers=[('Content-Disposition', 'attachment; filename="%s"' % self.filename(model)),
+ headers=[('Content-Disposition',
+ content_disposition(self.filename(model), req)),
('Content-Type', self.content_type)],
cookies={'fileToken': int(token)})
action = simplejson.loads(action)
report_srv = req.session.proxy("report")
- context = req.session.eval_context(
- common.nonliterals.CompoundContext(
- req.context or {}, action[ "context"]))
+ context = dict(req.context)
+ context.update(action["context"])
report_data = {}
report_ids = context["active_ids"]
report = zlib.decompress(report)
report_mimetype = self.TYPES_MAPPING.get(
report_struct['format'], 'octet-stream')
+ file_name = action.get('name', 'report')
+ if 'name' not in action:
+ reports = req.session.model('ir.actions.report.xml')
+ res_id = reports.search([('report_name', '=', action['report_name']),],
+ 0, False, False, context)
+ if len(res_id) > 0:
+ file_name = reports.read(res_id[0], ['name'], context)['name']
+ else:
+ file_name = action['report_name']
+ file_name = '%s.%s' % (file_name, report_struct['format'])
+
return req.make_response(report,
headers=[
- ('Content-Disposition', 'attachment; filename="%s.%s"' % (action['report_name'], report_struct['format'])),
+ ('Content-Disposition', content_disposition(file_name, req)),
('Content-Type', report_mimetype),
('Content-Length', len(report))],
cookies={'fileToken': int(token)})
-class Import(View):
- _cp_path = "/web/import"
-
- def fields_get(self, req, model):
- Model = req.session.model(model)
- fields = Model.fields_get(False, req.session.eval_context(req.context))
- return fields
-
- @openerpweb.httprequest
- def detect_data(self, req, csvfile, csvsep=',', csvdel='"', csvcode='utf-8', jsonp='callback'):
- try:
- data = list(csv.reader(
- csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
- except csv.Error, e:
- csvfile.seek(0)
- return '<script>window.top.%s(%s);</script>' % (
- jsonp, simplejson.dumps({'error': {
- 'message': 'Error parsing CSV file: %s' % e,
- # decodes each byte to a unicode character, which may or
- # may not be printable, but decoding will succeed.
- # Otherwise simplejson will try to decode the `str` using
- # utf-8, which is very likely to blow up on characters out
- # of the ascii range (in range [128, 256))
- 'preview': csvfile.read(200).decode('iso-8859-1')}}))
-
- try:
- return '<script>window.top.%s(%s);</script>' % (
- jsonp, simplejson.dumps(
- {'records': data[:10]}, encoding=csvcode))
- except UnicodeDecodeError:
- return '<script>window.top.%s(%s);</script>' % (
- jsonp, simplejson.dumps({
- 'message': u"Failed to decode CSV file using encoding %s, "
- u"try switching to a different encoding" % csvcode
- }))
-
- @openerpweb.httprequest
- def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
- meta):
- skip, indices, fields, context = \
- operator.itemgetter('skip', 'indices', 'fields', 'context')(
- simplejson.loads(meta, object_hook=common.nonliterals.non_literal_decoder))
-
- error = None
- if not (csvdel and len(csvdel) == 1):
- error = u"The CSV delimiter must be a single character"
-
- if not indices and fields:
- error = u"You must select at least one field to import"
-
- if error:
- return '<script>window.top.%s(%s);</script>' % (
- jsonp, simplejson.dumps({'error': {'message': error}}))
-
- # skip ignored records (@skip parameter)
- # then skip empty lines (not valid csv)
- # nb: should these operations be reverted?
- rows_to_import = itertools.ifilter(
- None,
- itertools.islice(
- csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
- skip, None))
-
- # if only one index, itemgetter will return an atom rather than a tuple
- if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
- else: mapper = operator.itemgetter(*indices)
-
- data = None
- error = None
- try:
- # decode each data row
- data = [
- [record.decode(csvcode) for record in row]
- for row in itertools.imap(mapper, rows_to_import)
- # don't insert completely empty rows (can happen due to fields
- # filtering in case of e.g. o2m content rows)
- if any(row)
- ]
- except UnicodeDecodeError, e:
- # decode with iso-8859-1 for error display: always works-ish
- error = u"Failed to decode cell %r using encoding %s: '%s'" % (
- e.object, e.encoding, e.reason)
- except csv.Error, e:
- error = u"Could not process CSV file: %s" % e
-
- # If the file contains nothing, and the import has not already blown up
- if not (error or data):
- error = u"File to import is empty"
- if error:
- return '<script>window.top.%s(%s);</script>' % (
- jsonp, simplejson.dumps({'error': {'message': error}}))
-
- try:
- (code, record, message, _nope) = req.session.model(model).import_data(
- fields, data, 'init', '', False,
- req.session.eval_context(context))
- except xmlrpclib.Fault, e:
- error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
- return '<script>window.top.%s(%s);</script>' % (
- jsonp, simplejson.dumps({'error':error}))
-
- if code != -1:
- return '<script>window.top.%s(%s);</script>' % (
- jsonp, simplejson.dumps({'success':True}))
-
- msg = u"Error during import: %s\n\nTrying to import record %r" % (
- message, record)
- return '<script>window.top.%s(%s);</script>' % (
- jsonp, simplejson.dumps({'error': {'message':msg}}))
+# vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: