diff --git a/openerp/addons/base/base_demo.xml b/openerp/addons/base/base_demo.xml index f960c6d978a..9258dcf3553 100644 --- a/openerp/addons/base/base_demo.xml +++ b/openerp/addons/base/base_demo.xml @@ -18,7 +18,6 @@ 1725 Slough Ave. Scranton 18540 - +1 555 123 8069 info@yourcompany.example.com www.example.com diff --git a/openerp/addons/base/ir/ir_http.py b/openerp/addons/base/ir/ir_http.py index 089d962e330..7d0c4be8bc1 100644 --- a/openerp/addons/base/ir/ir_http.py +++ b/openerp/addons/base/ir/ir_http.py @@ -99,7 +99,7 @@ class ir_http(osv.AbstractModel): # check authentication level try: - auth_method = self._authenticate(getattr(func, "auth", None)) + auth_method = self._authenticate(func.routing["auth"]) except Exception: # force a Forbidden exception with the original traceback return self._handle_exception( diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index aef88a1b375..77a3c8435d6 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -20,6 +20,9 @@ from openerp.tools.translate import _ _logger = logging.getLogger(__name__) +#-------------------------------------------------------------------- +# QWeb template engine +#-------------------------------------------------------------------- class QWebException(Exception): def __init__(self, message, template=None, node=None, attribute=None): @@ -28,7 +31,6 @@ class QWebException(Exception): self.node = node self.attribute = attribute - class QWebContext(dict): def __init__(self, cr, uid, data, loader=None, templates=None, context=None): self.cr = cr @@ -59,29 +61,30 @@ class QWebContext(dict): class QWeb(orm.AbstractModel): """QWeb Xml templating engine - The templating engine use a very simple syntax, "magic" xml attributes, to - produce any kind of texutal output (even non-xml). + The templating engine use a very simple syntax based "magic" xml + attributes, to produce textual output (even non-xml). - QWebXml: - the template engine core implements the basic magic attributes: + The core magic attributes are: - t-att t-raw t-esc t-if t-foreach t-set t-call t-trim + flow attributes: + t-if t-foreach t-call + output attributes: + t-att t-raw t-esc t-trim - - loader: function that return a template + assignation attribute: + t-set - QWeb rendering can be used for many tasks. As a result, customizations - made by one task context (to either the main qweb rendering or to specific - fields rendering) could break other tasks. - - To avoid that, ``ir.qweb`` was consciously made inheritable and the "root" - of an object hierarchy. If you need extensions or alterations which could - be incompatible with other subsystems, you should create a local object - inheriting from ``ir.qweb`` and customize that. + QWeb can be extended like any OpenERP model and new attributes can be + added. If you need to customize t-fields rendering, subclass the ir.qweb.field model (and its sub-models) then override :meth:`~.get_converter_for` to fetch the right field converters for your qweb model. + + Beware that if you need extensions or alterations which could be + incompatible with other subsystems, you should create a local object + inheriting from ``ir.qweb`` and customize that. """ _name = 'ir.qweb' @@ -121,7 +124,7 @@ class QWeb(orm.AbstractModel): def register_tag(self, tag, func): self._render_tag[tag] = func - def load_document(self, x, into, context): + def load_document(self, x, qcontext): """ Loads an XML document and installs any contained template in the engine """ @@ -134,18 +137,20 @@ class QWeb(orm.AbstractModel): for n in dom.documentElement.childNodes: if n.nodeType == self.node.ELEMENT_NODE and n.getAttribute('t-name'): - self.add_template(into, str(n.getAttribute("t-name")), n, context) + name = str(n.getAttribute("t-name")) + + qcontext.templates[name] = n - def add_template(self, into, name, node, context): - into[name] = node + def get_template(self, name, qcontext): + if qcontext.loader and name not in qcontext.templates: + xml_doc = qcontext.loader(name) + self.load_document(xml_doc, qcontext=qcontext) - def get_template(self, name, context): - if context.loader and name not in context.templates: - xml_doc = context.loader(name) - self.load_document(xml_doc, into=context.templates, context=context) + if name in qcontext.templates: + return qcontext.templates[name] - if name in context.templates: - return context.templates[name] + import pdb + pdb.set_trace() e = KeyError('qweb: template "%s" not found' % name) setattr(e, 'qweb_template', name) @@ -191,32 +196,19 @@ class QWeb(orm.AbstractModel): else: return 0 - @openerp.tools.ormcache() - def get_template_xmlid(self, cr, uid, id): - imd = self.pool['ir.model.data'] - domain = [('model', '=', 'ir.ui.view'), ('res_id', '=', id)] - xmlid = imd.search_read(cr, uid, domain, ['module', 'name'])[0] - return '%s.%s' % (xmlid['module'], xmlid['name']) - def render(self, cr, uid, id_or_xml_id, v=None, loader=None, context=None): - if isinstance(id_or_xml_id, list): - id_or_xml_id = id_or_xml_id[0] - tname = id_or_xml_id - if isinstance(id_or_xml_id, (int, long)): - tname = self.get_template_xmlid(cr, uid, tname) - if v is None: v = {} if not isinstance(v, QWebContext): v = QWebContext(cr, uid, v, loader=loader, context=context) - v['__template__'] = tname + v['__template__'] = id_or_xml_id stack = v.get('__stack__', []) if stack: v['__caller__'] = stack[-1] - stack.append(tname) + stack.append(id_or_xml_id) v['__stack__'] = stack v['xmlid'] = str(stack[0]) # Temporary fix - return self.render_node(self.get_template(tname, v), v) + return self.render_node(self.get_template(id_or_xml_id, v), v) def render_node(self, e, v): r = "" @@ -314,10 +306,13 @@ class QWeb(orm.AbstractModel): def render_att_href(self, e, an, av, v): return self.url_for(e, an, av, v) + def render_att_src(self, e, an, av, v): return self.url_for(e, an, av, v) + def render_att_action(self, e, an, av, v): return self.url_for(e, an, av, v) + def url_for(self, e, an, av, v): if 'url_for' not in v: raise KeyError("qweb: no 'url_for' found in context") @@ -384,7 +379,7 @@ class QWeb(orm.AbstractModel): return "" def render_tag_call(self, e, t_att, g_att, v): - d = v if 'import' in t_att else v.copy() + d = v.copy() d[0] = self.render_element(e, t_att, g_att, d) return self.render(None, None, self.eval_format(t_att["call"], d), d) @@ -423,6 +418,9 @@ class QWeb(orm.AbstractModel): return self.pool.get('ir.qweb.field.' + field_type, self.pool['ir.qweb.field']) +#-------------------------------------------------------------------- +# QWeb Fields converters +#-------------------------------------------------------------------- class FieldConverter(osv.AbstractModel): """ Used to convert a t-field specification into an output HTML field. diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 29b1a45b5a7..806d108f4e6 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -40,6 +40,8 @@ from openerp.osv.orm import browse_record, browse_record_list _logger = logging.getLogger(__name__) +MOVABLE_BRANDING = ['data-oe-model', 'data-oe-id', 'data-oe-field', 'data-oe-xpath'] + class view_custom(osv.osv): _name = 'ir.ui.view.custom' _order = 'create_date desc' # search(limit=1) should return the last customization @@ -71,7 +73,6 @@ class view(osv.osv): 'type': fields.selection([ ('tree','Tree'), ('form','Form'), - ('mdx','mdx'), ('graph', 'Graph'), ('calendar', 'Calendar'), ('diagram','Diagram'), @@ -183,11 +184,18 @@ class view(osv.osv): # TODO: should be doable in a read and a write for view_ in self.browse(cr, uid, ids, context=context): if view_.model_data_id: - view_.model_data_id.write({'noupdate': True}) + self.pool.get('ir.model.data').write(cr, openerp.SUPERUSER_ID, view_.model_data_id.id, {'noupdate': True}) return ret - # default view selection + def copy(self, cr, uid, id, default=None, context=None): + if not default: + default = {} + default.update({ + 'model_ids': [], + }) + return super(view, self).copy(cr, uid, id, default, context=context) + # default view selection def default_view(self, cr, uid, model, view_type, context=None): """ Fetches the default view for the provided (model, view_type) pair: view with no parent (inherit_id=Fase) with the lowest priority. @@ -207,8 +215,9 @@ class view(osv.osv): return False return ids[0] - # inheritance - + #------------------------------------------------------ + # Inheritance mecanism + #------------------------------------------------------ def get_inheriting_views_arch(self, cr, uid, view_id, model, context=None): """Retrieves the architecture of views that inherit from the given view, from the sets of views that should currently be used in the system. During the module upgrade phase it @@ -422,8 +431,13 @@ class view(osv.osv): return dict(view, arch=etree.tostring(arch, encoding='utf-8')) - # postprocessing: groups, modifiers, ... - + #------------------------------------------------------ + # Postprocessing: translation, groups and modifiers + #------------------------------------------------------ + # TODO: + # - split postprocess so that it can be used instead of translate_qweb + # - remove group processing from ir_qweb + #------------------------------------------------------ def postprocess(self, cr, user, model, node, view_id, in_tree_view, model_fields, context=None): """Return the description of the fields in the node. @@ -685,30 +699,32 @@ class view(osv.osv): raise orm.except_orm('View error', msg) return arch, fields - # view used as templates - + #------------------------------------------------------ + # QWeb template views + #------------------------------------------------------ @tools.ormcache_context(accepted_keys=('lang','inherit_branding', 'editable', 'translatable')) - def read_template(self, cr, uid, id_, context=None): - try: - id_ = int(id_) - except ValueError: - if '.' not in id_: - raise ValueError('Invalid id: %r' % (id_,)) - IMD = self.pool['ir.model.data'] - m, _, n = id_.partition('.') - _, id_ = IMD.get_object_reference(cr, uid, m, n) + def read_template(self, cr, uid, xml_id, context=None): + if '.' not in xml_id: + raise ValueError('Invalid template id: %r' % (xml_id,)) - arch = self.read_combined(cr, uid, id_, fields=['arch'], context=context)['arch'] + m, n = xml_id.split('.', 1) + _, view_id = self.pool['ir.model.data'].get_object_reference(cr, uid, m, n) + + arch = self.read_combined(cr, uid, view_id, fields=['arch'], context=context)['arch'] arch_tree = etree.fromstring(arch) if 'lang' in context: - arch_tree = self.translate_qweb(cr, uid, id_, arch_tree, context['lang'], context) + arch_tree = self.translate_qweb(cr, uid, view_id, arch_tree, context['lang'], context) + self.distribute_branding(arch_tree) - root = etree.Element('tpl') + root = etree.Element('templates') root.append(arch_tree) arch = etree.tostring(root, encoding='utf-8', xml_declaration=True) return arch + def clear_cache(self): + self.read_template.clear_cache(self) + def distribute_branding(self, e, branding=None, parent_xpath='', index_map=misc.ConstantMapping(1)): if e.get('t-ignore') or e.tag == 'head': @@ -768,8 +784,6 @@ class view(osv.osv): text = h.unescape(text.strip()) if len(text) < 2 or (text.startswith('')): return None - # if text == 'Our Events': - # from pudb import set_trace;set_trace() ############################## Breakpoint ############################## return Translations._get_source(cr, uid, 'website', 'view', lang, text, id_) if arch.tag not in ['script']: @@ -788,16 +802,18 @@ class view(osv.osv): self.translate_qweb(cr, uid, id_, node, lang, context) return arch - def render(self, cr, uid, id_or_xml_id, values=None, engine='ir.qweb', context=None): + def render(self, cr, uid, xml_id, values=None, engine='ir.qweb', context=None): if not context: context = {} def loader(name): return self.read_template(cr, uid, name, context=context) - return self.pool[engine].render(cr, uid, id_or_xml_id, values, loader=loader, context=context) + return self.pool[engine].render(cr, uid, xml_id, values, loader=loader, context=context) - # maybe used to print the workflow ? + #------------------------------------------------------ + # Misc + #------------------------------------------------------ def graph_get(self, cr, uid, id, model, node_obj, conn_obj, src_node, des_node, label, scale, context=None): nodes=[] @@ -866,14 +882,6 @@ class view(osv.osv): 'blank_nodes': blank_nodes, 'node_parent_field': _Model_Field,} - def copy(self, cr, uid, id, default=None, context=None): - if not default: - default = {} - default.update({ - 'model_ids': [], - }) - return super(view, self).copy(cr, uid, id, default, context=context) - def _validate_custom_views(self, cr, uid, model): """Validate architecture of custom views (= without xml id) for a given model. This method is called at the end of registry update. @@ -889,4 +897,4 @@ class view(osv.osv): ids = map(itemgetter(0), cr.fetchall()) return self._check_xml(cr, uid, ids) -MOVABLE_BRANDING = ['data-oe-model', 'data-oe-id', 'data-oe-field', 'data-oe-xpath'] +# vim:et: diff --git a/openerp/addons/base/res/res_users.py b/openerp/addons/base/res/res_users.py index 43d3706d72d..30d93a7c747 100644 --- a/openerp/addons/base/res/res_users.py +++ b/openerp/addons/base/res/res_users.py @@ -600,6 +600,7 @@ class users_implied(osv.osv): if groups: # delegate addition of groups to add implied groups self.write(cr, uid, [user_id], {'groups_id': groups}, context) + self.pool['ir.ui.view'].clear_cache() return user_id def write(self, cr, uid, ids, values, context=None): @@ -612,6 +613,7 @@ class users_implied(osv.osv): gs = set(concat([g.trans_implied_ids for g in user.groups_id])) vals = {'groups_id': [(4, g.id) for g in gs]} super(users_implied, self).write(cr, uid, [user.id], vals, context) + self.pool['ir.ui.view'].clear_cache() return res #---------------------------------------------------------- diff --git a/openerp/http.py b/openerp/http.py index 92d9349e7eb..03c8c999dde 100644 --- a/openerp/http.py +++ b/openerp/http.py @@ -168,7 +168,7 @@ class WebRequest(object): if not k.startswith("_ignored_")) self.func = func - self.func_request_type = func.exposed + self.func_request_type = func.routing['type'] self.func_arguments = arguments self.auth_method = auth @@ -207,7 +207,7 @@ class WebRequest(object): warnings.warn('please use request.registry and request.cr directly', DeprecationWarning) yield (self.registry, self.cr) -def route(route, type="http", auth="user", methods=None): +def route(route=None, **kw): """ Decorator marking the decorated method as being a handler for requests. The method must be part of a subclass of ``Controller``. @@ -226,16 +226,16 @@ def route(route, type="http", auth="user", methods=None): configuration indicating the current database nor the current user. :param methods: A sequence of http methods this route applies to. If not specified, all methods are allowed. """ - assert type in ["http", "json"] + routing = kw.copy() + assert not 'type' in routing or routing['type'] in ("http", "json") def decorator(f): - if isinstance(route, list): - f.routes = route - else: - f.routes = [route] - f.methods = methods - f.exposed = type - if getattr(f, "auth", None) is None: - f.auth = auth + if route: + if isinstance(route, list): + routes = route + else: + routes = [route] + routing['routes'] = routes + f.routing = routing return f return decorator @@ -388,11 +388,10 @@ def jsonrequest(f): Use the ``route()`` decorator instead. """ - f.combine = True base = f.__name__.lstrip('/') if f.__name__ == "index": base = "" - return route([base, base + "/"], type="json", auth="user")(f) + return route([base, base + "/"], type="json", auth="user", combine=True)(f) class HttpRequest(WebRequest): """ Regular GET/POST request @@ -444,11 +443,10 @@ def httprequest(f): Use the ``route()`` decorator instead. """ - f.combine = True base = f.__name__.lstrip('/') if f.__name__ == "index": base = "" - return route([base, base + "/"], type="http", auth="user")(f) + return route([base, base + "/"], type="http", auth="user", combine=True)(f) #---------------------------------------------------------- # Controller and route registration @@ -501,13 +499,22 @@ def routing_map(modules, nodb_only, converters=None): o = cls() members = inspect.getmembers(o) for mk, mv in members: - if inspect.ismethod(mv) and getattr(mv, 'exposed', False) and (not nodb_only or nodb_only == (mv.auth == "none")): - for url in mv.routes: - if getattr(mv, "combine", False): - url = o._cp_path.rstrip('/') + '/' + url.lstrip('/') - if url.endswith("/") and len(url) > 1: - url = url[: -1] - routing_map.add(werkzeug.routing.Rule(url, endpoint=mv, methods=mv.methods)) + if inspect.ismethod(mv) and hasattr(mv, 'routing'): + routing = dict(type='http', auth='user', methods=None) + for claz in reversed(mv.im_class.mro()): + fn = getattr(claz, mv.func_name, None) + if fn and hasattr(fn, 'routing'): + routing.update(fn.routing) + mv.routing.update(routing) + assert 'routes' in mv.routing + if not nodb_only or nodb_only == (mv.routing['auth'] == "none"): + for url in mv.routing['routes']: + if mv.routing.get("combine", False): + # deprecated + url = o._cp_path.rstrip('/') + '/' + url.lstrip('/') + if url.endswith("/") and len(url) > 1: + url = url[: -1] + routing_map.add(werkzeug.routing.Rule(url, endpoint=mv, methods=mv.routing['methods'])) return routing_map #---------------------------------------------------------- diff --git a/openerp/workflow/instance.py b/openerp/workflow/instance.py index 8ae633b5ba1..fd15eee6243 100644 --- a/openerp/workflow/instance.py +++ b/openerp/workflow/instance.py @@ -106,7 +106,7 @@ class WorkflowInstance(object): for cur_instance_id, cur_model_name, cur_record_id in cr.fetchall(): cur_record = Record(cur_model_name, cur_record_id) for act_name in act_names: - WorkflowInstance(self.session, cur_record, {'id':cur_instance_id}).validate('subflow.{}'.format(act_name[0])) + WorkflowInstance(self.session, cur_record, {'id':cur_instance_id}).validate('subflow.%s' % act_name[0]) return ok