[MERGE] upstream

bzr revid: fme@openerp.com-20140120161129-elnp1afn8ognyc6z
This commit is contained in:
Fabien Meghazi 2014-01-20 17:11:29 +01:00
commit 05e2942121
7 changed files with 116 additions and 102 deletions

View File

@ -18,7 +18,6 @@
<field name="street">1725 Slough Ave.</field> <field name="street">1725 Slough Ave.</field>
<field name="city">Scranton</field> <field name="city">Scranton</field>
<field name="zip">18540</field> <field name="zip">18540</field>
<field name="country_id" ref="us"/>
<field name="phone">+1 555 123 8069</field> <field name="phone">+1 555 123 8069</field>
<field name="email">info@yourcompany.example.com</field> <field name="email">info@yourcompany.example.com</field>
<field name="website">www.example.com</field> <field name="website">www.example.com</field>

View File

@ -99,7 +99,7 @@ class ir_http(osv.AbstractModel):
# check authentication level # check authentication level
try: try:
auth_method = self._authenticate(getattr(func, "auth", None)) auth_method = self._authenticate(func.routing["auth"])
except Exception: except Exception:
# force a Forbidden exception with the original traceback # force a Forbidden exception with the original traceback
return self._handle_exception( return self._handle_exception(

View File

@ -20,6 +20,9 @@ from openerp.tools.translate import _
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
#--------------------------------------------------------------------
# QWeb template engine
#--------------------------------------------------------------------
class QWebException(Exception): class QWebException(Exception):
def __init__(self, message, template=None, node=None, attribute=None): def __init__(self, message, template=None, node=None, attribute=None):
@ -28,7 +31,6 @@ class QWebException(Exception):
self.node = node self.node = node
self.attribute = attribute self.attribute = attribute
class QWebContext(dict): class QWebContext(dict):
def __init__(self, cr, uid, data, loader=None, templates=None, context=None): def __init__(self, cr, uid, data, loader=None, templates=None, context=None):
self.cr = cr self.cr = cr
@ -59,29 +61,30 @@ class QWebContext(dict):
class QWeb(orm.AbstractModel): class QWeb(orm.AbstractModel):
"""QWeb Xml templating engine """QWeb Xml templating engine
The templating engine use a very simple syntax, "magic" xml attributes, to The templating engine use a very simple syntax based "magic" xml
produce any kind of texutal output (even non-xml). attributes, to produce textual output (even non-xml).
QWebXml: The core magic attributes are:
the template engine core implements the basic magic attributes:
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 QWeb can be extended like any OpenERP model and new attributes can be
made by one task context (to either the main qweb rendering or to specific added.
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.
If you need to customize t-fields rendering, subclass the ir.qweb.field 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 model (and its sub-models) then override :meth:`~.get_converter_for` to
fetch the right field converters for your qweb model. 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' _name = 'ir.qweb'
@ -121,7 +124,7 @@ class QWeb(orm.AbstractModel):
def register_tag(self, tag, func): def register_tag(self, tag, func):
self._render_tag[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 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: for n in dom.documentElement.childNodes:
if n.nodeType == self.node.ELEMENT_NODE and n.getAttribute('t-name'): 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): def get_template(self, name, qcontext):
into[name] = node 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 name in qcontext.templates:
if context.loader and name not in context.templates: return qcontext.templates[name]
xml_doc = context.loader(name)
self.load_document(xml_doc, into=context.templates, context=context)
if name in context.templates: import pdb
return context.templates[name] pdb.set_trace()
e = KeyError('qweb: template "%s" not found' % name) e = KeyError('qweb: template "%s" not found' % name)
setattr(e, 'qweb_template', name) setattr(e, 'qweb_template', name)
@ -191,32 +196,19 @@ class QWeb(orm.AbstractModel):
else: else:
return 0 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): 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: if v is None:
v = {} v = {}
if not isinstance(v, QWebContext): if not isinstance(v, QWebContext):
v = QWebContext(cr, uid, v, loader=loader, context=context) v = QWebContext(cr, uid, v, loader=loader, context=context)
v['__template__'] = tname v['__template__'] = id_or_xml_id
stack = v.get('__stack__', []) stack = v.get('__stack__', [])
if stack: if stack:
v['__caller__'] = stack[-1] v['__caller__'] = stack[-1]
stack.append(tname) stack.append(id_or_xml_id)
v['__stack__'] = stack v['__stack__'] = stack
v['xmlid'] = str(stack[0]) # Temporary fix 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): def render_node(self, e, v):
r = "" r = ""
@ -314,10 +306,13 @@ class QWeb(orm.AbstractModel):
def render_att_href(self, e, an, av, v): def render_att_href(self, e, an, av, v):
return self.url_for(e, an, av, v) return self.url_for(e, an, av, v)
def render_att_src(self, e, an, av, v): def render_att_src(self, e, an, av, v):
return self.url_for(e, an, av, v) return self.url_for(e, an, av, v)
def render_att_action(self, e, an, av, v): def render_att_action(self, e, an, av, v):
return self.url_for(e, an, av, v) return self.url_for(e, an, av, v)
def url_for(self, e, an, av, v): def url_for(self, e, an, av, v):
if 'url_for' not in v: if 'url_for' not in v:
raise KeyError("qweb: no 'url_for' found in context") raise KeyError("qweb: no 'url_for' found in context")
@ -384,7 +379,7 @@ class QWeb(orm.AbstractModel):
return "" return ""
def render_tag_call(self, e, t_att, g_att, v): 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) d[0] = self.render_element(e, t_att, g_att, d)
return self.render(None, None, self.eval_format(t_att["call"], d), 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, return self.pool.get('ir.qweb.field.' + field_type,
self.pool['ir.qweb.field']) self.pool['ir.qweb.field'])
#--------------------------------------------------------------------
# QWeb Fields converters
#--------------------------------------------------------------------
class FieldConverter(osv.AbstractModel): class FieldConverter(osv.AbstractModel):
""" Used to convert a t-field specification into an output HTML field. """ Used to convert a t-field specification into an output HTML field.

View File

@ -40,6 +40,8 @@ from openerp.osv.orm import browse_record, browse_record_list
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
MOVABLE_BRANDING = ['data-oe-model', 'data-oe-id', 'data-oe-field', 'data-oe-xpath']
class view_custom(osv.osv): class view_custom(osv.osv):
_name = 'ir.ui.view.custom' _name = 'ir.ui.view.custom'
_order = 'create_date desc' # search(limit=1) should return the last customization _order = 'create_date desc' # search(limit=1) should return the last customization
@ -71,7 +73,6 @@ class view(osv.osv):
'type': fields.selection([ 'type': fields.selection([
('tree','Tree'), ('tree','Tree'),
('form','Form'), ('form','Form'),
('mdx','mdx'),
('graph', 'Graph'), ('graph', 'Graph'),
('calendar', 'Calendar'), ('calendar', 'Calendar'),
('diagram','Diagram'), ('diagram','Diagram'),
@ -183,11 +184,18 @@ class view(osv.osv):
# TODO: should be doable in a read and a write # TODO: should be doable in a read and a write
for view_ in self.browse(cr, uid, ids, context=context): for view_ in self.browse(cr, uid, ids, context=context):
if view_.model_data_id: 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 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): def default_view(self, cr, uid, model, view_type, context=None):
""" Fetches the default view for the provided (model, view_type) pair: """ Fetches the default view for the provided (model, view_type) pair:
view with no parent (inherit_id=Fase) with the lowest priority. view with no parent (inherit_id=Fase) with the lowest priority.
@ -207,8 +215,9 @@ class view(osv.osv):
return False return False
return ids[0] return ids[0]
# inheritance #------------------------------------------------------
# Inheritance mecanism
#------------------------------------------------------
def get_inheriting_views_arch(self, cr, uid, view_id, model, context=None): 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 """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 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')) 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): 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. """Return the description of the fields in the node.
@ -685,30 +699,32 @@ class view(osv.osv):
raise orm.except_orm('View error', msg) raise orm.except_orm('View error', msg)
return arch, fields return arch, fields
# view used as templates #------------------------------------------------------
# QWeb template views
#------------------------------------------------------
@tools.ormcache_context(accepted_keys=('lang','inherit_branding', 'editable', 'translatable')) @tools.ormcache_context(accepted_keys=('lang','inherit_branding', 'editable', 'translatable'))
def read_template(self, cr, uid, id_, context=None): def read_template(self, cr, uid, xml_id, context=None):
try: if '.' not in xml_id:
id_ = int(id_) raise ValueError('Invalid template id: %r' % (xml_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)
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) arch_tree = etree.fromstring(arch)
if 'lang' in context: 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) self.distribute_branding(arch_tree)
root = etree.Element('tpl') root = etree.Element('templates')
root.append(arch_tree) root.append(arch_tree)
arch = etree.tostring(root, encoding='utf-8', xml_declaration=True) arch = etree.tostring(root, encoding='utf-8', xml_declaration=True)
return arch return arch
def clear_cache(self):
self.read_template.clear_cache(self)
def distribute_branding(self, e, branding=None, parent_xpath='', def distribute_branding(self, e, branding=None, parent_xpath='',
index_map=misc.ConstantMapping(1)): index_map=misc.ConstantMapping(1)):
if e.get('t-ignore') or e.tag == 'head': if e.get('t-ignore') or e.tag == 'head':
@ -768,8 +784,6 @@ class view(osv.osv):
text = h.unescape(text.strip()) text = h.unescape(text.strip())
if len(text) < 2 or (text.startswith('<!') and text.endswith('>')): if len(text) < 2 or (text.startswith('<!') and text.endswith('>')):
return None 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_) return Translations._get_source(cr, uid, 'website', 'view', lang, text, id_)
if arch.tag not in ['script']: if arch.tag not in ['script']:
@ -788,16 +802,18 @@ class view(osv.osv):
self.translate_qweb(cr, uid, id_, node, lang, context) self.translate_qweb(cr, uid, id_, node, lang, context)
return arch 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: if not context:
context = {} context = {}
def loader(name): def loader(name):
return self.read_template(cr, uid, name, context=context) 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): def graph_get(self, cr, uid, id, model, node_obj, conn_obj, src_node, des_node, label, scale, context=None):
nodes=[] nodes=[]
@ -866,14 +882,6 @@ class view(osv.osv):
'blank_nodes': blank_nodes, 'blank_nodes': blank_nodes,
'node_parent_field': _Model_Field,} '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): def _validate_custom_views(self, cr, uid, model):
"""Validate architecture of custom views (= without xml id) for a given model. """Validate architecture of custom views (= without xml id) for a given model.
This method is called at the end of registry update. This method is called at the end of registry update.
@ -889,4 +897,4 @@ class view(osv.osv):
ids = map(itemgetter(0), cr.fetchall()) ids = map(itemgetter(0), cr.fetchall())
return self._check_xml(cr, uid, ids) return self._check_xml(cr, uid, ids)
MOVABLE_BRANDING = ['data-oe-model', 'data-oe-id', 'data-oe-field', 'data-oe-xpath'] # vim:et:

