diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 9239dae3f4c..a90d231e471 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -31,9 +31,138 @@ from .. import common openerpweb = common.http #---------------------------------------------------------- -# OpenERP Web web Controllers +# OpenERP Web helpers #---------------------------------------------------------- +def sass2scss(src): + # Validated by diff -u of sass2scss against: + # sass-convert -F sass -T scss openerp.sass openerp.scss + block = [] + sass = ('', block) + reComment = re.compile(r'//.*$') + reIndent = re.compile(r'^\s+') + reIgnore = re.compile(r'^\s*(//.*)?$') + reFixes = { re.compile(r'\(\((.*)\)\)') : r'(\1)', } + lastLevel = 0 + prevBlocks = {} + for l in src.split('\n'): + l = l.rstrip() + if reIgnore.search(l): continue + l = reComment.sub('', l) + l = l.rstrip() + indent = reIndent.match(l) + level = indent.end() if indent else 0 + l = l[level:] + if level>lastLevel: + prevBlocks[lastLevel] = block + newBlock = [] + block[-1] = (block[-1], newBlock) + block = newBlock + elif level=0: + out += indent+sass[0]+" {\n" + for e in sass[1]: + out += write(e, level+1) + if level>=0: + out = out.rstrip(" \n") + out += ' }\n' + if level==0: + out += "\n" + else: + out += indent+sass+";\n" + return out + return write(sass) + +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 = req.config.dbfilter.replace('%h', h).replace('%d', d) + dbs = [i for i in dbs if re.match(r, i)] + return dbs + +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 + 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_boot(req): + addons = [] + for i in req.config.server_wide_modules: + if i in openerpweb.addons_manifest: + addons.append(i) + return addons + def concat_xml(file_list): """Concatenate xml files @@ -89,64 +218,9 @@ def concat_files(file_list, reader=None, intersperse=""): files_concat = intersperse.join(files_content) return files_concat, checksum.hexdigest() -def sass2scss(src): - # Validated by diff -u of sass2scss against: - # sass-convert -F sass -T scss openerp.sass openerp.scss - block = [] - sass = ('', block) - reComment = re.compile(r'//.*$') - reIndent = re.compile(r'^\s+') - reIgnore = re.compile(r'^\s*(//.*)?$') - reFixes = { re.compile(r'\(\((.*)\)\)') : r'(\1)', } - lastLevel = 0 - prevBlocks = {} - for l in src.split('\n'): - l = l.rstrip() - if reIgnore.search(l): continue - l = reComment.sub('', l) - l = l.rstrip() - indent = reIndent.match(l) - level = indent.end() if indent else 0 - l = l[level:] - if level>lastLevel: - prevBlocks[lastLevel] = block - newBlock = [] - block[-1] = (block[-1], newBlock) - block = newBlock - elif level=0: - out += indent+sass[0]+" {\n" - for e in sass[1]: - out += write(e, level+1) - if level>=0: - out = out.rstrip(" \n") - out += ' }\n' - if level==0: - out += "\n" - else: - out += indent+sass+";\n" - return out - return write(sass) - -def server_wide_modules(req): - addons = [i for i in req.config.server_wide_modules if i in openerpweb.addons_manifest] - return addons - def manifest_glob(req, addons, key): if addons is None: - addons = server_wide_modules(req) + addons = module_boot(req) else: addons = addons.split(',') r = [] @@ -214,6 +288,164 @@ def make_conditional(req, response, last_modified=None, etag=None): response.set_etag(etag) return response.make_conditional(req.httprequest) +def login_and_redirect(req, db, login, key, redirect_url='/'): + req.session.authenticate(db, login, key, {}) + redirect = werkzeug.utils.redirect(redirect_url, 303) + cookie_val = urllib2.quote(simplejson.dumps(req.session_id)) + redirect.set_cookie('instance0|session_id', cookie_val) + return redirect + +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): + 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. + + 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 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 + +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) + +#---------------------------------------------------------- +# OpenERP Web web Controllers +#---------------------------------------------------------- + html_template = """ @@ -246,21 +478,14 @@ class Home(openerpweb.Controller): r = html_template % { 'js': js, 'css': css, - 'modules': simplejson.dumps(server_wide_modules(req)), + 'modules': simplejson.dumps(module_boot(req)), 'init': 'var wc = new s.web.WebClient();wc.appendTo($(document.body));' } return r @openerpweb.httprequest def login(self, req, db, login, key): - return self._login(req, db, login, key) - - def _login(self, req, db, login, key, redirect_url='/'): - req.session.authenticate(db, login, key, {}) - redirect = werkzeug.utils.redirect(redirect_url, 303) - cookie_val = urllib2.quote(simplejson.dumps(req.session_id)) - redirect.set_cookie('instance0|session_id', cookie_val) - return redirect + return login_and_redirect(req, db, login, key) class WebClient(openerpweb.Controller): _cp_path = "/web/webclient" @@ -410,12 +635,7 @@ class Database(openerpweb.Controller): @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)] + dbs = db_list(req) return {"db_list": dbs} @openerpweb.jsonrequest @@ -484,50 +704,6 @@ class Database(openerpweb.Controller): 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 - class Session(openerpweb.Controller): _cp_path = "/web/session" @@ -593,34 +769,9 @@ class Session(openerpweb.Controller): @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] + loaded = module_boot(req) + modules = module_installed(req) + return [module for module in modules if module not in loaded] @openerpweb.jsonrequest def eval_domain_and_context(self, req, contexts, domains, @@ -724,120 +875,6 @@ class Session(openerpweb.Controller): 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): - 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. - - 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 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" @@ -1202,39 +1239,6 @@ class View(openerpweb.Controller): 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" @@ -1308,7 +1312,6 @@ class SearchView(View): del filter['context'] del filter['domain'] return filters - class Binary(openerpweb.Controller): _cp_path = "/web/binary" @@ -1955,3 +1958,5 @@ class Import(View): message, record) return '' % ( jsonp, simplejson.dumps({'error': {'message':msg}})) + +# vim:expandtab:tabstop=4:softtabstop=4:shiftwidth=4: