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.