View File

@ -600,6 +600,7 @@ class users_implied(osv.osv):
if groups: if groups:
# delegate addition of groups to add implied groups # delegate addition of groups to add implied groups
self.write(cr, uid, [user_id], {'groups_id': groups}, context) self.write(cr, uid, [user_id], {'groups_id': groups}, context)
self.pool['ir.ui.view'].clear_cache()
return user_id return user_id
def write(self, cr, uid, ids, values, context=None): 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])) gs = set(concat([g.trans_implied_ids for g in user.groups_id]))
vals = {'groups_id': [(4, g.id) for g in gs]} vals = {'groups_id': [(4, g.id) for g in gs]}
super(users_implied, self).write(cr, uid, [user.id], vals, context) super(users_implied, self).write(cr, uid, [user.id], vals, context)
self.pool['ir.ui.view'].clear_cache()
return res return res
#---------------------------------------------------------- #----------------------------------------------------------

View File

@ -168,7 +168,7 @@ class WebRequest(object):
if not k.startswith("_ignored_")) if not k.startswith("_ignored_"))
self.func = func self.func = func
self.func_request_type = func.exposed self.func_request_type = func.routing['type']
self.func_arguments = arguments self.func_arguments = arguments
self.auth_method = auth self.auth_method = auth
@ -207,7 +207,7 @@ class WebRequest(object):
warnings.warn('please use request.registry and request.cr directly', DeprecationWarning) warnings.warn('please use request.registry and request.cr directly', DeprecationWarning)
yield (self.registry, self.cr) 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 Decorator marking the decorated method as being a handler for requests. The method must be part of a subclass
of ``Controller``. of ``Controller``.
@ -226,16 +226,16 @@ def route(route, type="http", auth="user", methods=None):
configuration indicating the current database nor the current user. 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. :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): def decorator(f):
if isinstance(route, list): if route:
f.routes = route if isinstance(route, list):
else: routes = route
f.routes = [route] else:
f.methods = methods routes = [route]
f.exposed = type routing['routes'] = routes
if getattr(f, "auth", None) is None: f.routing = routing
f.auth = auth
return f return f
return decorator return decorator
@ -388,11 +388,10 @@ def jsonrequest(f):
Use the ``route()`` decorator instead. Use the ``route()`` decorator instead.
""" """
f.combine = True
base = f.__name__.lstrip('/') base = f.__name__.lstrip('/')
if f.__name__ == "index": if f.__name__ == "index":
base = "" base = ""
return route([base, base + "/<path:_ignored_path>"], type="json", auth="user")(f) return route([base, base + "/<path:_ignored_path>"], type="json", auth="user", combine=True)(f)
class HttpRequest(WebRequest): class HttpRequest(WebRequest):
""" Regular GET/POST request """ Regular GET/POST request
@ -444,11 +443,10 @@ def httprequest(f):
Use the ``route()`` decorator instead. Use the ``route()`` decorator instead.
""" """
f.combine = True
base = f.__name__.lstrip('/') base = f.__name__.lstrip('/')
if f.__name__ == "index": if f.__name__ == "index":
base = "" base = ""
return route([base, base + "/<path:_ignored_path>"], type="http", auth="user")(f) return route([base, base + "/<path:_ignored_path>"], type="http", auth="user", combine=True)(f)
#---------------------------------------------------------- #----------------------------------------------------------
# Controller and route registration # Controller and route registration
@ -501,13 +499,22 @@ def routing_map(modules, nodb_only, converters=None):
o = cls() o = cls()
members = inspect.getmembers(o) members = inspect.getmembers(o)
for mk, mv in members: for mk, mv in members:
if inspect.ismethod(mv) and getattr(mv, 'exposed', False) and (not nodb_only or nodb_only == (mv.auth == "none")): if inspect.ismethod(mv) and hasattr(mv, 'routing'):
for url in mv.routes: routing = dict(type='http', auth='user', methods=None)
if getattr(mv, "combine", False): for claz in reversed(mv.im_class.mro()):
url = o._cp_path.rstrip('/') + '/' + url.lstrip('/') fn = getattr(claz, mv.func_name, None)
if url.endswith("/") and len(url) > 1: if fn and hasattr(fn, 'routing'):
url = url[: -1] routing.update(fn.routing)
routing_map.add(werkzeug.routing.Rule(url, endpoint=mv, methods=mv.methods)) 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 return routing_map
#---------------------------------------------------------- #----------------------------------------------------------

View File

@ -106,7 +106,7 @@ class WorkflowInstance(object):
for cur_instance_id, cur_model_name, cur_record_id in cr.fetchall(): for cur_instance_id, cur_model_name, cur_record_id in cr.fetchall():
cur_record = Record(cur_model_name, cur_record_id) cur_record = Record(cur_model_name, cur_record_id)
for act_name in act_names: 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 return ok