diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py
index f23f9e123b3..3845d916843 100644
--- a/openerp/addons/base/ir/ir_ui_view.py
+++ b/openerp/addons/base/ir/ir_ui_view.py
@@ -18,6 +18,7 @@
# along with this program. If not, see .
#
##############################################################################
+import collections
import copy
import logging
import os
@@ -25,7 +26,8 @@ import sys
import re
import time
-from lxml import etree
+from lxml import etree, html
+from functools import partial
from openerp import tools
from openerp.osv import fields, osv, orm
@@ -251,24 +253,16 @@ class view(osv.osv):
return node
return None
- def inherit_branding(self, specs_tree, view_id, base_xpath=None, count=None):
- if not count:
- count = {}
+ def inherit_branding(self, specs_tree, view_id):
for node in specs_tree:
- try:
- count[node.tag] = count.get(node.tag, 0) + 1
- xpath = "%s/%s[%s]" % (base_xpath or '', node.tag, count.get(node.tag))
- if node.tag == 'data' or node.tag == 'xpath':
- node = self.inherit_branding(node, view_id, xpath, count)
- else:
- node.attrib.update({
- 'data-oe-model': 'ir.ui.view',
- 'data-oe-id': str(view_id),
- 'data-oe-field': 'arch',
- 'data-oe-xpath': xpath
- })
- except Exception,e:
- print "inherit branding error",e,xpath,node.tag
+ xpath = node.getroottree().getpath(node)
+ if node.tag == 'data' or node.tag == 'xpath':
+ self.inherit_branding(node, view_id)
+ else:
+ node.set('data-oe-id', str(view_id))
+ node.set('data-oe-xpath', xpath)
+ node.set('data-oe-model', 'ir.ui.view')
+ node.set('data-oe-field', 'arch')
return specs_tree
@@ -691,37 +685,62 @@ class view(osv.osv):
r = self.read_combined(cr, uid, id_, fields=['arch'], context=context)
return r['arch']
- def distribute_branding(self, e, branding=None, xpath=None, count=None):
- if e.attrib.get('t-ignore') or e.tag in ('head',):
+ def distribute_branding(self, e, branding=None, parent_xpath='',
+ index_map=misc.ConstantMapping(1)):
+ if e.get('t-ignore') or e.tag == 'head':
# TODO: find a better name and check if we have a string to boolean helper
return
- branding_copy = ['data-oe-model','data-oe-id','data-oe-field','data-oe-xpath']
- branding_dist = {}
- xpath = "%s/%s[%s]" % (xpath or '', e.tag, (count and count.get(e.tag)) or 1)
- if branding and not (e.attrib.get('data-oe-model') or e.attrib.get('t-field')):
+
+ node_path = e.get('data-oe-xpath')
+ if node_path is None:
+ node_path = "%s/%s[%d]" % (parent_xpath, e.tag, index_map[e.tag])
+ if branding and not (e.get('data-oe-model') or e.get('t-field')):
e.attrib.update(branding)
- e.attrib['data-oe-xpath'] = xpath
- if e.attrib.get('data-oe-model'):
- # if a branded tag containg branded tag distribute to the childs
- child_text = "".join([etree.tostring(x, encoding='utf-8') for x in e])
- if re.search('( data-oe-model=| t-esc=| t-raw=| t-field=| t-call=| t-ignore=)',child_text) or e.tag == "t" or 't-raw' in e.attrib:
- for i in branding_copy:
- if e.attrib.get(i):
- branding_dist[i] = e.attrib.get(i)
- e.attrib.pop(i)
- if 't-raw' not in e.attrib:
- count = {}
- for child in e:
- count[child.tag] = count.get(child.tag, 0) + 1
- self.distribute_branding(child, branding_dist, xpath, count)
+ e.set('data-oe-xpath', node_path)
+ if not e.get('data-oe-model'): return
+
+ # if a branded element contains branded elements distribute own
+ # branding to children unless it's t-raw, then just remove branding
+ # on current element
+ if e.tag == 't' or 't-raw' in e.attrib or \
+ any(self.is_node_branded(child) for child in e.iterdescendants()):
+ distributed_branding = dict(
+ (attribute, e.attrib.pop(attribute))
+ for attribute in MOVABLE_BRANDING
+ if e.get(attribute))
+
+ if 't-raw' not in e.attrib:
+ # TODO: collections.Counter if remove p2.6 compat
+ # running index by tag type, for XPath query generation
+ indexes = collections.defaultdict(lambda: 0)
+ for child in e.iterchildren(tag=etree.Element):
+ indexes[child.tag] += 1
+ self.distribute_branding(child, distributed_branding,
+ parent_xpath=node_path,
+ index_map=indexes)
+
+ def is_node_branded(self, node):
+ """ Finds out whether a node is branded or qweb-active (bears a
+ @data-oe-model or a @t-* *which is not t-field* as t-field does not
+ section out views)
+
+ :param node: an etree-compatible element to test
+ :type node: etree._Element
+ :rtype: boolean
+ """
+ return any(
+ (attr == 'data-oe-model' or (attr != 't-field' and attr.startswith('t-')))
+ for attr in node.attrib
+ )
def render(self, cr, uid, id_or_xml_id, values, context=None):
def loader(name):
arch = self.read_template(cr, uid, name, context=context)
arch_tree = etree.fromstring(arch)
self.distribute_branding(arch_tree)
- arch = etree.tostring(arch_tree, encoding='utf-8')
- arch = '%s' % (arch)
+ root = etree.Element('tpl')
+ root.append(arch_tree)
+ arch = etree.tostring(root, encoding='utf-8', xml_declaration=True)
return arch
engine = qweb.QWebXml(loader=loader, undefined_handler=lambda key, v: None)
@@ -804,6 +823,7 @@ class view(osv.osv):
})
return super(view, self).copy(cr, uid, id, default, context=context)
+MOVABLE_BRANDING = ['data-oe-model','data-oe-id','data-oe-field','data-oe-xpath']
class view_sc(osv.osv):
_name = 'ir.ui.view_sc'
diff --git a/openerp/addons/base/tests/test_views.py b/openerp/addons/base/tests/test_views.py
index 6feba4df672..b83f824bfb0 100644
--- a/openerp/addons/base/tests/test_views.py
+++ b/openerp/addons/base/tests/test_views.py
@@ -3,7 +3,6 @@ from lxml import etree as ET
from lxml.builder import E
from openerp.tests import common
-import unittest2
Field = E.field
diff --git a/openerp/tools/convert.py b/openerp/tools/convert.py
index 6df22828b84..0b65cfc63eb 100644
--- a/openerp/tools/convert.py
+++ b/openerp/tools/convert.py
@@ -49,7 +49,7 @@ except:
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
-from lxml import etree
+from lxml import etree, builder
import misc
from config import config
from translate import _
@@ -854,33 +854,33 @@ form: module.record_id""" % (xml_id,)
module, tpl_id = tpl_id.split('.', 1)
# set the full template name for qweb .
if not (el.get('inherit_id') or el.get('inherit_option_id')):
- el.attrib['t-name'] = '%s.%s' % (module, tpl_id)
+ el.set('t-name', '%s.%s' % (module, tpl_id))
el.tag = 't'
else:
el.tag = 'data'
el.attrib.pop('id', None)
- record = etree.Element('record')
record_attrs = {
'id': tpl_id,
'model': 'ir.ui.view',
}
for att in ['forcecreate', 'context']:
- if att in el.attrib:
+ if att in el.keys():
record_attrs[att] = el.attrib.pop(att)
- record.attrib.update(record_attrs)
+ Field = builder.E.field
name = el.get('name', tpl_id)
- record.append(etree.fromstring('%s' % name))
- record.append(etree.fromstring('qweb'))
- record.append(etree.fromstring(''))
- record[-1].append(el)
- for key in ('inherit_id','inherit_option_id'):
- if el.get(key):
- record.append(etree.fromstring('' % (key, el.get(key))))
- el.attrib.pop(key, None)
- if el.get('page'):
- record.append(etree.Element('field', name="page", eval="True"))
+
+ record = etree.Element('record', attrib=record_attrs)
+ record.append(Field(name, name='name'))
+ record.append(Field("qweb", name='type'))
+ record.append(Field(el, name="arch", type="xml"))
+ for field_name in ('inherit_id','inherit_option_id'):
+ value = el.attrib.pop(field_name, None)
+ if value: record.append(Field(name=field_name, ref=value))
+ if el.attrib.pop('page', None) == 'True':
+ record.append(Field(name="page", eval="True"))
+
return self._tag_record(cr, record, data_node)
def id_get(self, cr, id_str):
diff --git a/openerp/tools/misc.py b/openerp/tools/misc.py
index 65e7c38e536..bdbb4d13e0f 100644
--- a/openerp/tools/misc.py
+++ b/openerp/tools/misc.py
@@ -35,7 +35,7 @@ import sys
import threading
import time
import zipfile
-from collections import defaultdict
+from collections import defaultdict, Mapping
from datetime import datetime
from itertools import islice, izip, groupby
from lxml import etree
@@ -1066,4 +1066,32 @@ def stripped_sys_argv(*strip_args):
return [x for i, x in enumerate(args) if not strip(args, i)]
+class ConstantMapping(Mapping):
+ """
+ An immutable mapping returning the provided value for every single key.
+
+ Useful for default value to methods
+ """
+ __slots__ = ['_value']
+ def __init__(self, val):
+ self._value = val
+
+ def __len__(self):
+ """
+ defaultdict updates its length for each individually requested key, is
+ that really useful?
+ """
+ return 0
+
+ def __iter__(self):
+ """
+ same as len, defaultdict udpates its iterable keyset with each key
+ requested, is there a point for this?
+ """
+ return iter([])
+
+ def __getitem__(self, item):
+ return self._value
+
+
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/openerp/tools/qweb.py b/openerp/tools/qweb.py
index 7919de8d916..86a69e2b52b 100644
--- a/openerp/tools/qweb.py
+++ b/openerp/tools/qweb.py
@@ -1,12 +1,10 @@
import cgi
import logging
import re
-import types
#from openerp.tools.safe_eval import safe_eval as eval
import xml # FIXME use lxml
-import xml.dom.minidom
import traceback
from openerp.osv import osv, orm
@@ -170,7 +168,7 @@ class QWebXml(object):
t_att = {}
for (an, av) in e.attributes.items():
an = str(an)
- if isinstance(av, types.UnicodeType):
+ if isinstance(av, unicode):
av = av.encode("utf8")
else:
av = av.nodeValue.encode("utf8")
@@ -262,7 +260,7 @@ class QWebXml(object):
var = t_att.get('as', expr).replace('.', '_')
d = QWebContext(v.copy(), self.undefined_handler)
size = -1
- if isinstance(enum, (types.ListType, types.TupleType)):
+ if isinstance(enum, (list, tuple)):
size = len(enum)
elif hasattr(enum, 'count'):
size = enum.count()
@@ -283,7 +281,7 @@ class QWebXml(object):
d["%s_parity" % var] = 'even'
if 'as' in t_att:
d[var] = i
- elif isinstance(i, types.DictType):
+ elif isinstance(i, dict):
d.update(i)
ru.append(self.render_element(e, t_att, g_att, d))
index += 1
@@ -316,35 +314,51 @@ class QWebXml(object):
def render_tag_field(self, e, t_att, g_att, v):
""" eg: +1 555 555 8069"""
+ node_name = e.nodeName
+ assert node_name not in ("table", "tbody", "thead", "tfoot", "tr", "td"),\
+ "RTE widgets do not work correctly on %r elements" % node_name
record, field = t_att["field"].rsplit('.', 1)
record = self.eval_object(record, v)
- inner = ""
- field_type = record._model._all_columns.get(field).column._type
+ inner = None
+ field_type = record._model._all_columns[field].column._type
try:
if field_type == 'many2one':
field_data = record.read([field])[0].get(field)
- inner = field_data and field_data[1] or ""
+ inner = field_data and field_data[1]
else:
- inner = getattr(record, field) or ""
- if isinstance(inner, types.UnicodeType):
+ inner = getattr(record, field)
+
+ if isinstance(inner, unicode):
inner = inner.encode("utf8")
- if field_type != 'html':
- cgi.escape(str(inner))
- if e.tagName != 't':
- g_att += ''.join(
- ' %s="%s"' % (name, cgi.escape(str(value), True))
- for name, value in [
- ('data-oe-model', record._model._name),
- ('data-oe-id', str(record.id)),
- ('data-oe-field', field),
- ('data-oe-type', field_type),
- ]
- )
+
+ if node_name == 't':
+ e.nodeName = DEFAULT_TAG_BY_TYPE[field_type]
+
+ g_att += ''.join(
+ ' %s="%s"' % (name, cgi.escape(str(value), True))
+ for name, value in [
+ ('data-oe-model', record._model._name),
+ ('data-oe-id', str(record.id)),
+ ('data-oe-field', field),
+ ('data-oe-type', field_type),
+ ('data-oe-expression', t_att['field']),
+ ]
+ )
except AttributeError:
_logger.warning("t-field no field %s for model %s", field, record._model._name)
- return self.render_element(e, t_att, g_att, v, str(inner))
+ return self.render_element(e, t_att, g_att, v, str(inner or ""))
+
+# If a t-field is set on a element, by default only its text will be
+# rendered losing the information that it's a tag and completely breaking
+# edition => replace by some default tag depending on field type
+DEFAULT_TAG_BY_TYPE = {
+ 'integer': 'span',
+ 'float': 'span',
+ 'char': 'span',
+ 'many2one': 'span',
+}
# leave this, al.