From 4cc13f062571133d086121ac1fdb2d8bf9e96c7c Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 2 Aug 2013 12:42:08 +0200 Subject: [PATCH 01/28] [IMP] branding distribution * don't serialize subtrees to string all the time * don't add XML root and prefix by string munging * extract trigger and moved branding attributes to constants * reduce unnecessary nesting * don't brand sub-elements of head bzr revid: xmo@openerp.com-20130802104208-rirrsgtxp2lydd7m --- openerp/addons/base/ir/ir_ui_view.py | 43 ++++++++++++++++------------ 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 4743b6888da..96c56cdbd4a 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -714,36 +714,39 @@ class view(osv.osv): 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',): + if e.attrib.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) + xpath = "%s/%s[%s]" % (xpath or '', e.tag, count[e.tag] if count else 1) if branding and not (e.attrib.get('data-oe-model') or e.attrib.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) + if not e.attrib.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(BRANDING_TRIGGERS.intersection(child.attrib) for child in e.iterdescendants()): + distributed_branding = dict( + (attribute, e.attrib.pop(attribute)) + for attribute in MOVABLE_BRANDING + if e.attrib.get(attribute)) + + if 't-raw' not in e.attrib: + # running index by tag type, for XPath query generation + count = {} + for child in e: + count[child.tag] = count.get(child.tag, 0) + 1 + self.distribute_branding(child, distributed_branding, xpath, count) 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) @@ -826,6 +829,8 @@ 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'] +BRANDING_TRIGGERS = frozenset(('data-oe-model', 't-esc', 't-raw', 't-field', 't-call', 't-ignore')) class view_sc(osv.osv): _name = 'ir.ui.view_sc' From bfd97c110f9726a4e665e54670c5dffffb194edd Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 2 Aug 2013 13:46:06 +0200 Subject: [PATCH 02/28] [FIX] typo in attribute name, don't distribute on t-field bzr revid: xmo@openerp.com-20130802114606-ef0pddt2uhc4ghlj --- openerp/addons/base/ir/ir_ui_view.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 96c56cdbd4a..f6723d64a03 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -726,7 +726,9 @@ class view(osv.osv): # 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(BRANDING_TRIGGERS.intersection(child.attrib) for child in e.iterdescendants()): + if e.tag == 't' or 't-raw' in e.attrib or\ + any(BRANDING_TRIGGERS.intersection(child.attrib) + for child in e.iterdescendants()): distributed_branding = dict( (attribute, e.attrib.pop(attribute)) for attribute in MOVABLE_BRANDING @@ -830,7 +832,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'] -BRANDING_TRIGGERS = frozenset(('data-oe-model', 't-esc', 't-raw', 't-field', 't-call', 't-ignore')) +BRANDING_TRIGGERS = frozenset(('data-oe-model', 't-esc', 't-raw', 't-call', 't-ignore')) class view_sc(osv.osv): _name = 'ir.ui.view_sc' From 2e7dab82cb56babb913fd4892364197b4b20f89b Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 9 Aug 2013 15:44:44 +0200 Subject: [PATCH 03/28] [IMP] don't use dict.get when not handling None result anyway bzr revid: xmo@openerp.com-20130809134444-ijl0u4glzuufylby --- openerp/tools/qweb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/tools/qweb.py b/openerp/tools/qweb.py index cc93df61208..111a7cd1186 100644 --- a/openerp/tools/qweb.py +++ b/openerp/tools/qweb.py @@ -283,7 +283,7 @@ class QWebXml(object): record = self.eval_object(record, v) inner = "" - field_type = record._model._all_columns.get(field).column._type + field_type = record._model._all_columns[field].column._type try: if field_type == 'many2one': field_data = record.read([field])[0].get(field) From 493bfc9374d7c9adfd7dcf9625dc1d6024e7cb77 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 9 Aug 2013 15:45:24 +0200 Subject: [PATCH 04/28] [FIX] omagerd don't completely misuse lxml in retarded ways bzr revid: xmo@openerp.com-20130809134524-wldxjgda13sl21fa --- openerp/tools/convert.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/openerp/tools/convert.py b/openerp/tools/convert.py index deed121604f..5447aa2d4a0 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 _ @@ -850,7 +850,6 @@ form: module.record_id""" % (xml_id,) el.attrib.pop('id', None) el.tag = 't' - record = etree.Element('record') record_attrs = { 'id': tpl_id, 'model': 'ir.ui.view', @@ -859,11 +858,10 @@ form: module.record_id""" % (xml_id,) if att in el.attrib: record_attrs[att] = el.attrib.pop(att) - record.attrib.update(record_attrs) - record.append(etree.fromstring('%s' % tpl_id)) - record.append(etree.fromstring('qweb')) - record.append(etree.fromstring('')) - record[-1].append(el) + record = etree.Element('record', attrib=record_attrs) + record.append(builder.E.field(str(tpl_id), name='name')) + record.append(builder.E.field("qweb", name='type')) + record.append(builder.E.field(el, name="arch", type="xml")) return self._tag_record(cr, record, data_node) def id_get(self, cr, id_str): From 45d5d2f0e1e5f496a2b8e273b1e08beaac7a5875 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 9 Aug 2013 15:47:46 +0200 Subject: [PATCH 05/28] [IMP] tpl_id should already be a string bzr revid: xmo@openerp.com-20130809134746-3mgio3vaedt93l3u --- openerp/tools/convert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/tools/convert.py b/openerp/tools/convert.py index 5447aa2d4a0..13a0f1ab0cf 100644 --- a/openerp/tools/convert.py +++ b/openerp/tools/convert.py @@ -859,7 +859,7 @@ form: module.record_id""" % (xml_id,) record_attrs[att] = el.attrib.pop(att) record = etree.Element('record', attrib=record_attrs) - record.append(builder.E.field(str(tpl_id), name='name')) + record.append(builder.E.field(tpl_id, name='name')) record.append(builder.E.field("qweb", name='type')) record.append(builder.E.field(el, name="arch", type="xml")) return self._tag_record(cr, record, data_node) From 2a2398eaf90c6a76f9ecc20fef2fb029c1e5bb84 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 9 Aug 2013 15:51:41 +0200 Subject: [PATCH 06/28] [IMP] lxml doc recommends using .get/.set/.keys/.values/.items for attributes manipulation where possible bzr revid: xmo@openerp.com-20130809135141-114l6262wdts0i43 --- openerp/tools/convert.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openerp/tools/convert.py b/openerp/tools/convert.py index 13a0f1ab0cf..6c776dcce54 100644 --- a/openerp/tools/convert.py +++ b/openerp/tools/convert.py @@ -846,7 +846,7 @@ form: module.record_id""" % (xml_id,) if '.' in tpl_id: module, tpl_id = tpl_id.split('.', 1) # set the full template name for qweb . - el.attrib['t-name'] = '%s.%s' % (module, tpl_id) + el.set('t-name', '%s.%s' % (module, tpl_id)) el.attrib.pop('id', None) el.tag = 't' @@ -855,7 +855,7 @@ form: module.record_id""" % (xml_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 = etree.Element('record', attrib=record_attrs) From db5e36efa326be7ddb3bfface4ae7fb950462eaf Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 12 Aug 2013 09:45:09 +0200 Subject: [PATCH 07/28] [FIX] rewrite ir_ui_view.save with new semantics, xpath fixes since lxml provides built-in tools to generate the path for a node in a tree, don't reimplement it manually bzr revid: xmo@openerp.com-20130812074509-yopeb4pxtsads4d9 --- openerp/addons/base/ir/ir_ui_view.py | 93 +++++++++------ openerp/addons/base/tests/test_views.py | 152 +++++++++++++++++++++++- 2 files changed, 207 insertions(+), 38 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index f6723d64a03..27bcf4a0288 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -26,8 +26,7 @@ import sys import re import time -import lxml.html -from lxml import etree +from lxml import etree, html from functools import partial from openerp import tools @@ -161,24 +160,58 @@ class view(osv.osv): return super(view, self).write(cr, uid, ids, vals, context) - def save(self, cr, uid, model, res_id, field, value, xpath=None, context=None): - """ Update the content of a field + def extract_embedded_fields(self, cr, uid, arch, context=None): + return arch.xpath('//*[@data-oe-model]') + + def save_embedded_field(self, cr, uid, el, context=None): + embedded_id = int(el.get('data-oe-id')) + # FIXME: type conversions + self.pool[el.get('data-oe-model')].write(cr, uid, embedded_id, { + el.get('data-oe-field'): el.text + }, context=context) + + def to_field_ref(self, cr, uid, el, context=None): + # FIXME: better ref? + return html.html_parser.makeelement(el.tag, attrib={ + 't-field': 'registry[%(model)r].browse(cr, uid, %(id)r).%(field)s' % { + 'model': el.get('data-oe-model'), + 'id': int(el.get('data-oe-id')), + 'field': el.get('data-oe-field'), + } + }) + + def replace_arch_section(self, cr, uid, view_id, section_xpath, replacement, context=None): + arch = replacement + if section_xpath: + previous_arch = etree.fromstring(self.browse(cr, uid, view_id, context=context).arch.encode('utf-8')) + # ensure there's only one match + [previous_section] = previous_arch.xpath(section_xpath) + previous_section.getparent().replace(previous_section, replacement) + arch = previous_arch + return arch + + def save(self, cr, uid, res_id, value, xpath=None, context=None): + """ Update a view section. The view section may embed fields to write :param str model: :param int res_id: :param str xpath: valid xpath to the tag to replace """ - model_obj = self.pool.get(model) - if xpath: - origin = model_obj.read(cr, uid, [res_id], [field], context=context)[0][field] - origin_tree = etree.fromstring(origin.encode('utf-8')) - zone = origin_tree.xpath(xpath)[0] - zone.getparent().replace(zone, lxml.html.fromstring(value)) - value = etree.tostring(origin_tree, encoding='utf-8') + res_id = int(res_id) - model_obj.write(cr, uid, res_id, {field: value}, context=context) + arch_section = etree.fromstring(value) + + for el in self.extract_embedded_fields(cr, uid, arch_section, context=context): + self.save_embedded_field(cr, uid, el, context=context) + + # transform embedded field back to t-field + el.getparent().replace(el, self.to_field_ref(cr, uid, el, context=context)) + + arch = self.replace_arch_section(cr, uid, res_id, xpath, arch_section, context=context) + self.write(cr, uid, res_id, { + 'arch': etree.tostring(arch, encoding='utf-8').decode('utf-8') + }, 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: @@ -272,24 +305,14 @@ 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) return specs_tree @@ -713,14 +736,13 @@ 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): + def distribute_branding(self, e, branding=None): if e.attrib.get('t-ignore') or e.tag == 'head': # TODO: find a better name and check if we have a string to boolean helper return - xpath = "%s/%s[%s]" % (xpath or '', e.tag, count[e.tag] if count else 1) if branding and not (e.attrib.get('data-oe-model') or e.attrib.get('t-field')): e.attrib.update(branding) - e.attrib['data-oe-xpath'] = xpath + e.attrib['data-oe-xpath'] = e.getroottree().getpath(e) if not e.attrib.get('data-oe-model'): return # if a branded element contains branded elements distribute own @@ -735,11 +757,8 @@ class view(osv.osv): if e.attrib.get(attribute)) if 't-raw' not in e.attrib: - # running index by tag type, for XPath query generation - count = {} for child in e: - count[child.tag] = count.get(child.tag, 0) + 1 - self.distribute_branding(child, distributed_branding, xpath, count) + self.distribute_branding(child, distributed_branding) def render(self, cr, uid, id_or_xml_id, values, context=None): def loader(name): diff --git a/openerp/addons/base/tests/test_views.py b/openerp/addons/base/tests/test_views.py index 6feba4df672..1a35755bbac 100644 --- a/openerp/addons/base/tests/test_views.py +++ b/openerp/addons/base/tests/test_views.py @@ -1,6 +1,8 @@ # -*- encoding: utf-8 -*- -from lxml import etree as ET +import itertools +from lxml import etree as ET, html from lxml.builder import E +from lxml.html import builder as h from openerp.tests import common import unittest2 @@ -424,3 +426,151 @@ class TestNoModel(common.TransactionCase): 'Copyrighter, tous droits réservés')) self.assertEqual(fields, {}) +def attrs(**kwargs): + return dict(('data-oe-%s' % key, str(value)) for key, value in kwargs.iteritems()) +class TestViewSaving(common.TransactionCase): + def eq(self, a, b): + self.assertEqual(a.tag, b.tag) + self.assertEqual(a.attrib, b.attrib) + self.assertEqual(a.text, b.text) + self.assertEqual(a.tail, b.tail) + for ca, cb in itertools.izip_longest(a, b): + self.eq(ca, cb) + + def setUp(self): + super(TestViewSaving, self).setUp() + self.arch = h.DIV( + h.DIV( + h.H3("Column 1"), + h.UL( + h.LI("Item 1"), + h.LI("Item 2"), + h.LI("Item 3"))), + h.DIV( + h.H3("Column 2"), + h.UL( + h.LI("Item 1"), + h.LI(h.SPAN("My Company", attrs(model='res.company', id=1, field='name'))), + h.LI(h.SPAN("+00 00 000 00 0 000", attrs(model='res.company', id=1, field='phone'))) + )) + ) + self.view_id = self.registry('ir.ui.view').create(self.cr, self.uid, { + 'name': "Test View", + 'type': 'qweb', + 'arch': ET.tostring(self.arch, encoding='utf-8').decode('utf-8') + }) + + def test_embedded_extraction(self): + fields = self.registry('ir.ui.view').extract_embedded_fields( + self.cr, self.uid, self.arch, context=None) + + expect = [ + h.SPAN("My Company", attrs(model='res.company', id=1, field='name')), + h.SPAN("+00 00 000 00 0 000", attrs(model='res.company', id=1, field='phone')), + ] + for actual, expected in itertools.izip_longest(fields, expect): + self.eq(actual, expected) + + def test_embedded_save(self): + embedded = h.SPAN("+00 00 000 00 0 000", attrs(model='res.company', id=1, field='phone')) + + self.registry('ir.ui.view').save_embedded_field(self.cr, self.uid, embedded) + + company = self.registry('res.company').browse(self.cr, self.uid, 1) + self.assertEqual(company.phone, "+00 00 000 00 0 000") + + @unittest2.skip("save conflict for embedded (saved by third party or previous version in page) not implemented") + def test_embedded_conflict(self): + e1 = h.SPAN("My Company", attrs(model='res.company', id=1, field='name')) + e2 = h.SPAN("Leeroy Jenkins", attrs(model='res.company', id=1, field='name')) + + View = self.registry('ir.ui.view') + + View.save_embedded_field(self.cr, self.uid, e1) + # FIXME: more precise exception + with self.assertRaises(Exception): + View.save_embedded_field(self.cr, self.uid, e2) + + def test_embedded_to_field_ref(self): + View = self.registry('ir.ui.view') + embedded = h.SPAN("My Company", attrs(model='res.company', id=1, field='name')) + self.eq( + View.to_field_ref(self.cr, self.uid, embedded, context=None), + h.SPAN({'t-field': 'registry[%r].browse(cr, uid, %r).%s' % ( + 'res.company', 1, 'name' + )}) + ) + + def test_replace_arch(self): + replacement = h.P("Wheee") + + result = self.registry('ir.ui.view').replace_arch_section( + self.cr, self.uid, self.view_id, None, replacement) + + self.eq(result, replacement) + + def test_fixup_arch(self): + replacement = h.H1("I am the greatest title alive!") + + result = self.registry('ir.ui.view').replace_arch_section( + self.cr, self.uid, self.view_id, '/div/div[1]/h3', + replacement) + + self.eq(result, h.DIV( + h.DIV( + h.H1("I am the greatest title alive!"), + h.UL( + h.LI("Item 1"), + h.LI("Item 2"), + h.LI("Item 3"))), + h.DIV( + h.H3("Column 2"), + h.UL( + h.LI("Item 1"), + h.LI(h.SPAN("My Company", attrs(model='res.company', id=1, field='name'))), + h.LI(h.SPAN("+00 00 000 00 0 000", attrs(model='res.company', id=1, field='phone'))) + )) + )) + + def test_multiple_xpath_matches(self): + with self.assertRaises(ValueError): + self.registry('ir.ui.view').replace_arch_section( + self.cr, self.uid, self.view_id, '/div/div/h3', + h.H6("Lol nope")) + + def test_save(self): + Company = self.registry('res.company') + View = self.registry('ir.ui.view') + + replacement = ET.tostring(h.DIV( + h.H3("Column 2"), + h.UL( + h.LI("wob wob wob"), + h.LI(h.SPAN("Acme Corporation", attrs(model='res.company', id=1, field='name'))), + h.LI(h.SPAN("+12 3456789", attrs(model='res.company', id=1, field='phone'))), + ) + ), encoding='utf-8') + View.save(self.cr, self.uid, res_id=self.view_id, value=replacement, + xpath='/div/div[2]') + + company = Company.browse(self.cr, self.uid, 1) + self.assertEqual(company.name, "Acme Corporation") + self.assertEqual(company.phone, "+12 3456789") + self.eq( + ET.fromstring(View.browse(self.cr, self.uid, self.view_id).arch.encode('utf-8')), + h.DIV( + h.DIV( + h.H3("Column 1"), + h.UL( + h.LI("Item 1"), + h.LI("Item 2"), + h.LI("Item 3"))), + h.DIV( + h.H3("Column 2"), + h.UL( + h.LI("wob wob wob"), + h.LI(h.SPAN({'t-field': "registry['res.company'].browse(cr, uid, 1).name"})), + h.LI(h.SPAN({'t-field': "registry['res.company'].browse(cr, uid, 1).phone"})) + )) + ) + ) From fcdbfe364d3e3a0033453e50835ed1abcda0b723 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 12 Aug 2013 09:50:44 +0200 Subject: [PATCH 08/28] [IMP] dict.get's default is None, remove pointless explicit defaults bzr revid: xmo@openerp.com-20130812075044-ko8hozdnz2b5c3vk --- openerp/tools/convert.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openerp/tools/convert.py b/openerp/tools/convert.py index 4fccacbb743..645988352d7 100644 --- a/openerp/tools/convert.py +++ b/openerp/tools/convert.py @@ -846,7 +846,7 @@ form: module.record_id""" % (xml_id,) if '.' in tpl_id: module, tpl_id = tpl_id.split('.', 1) # set the full template name for qweb . - if (not el.get('inherit_id', False)) and (not el.get('inherit_option_id', False)): + if not (el.get('inherit_id') or el.get('inherit_option_id')): el.attrib['t-name'] = '%s.%s' % (module, tpl_id) el.tag = 't' else: @@ -869,7 +869,7 @@ form: module.record_id""" % (xml_id,) record.append(etree.fromstring('')) record[-1].append(el) for key in ('inherit_id','inherit_option_id'): - if el.get(key, False): + if el.get(key): record.append(etree.fromstring('' % (key, el.get(key)))) el.attrib.pop(key, None) return self._tag_record(cr, record, data_node) From de952ae35269fb1cae6d875ad935892e849e83f5 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 12 Aug 2013 14:11:21 +0200 Subject: [PATCH 09/28] [FIX] don't extract the root ir.ui.view as an embedded field bzr revid: xmo@openerp.com-20130812121121-e3ch0b2u5j1u3ytp --- openerp/addons/base/ir/ir_ui_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index f2fcf0045fa..b10a8fbbbbf 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -162,7 +162,7 @@ class view(osv.osv): return super(view, self).write(cr, uid, ids, vals, context) def extract_embedded_fields(self, cr, uid, arch, context=None): - return arch.xpath('//*[@data-oe-model]') + return arch.xpath('//*[@data-oe-model != "ir.ui.view"]') def save_embedded_field(self, cr, uid, el, context=None): embedded_id = int(el.get('data-oe-id')) From 8bb10bdf6796e8f1662419815da2a8a4ab1403aa Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 10 Sep 2013 12:35:26 +0200 Subject: [PATCH 10/28] [FIX] incorrect conversion of some fields transmogrification in template tag bzr revid: xmo@openerp.com-20130910103526-r9q98zirvmiylr69 --- openerp/tools/convert.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openerp/tools/convert.py b/openerp/tools/convert.py index 5e16ae4cf69..81abfc5b5fe 100644 --- a/openerp/tools/convert.py +++ b/openerp/tools/convert.py @@ -869,11 +869,11 @@ form: module.record_id""" % (xml_id,) record.append(Field("qweb", name='type')) record.append(Field(el, name="arch", type="xml")) for field_name in ('inherit_id','inherit_option_id'): - if el.get(field_name): - record.append(Field( - field_name, el.attrib.pop(field_name, None))) - if el.get('page'): - record.append(etree.Element('field', name="page", eval="True")) + 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): From 21ae2e059ac47364e750f2315d9332f17232507b Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 10 Sep 2013 14:47:31 +0200 Subject: [PATCH 11/28] [REM] noop bzr revid: xmo@openerp.com-20130910124731-dx2hygjm6ixkw81j --- openerp/tools/qweb.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openerp/tools/qweb.py b/openerp/tools/qweb.py index 6182b3b99df..2250567ed5b 100644 --- a/openerp/tools/qweb.py +++ b/openerp/tools/qweb.py @@ -330,8 +330,7 @@ class QWebXml(object): inner = getattr(record, field) or "" if isinstance(inner, types.UnicodeType): 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)) From 420a65cdad63b9dd0b33ae83941637b2a5e1b66d Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 10 Sep 2013 14:50:15 +0200 Subject: [PATCH 12/28] [IMP] remove unneeded import, cases redundancy bzr revid: xmo@openerp.com-20130910125015-75tjdv1eopipwk4m --- openerp/tools/qweb.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openerp/tools/qweb.py b/openerp/tools/qweb.py index 2250567ed5b..acd1a2a0610 100644 --- a/openerp/tools/qweb.py +++ b/openerp/tools/qweb.py @@ -1,7 +1,6 @@ import cgi import logging import re -import types #from openerp.tools.safe_eval import safe_eval as eval @@ -320,15 +319,16 @@ class QWebXml(object): record, field = t_att["field"].rsplit('.', 1) record = self.eval_object(record, v) - inner = "" + 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 e.tagName != 't': @@ -344,6 +344,6 @@ class QWebXml(object): 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 "")) # leave this, al. From 85a3d8eaeb70283dccec7a24a788e2c31cd09ea4 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 10 Sep 2013 15:26:38 +0200 Subject: [PATCH 13/28] [FIX] moved view.save to website, forgot to move tests bzr revid: xmo@openerp.com-20130910132638-2v4ilmi5uuboazt9 --- openerp/addons/base/tests/test_views.py | 153 +----------------------- 1 file changed, 1 insertion(+), 152 deletions(-) diff --git a/openerp/addons/base/tests/test_views.py b/openerp/addons/base/tests/test_views.py index 1a35755bbac..b83f824bfb0 100644 --- a/openerp/addons/base/tests/test_views.py +++ b/openerp/addons/base/tests/test_views.py @@ -1,11 +1,8 @@ # -*- encoding: utf-8 -*- -import itertools -from lxml import etree as ET, html +from lxml import etree as ET from lxml.builder import E -from lxml.html import builder as h from openerp.tests import common -import unittest2 Field = E.field @@ -426,151 +423,3 @@ class TestNoModel(common.TransactionCase): 'Copyrighter, tous droits réservés')) self.assertEqual(fields, {}) -def attrs(**kwargs): - return dict(('data-oe-%s' % key, str(value)) for key, value in kwargs.iteritems()) -class TestViewSaving(common.TransactionCase): - def eq(self, a, b): - self.assertEqual(a.tag, b.tag) - self.assertEqual(a.attrib, b.attrib) - self.assertEqual(a.text, b.text) - self.assertEqual(a.tail, b.tail) - for ca, cb in itertools.izip_longest(a, b): - self.eq(ca, cb) - - def setUp(self): - super(TestViewSaving, self).setUp() - self.arch = h.DIV( - h.DIV( - h.H3("Column 1"), - h.UL( - h.LI("Item 1"), - h.LI("Item 2"), - h.LI("Item 3"))), - h.DIV( - h.H3("Column 2"), - h.UL( - h.LI("Item 1"), - h.LI(h.SPAN("My Company", attrs(model='res.company', id=1, field='name'))), - h.LI(h.SPAN("+00 00 000 00 0 000", attrs(model='res.company', id=1, field='phone'))) - )) - ) - self.view_id = self.registry('ir.ui.view').create(self.cr, self.uid, { - 'name': "Test View", - 'type': 'qweb', - 'arch': ET.tostring(self.arch, encoding='utf-8').decode('utf-8') - }) - - def test_embedded_extraction(self): - fields = self.registry('ir.ui.view').extract_embedded_fields( - self.cr, self.uid, self.arch, context=None) - - expect = [ - h.SPAN("My Company", attrs(model='res.company', id=1, field='name')), - h.SPAN("+00 00 000 00 0 000", attrs(model='res.company', id=1, field='phone')), - ] - for actual, expected in itertools.izip_longest(fields, expect): - self.eq(actual, expected) - - def test_embedded_save(self): - embedded = h.SPAN("+00 00 000 00 0 000", attrs(model='res.company', id=1, field='phone')) - - self.registry('ir.ui.view').save_embedded_field(self.cr, self.uid, embedded) - - company = self.registry('res.company').browse(self.cr, self.uid, 1) - self.assertEqual(company.phone, "+00 00 000 00 0 000") - - @unittest2.skip("save conflict for embedded (saved by third party or previous version in page) not implemented") - def test_embedded_conflict(self): - e1 = h.SPAN("My Company", attrs(model='res.company', id=1, field='name')) - e2 = h.SPAN("Leeroy Jenkins", attrs(model='res.company', id=1, field='name')) - - View = self.registry('ir.ui.view') - - View.save_embedded_field(self.cr, self.uid, e1) - # FIXME: more precise exception - with self.assertRaises(Exception): - View.save_embedded_field(self.cr, self.uid, e2) - - def test_embedded_to_field_ref(self): - View = self.registry('ir.ui.view') - embedded = h.SPAN("My Company", attrs(model='res.company', id=1, field='name')) - self.eq( - View.to_field_ref(self.cr, self.uid, embedded, context=None), - h.SPAN({'t-field': 'registry[%r].browse(cr, uid, %r).%s' % ( - 'res.company', 1, 'name' - )}) - ) - - def test_replace_arch(self): - replacement = h.P("Wheee") - - result = self.registry('ir.ui.view').replace_arch_section( - self.cr, self.uid, self.view_id, None, replacement) - - self.eq(result, replacement) - - def test_fixup_arch(self): - replacement = h.H1("I am the greatest title alive!") - - result = self.registry('ir.ui.view').replace_arch_section( - self.cr, self.uid, self.view_id, '/div/div[1]/h3', - replacement) - - self.eq(result, h.DIV( - h.DIV( - h.H1("I am the greatest title alive!"), - h.UL( - h.LI("Item 1"), - h.LI("Item 2"), - h.LI("Item 3"))), - h.DIV( - h.H3("Column 2"), - h.UL( - h.LI("Item 1"), - h.LI(h.SPAN("My Company", attrs(model='res.company', id=1, field='name'))), - h.LI(h.SPAN("+00 00 000 00 0 000", attrs(model='res.company', id=1, field='phone'))) - )) - )) - - def test_multiple_xpath_matches(self): - with self.assertRaises(ValueError): - self.registry('ir.ui.view').replace_arch_section( - self.cr, self.uid, self.view_id, '/div/div/h3', - h.H6("Lol nope")) - - def test_save(self): - Company = self.registry('res.company') - View = self.registry('ir.ui.view') - - replacement = ET.tostring(h.DIV( - h.H3("Column 2"), - h.UL( - h.LI("wob wob wob"), - h.LI(h.SPAN("Acme Corporation", attrs(model='res.company', id=1, field='name'))), - h.LI(h.SPAN("+12 3456789", attrs(model='res.company', id=1, field='phone'))), - ) - ), encoding='utf-8') - View.save(self.cr, self.uid, res_id=self.view_id, value=replacement, - xpath='/div/div[2]') - - company = Company.browse(self.cr, self.uid, 1) - self.assertEqual(company.name, "Acme Corporation") - self.assertEqual(company.phone, "+12 3456789") - self.eq( - ET.fromstring(View.browse(self.cr, self.uid, self.view_id).arch.encode('utf-8')), - h.DIV( - h.DIV( - h.H3("Column 1"), - h.UL( - h.LI("Item 1"), - h.LI("Item 2"), - h.LI("Item 3"))), - h.DIV( - h.H3("Column 2"), - h.UL( - h.LI("wob wob wob"), - h.LI(h.SPAN({'t-field': "registry['res.company'].browse(cr, uid, 1).name"})), - h.LI(h.SPAN({'t-field': "registry['res.company'].browse(cr, uid, 1).phone"})) - )) - ) - ) From 59734d3e349e195297a5aa5187fdc93a006fbac3 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 10 Sep 2013 15:44:02 +0200 Subject: [PATCH 14/28] [FIX] no need to use types for builtins (also fixes broken import, oops) bzr revid: xmo@openerp.com-20130910134402-1fj37o3pxtb4hrau --- openerp/tools/qweb.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openerp/tools/qweb.py b/openerp/tools/qweb.py index acd1a2a0610..4cd0c2717fa 100644 --- a/openerp/tools/qweb.py +++ b/openerp/tools/qweb.py @@ -5,7 +5,6 @@ import re #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 @@ -169,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") @@ -261,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() @@ -282,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 From 5bd3396023669f70f858aa9f2eb36cb78a22512a Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 10 Sep 2013 16:33:45 +0200 Subject: [PATCH 15/28] [TEMP] embed original expression in output to reverse correctly bzr revid: xmo@openerp.com-20130910143345-es9vbvxlfq5sra6h --- openerp/tools/qweb.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openerp/tools/qweb.py b/openerp/tools/qweb.py index 4cd0c2717fa..2ed268b0f5c 100644 --- a/openerp/tools/qweb.py +++ b/openerp/tools/qweb.py @@ -338,6 +338,7 @@ class QWebXml(object): ('data-oe-id', str(record.id)), ('data-oe-field', field), ('data-oe-type', field_type), + ('data-oe-expression', t_att['field']), ] ) except AttributeError: From b84c7c6ebcef11a87eb2be6e271f464587d88b1b Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 12 Sep 2013 16:05:32 +0200 Subject: [PATCH 16/28] [FIX] avoid yielding just text without markup bzr revid: xmo@openerp.com-20130912140532-qejpqr037x99qrln --- openerp/tools/qweb.py | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/openerp/tools/qweb.py b/openerp/tools/qweb.py index 2ed268b0f5c..0633baf8c40 100644 --- a/openerp/tools/qweb.py +++ b/openerp/tools/qweb.py @@ -330,20 +330,32 @@ class QWebXml(object): if isinstance(inner, unicode): inner = inner.encode("utf8") - 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), - ('data-oe-expression', t_att['field']), - ] - ) + if e.tagName == 't': + e.tagName = 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 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 = { + 'int': 'span', + 'float': 'span', + 'char': 'span', + 'many2one': 'span', +} + # leave this, al. From f4520d3f86feed2a8813d016e5942e862a2c80e5 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 13 Sep 2013 15:21:31 +0200 Subject: [PATCH 17/28] [FIX] mapping type is integer not int bzr revid: xmo@openerp.com-20130913132131-bs8auaq7bvz64dbf --- openerp/tools/qweb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/tools/qweb.py b/openerp/tools/qweb.py index 0633baf8c40..a9a379c21fd 100644 --- a/openerp/tools/qweb.py +++ b/openerp/tools/qweb.py @@ -352,7 +352,7 @@ class QWebXml(object): # 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 = { - 'int': 'span', + 'integer': 'span', 'float': 'span', 'char': 'span', 'many2one': 'span', From 5e66ecaf559d05979bce6b44f84c334a7dc8530d Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 17 Sep 2013 13:27:18 +0200 Subject: [PATCH 18/28] [FIX] serializer uses nodeName, not tagName. Set that bzr revid: xmo@openerp.com-20130917112718-rnbxcoq584rt08qp --- openerp/tools/qweb.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openerp/tools/qweb.py b/openerp/tools/qweb.py index a9a379c21fd..42ec8a5b408 100644 --- a/openerp/tools/qweb.py +++ b/openerp/tools/qweb.py @@ -330,8 +330,8 @@ class QWebXml(object): if isinstance(inner, unicode): inner = inner.encode("utf8") - if e.tagName == 't': - e.tagName = DEFAULT_TAG_BY_TYPE[field_type] + if e.nodeName == 't': + e.nodeName = DEFAULT_TAG_BY_TYPE[field_type] g_att += ''.join( ' %s="%s"' % (name, cgi.escape(str(value), True)) From 779667d6902ed59d84cfa32aa93623b471d0d087 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 17 Sep 2013 13:30:22 +0200 Subject: [PATCH 19/28] [IMP] add assertion to prevent which breaks RTE widgets bzr revid: xmo@openerp.com-20130917113022-idty6l0cwuox3s59 --- openerp/tools/qweb.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openerp/tools/qweb.py b/openerp/tools/qweb.py index 42ec8a5b408..86a69e2b52b 100644 --- a/openerp/tools/qweb.py +++ b/openerp/tools/qweb.py @@ -314,6 +314,9 @@ 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) @@ -330,7 +333,7 @@ class QWebXml(object): if isinstance(inner, unicode): inner = inner.encode("utf8") - if e.nodeName == 't': + if node_name == 't': e.nodeName = DEFAULT_TAG_BY_TYPE[field_type] g_att += ''.join( From 8159503eb155d4ea4c7f6a8c736b55370d4159e4 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 17 Sep 2013 17:44:14 +0200 Subject: [PATCH 20/28] [FIX] ensure we distribute branding only over actual element, not over e.g. comments bzr revid: xmo@openerp.com-20130917154414-zdbv7861e9p6mdgi --- openerp/addons/base/ir/ir_ui_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index cfabb3f5fd4..36a67bd165b 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -703,7 +703,7 @@ class view(osv.osv): if e.attrib.get(attribute)) if 't-raw' not in e.attrib: - for child in e: + for child in e.iterchildren(tag=etree.Element): self.distribute_branding(child, distributed_branding) def render(self, cr, uid, id_or_xml_id, values, context=None): From 47fac4836b3f53c155ea899321b31221041e335a Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 17 Sep 2013 17:45:05 +0200 Subject: [PATCH 21/28] [IMP] avoid using Element.attrib when unnecessary bzr revid: xmo@openerp.com-20130917154505-4kf77rqjrg1bncvk --- openerp/addons/base/ir/ir_ui_view.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 36a67bd165b..f5c197c5495 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -683,13 +683,13 @@ class view(osv.osv): return r['arch'] def distribute_branding(self, e, branding=None): - if e.attrib.get('t-ignore') or e.tag == 'head': + 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 - if branding and not (e.attrib.get('data-oe-model') or e.attrib.get('t-field')): + if branding and not (e.get('data-oe-model') or e.get('t-field')): e.attrib.update(branding) - e.attrib['data-oe-xpath'] = e.getroottree().getpath(e) - if not e.attrib.get('data-oe-model'): return + e.set('data-oe-xpath', e.getroottree().getpath(e)) + 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 @@ -700,7 +700,7 @@ class view(osv.osv): distributed_branding = dict( (attribute, e.attrib.pop(attribute)) for attribute in MOVABLE_BRANDING - if e.attrib.get(attribute)) + if e.get(attribute)) if 't-raw' not in e.attrib: for child in e.iterchildren(tag=etree.Element): From 4b89b246a8befcd5a3a513eda591bfa8e19a72c8 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 17 Sep 2013 18:37:08 +0200 Subject: [PATCH 22/28] [FIX] distribute branding on t-if, t-for and t-att bzr revid: xmo@openerp.com-20130917163708-u61ol5yqr3nrd9mu --- openerp/addons/base/ir/ir_ui_view.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index f5c197c5495..1bf642ac202 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -694,9 +694,8 @@ class view(osv.osv): # 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(BRANDING_TRIGGERS.intersection(child.attrib) - for child in e.iterdescendants()): + 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 @@ -706,6 +705,20 @@ class view(osv.osv): for child in e.iterchildren(tag=etree.Element): self.distribute_branding(child, distributed_branding) + 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) @@ -797,7 +810,6 @@ 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'] -BRANDING_TRIGGERS = frozenset(('data-oe-model', 't-esc', 't-raw', 't-call', 't-ignore')) class view_sc(osv.osv): _name = 'ir.ui.view_sc' From 724bbffc88e444b50c1622c942b8c4554646d367 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 19 Sep 2013 15:08:47 +0200 Subject: [PATCH 23/28] [FIX] Partially revert xmo@openerp.com-20130812074509-yopeb4pxtsads4d9 Change attempted to simplify branding distribution, but based node branding on final view. To be able to rewrite, node branding must be based on source views positions before applying inheritance. Reintroduce manual generation of xpath in distribute_branding as ElementTree.getpath() can't handle it. Note: why isn't branding distribution performed during initial views reading (e.g. inherit_branding)? Surely that would obviate the need for special casing and manually creating paths no? bzr revid: xmo@openerp.com-20130919130847-3t8zfpv9m9pcibg0 --- openerp/addons/base/ir/ir_ui_view.py | 20 ++++++++++++++++--- openerp/tools/misc.py | 30 +++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 1bf642ac202..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 @@ -260,6 +261,8 @@ class view(osv.osv): 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 @@ -682,13 +685,18 @@ 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): + 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 + + 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.set('data-oe-xpath', e.getroottree().getpath(e)) + e.set('data-oe-xpath', node_path) if not e.get('data-oe-model'): return # if a branded element contains branded elements distribute own @@ -702,8 +710,14 @@ class view(osv.osv): 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): - self.distribute_branding(child, distributed_branding) + 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 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: From be966b02c7bfa1553a58b42fa013f8ab9c1388eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Mon, 23 Sep 2013 13:29:39 +0200 Subject: [PATCH 24/28] [IMP] tools: mail: improved html_email_clean - 'read more' links are now in the last container, not after, allowing to have read more links embedded inside shortened paragraphs - added expand_options dict allowing to customize the read more bzr revid: tde@openerp.com-20130923112939-0ab4idlzqpug00xz --- openerp/tools/mail.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/openerp/tools/mail.py b/openerp/tools/mail.py index 7895f2c7192..9efcb851cbf 100644 --- a/openerp/tools/mail.py +++ b/openerp/tools/mail.py @@ -99,7 +99,7 @@ def html_sanitize(src, silent=True): # HTML Cleaner #---------------------------------------------------------- -def html_email_clean(html, remove=False, shorten=False, max_length=300): +def html_email_clean(html, remove=False, shorten=False, max_length=300, expand_options=None): """ html_email_clean: clean the html by doing the following steps: - try to strip email quotes, by removing blockquotes or having some client- @@ -170,6 +170,9 @@ def html_email_clean(html, remove=False, shorten=False, max_length=300): iteration += 1 new_node = _insert_new_node(node, -1, new_node_tag, text[idx:] + (cur_node.tail or ''), None, {}) + if expand_options is None: + expand_options = {} + if not html or not isinstance(html, basestring): return html html = ustr(html) @@ -252,14 +255,26 @@ def html_email_clean(html, remove=False, shorten=False, max_length=300): stop_idx = len(outertext) node.text = innertext + outertext[0:stop_idx] # create ... read more node - read_more_node = _create_node('span', ' ... ', None, {'class': 'oe_mail_expand'}) - read_more_link_node = _create_node('a', 'read more', None, {'href': '#', 'class': 'oe_mail_expand'}) + read_more_node = _create_node( + 'span', + ' ... ', + None, + {'class': expand_options.get('oe_expand_span_class', 'oe_mail_expand')} + ) + read_more_link_node = _create_node( + 'a', + 'read more', + None, + { + 'href': expand_options.get('oe_expand_href', '#'), + 'class': expand_options.get('oe_expand_a_class', 'oe_mail_expand'), + } + ) read_more_node.append(read_more_link_node) # create outertext node new_node = _create_node('span', outertext[stop_idx:]) # add newly created nodes in dom - node.addnext(new_node) - node.addnext(read_more_node) + node.append(read_more_node) # tag node new_node.set('in_overlength', '1') From e07531f4518fe02756433185a595e50e88f00f33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Mon, 23 Sep 2013 16:33:49 +0200 Subject: [PATCH 25/28] [FIX] don't try to set attributes on comments, it makes no sense bzr revid: tde@openerp.com-20130923143349-y922fxtbkotsjzpf --- openerp/addons/base/ir/ir_ui_view.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 3845d916843..79f8a562f47 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -254,7 +254,8 @@ class view(osv.osv): return None def inherit_branding(self, specs_tree, view_id): - for node in specs_tree: + print map(etree.tostring, specs_tree) + for node in specs_tree.iterchildren(tag=etree.Element): xpath = node.getroottree().getpath(node) if node.tag == 'data' or node.tag == 'xpath': self.inherit_branding(node, view_id) From 470179bf80e774e34c8c7e80229df3d7d9a6f114 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Mon, 23 Sep 2013 16:34:16 +0200 Subject: [PATCH 26/28] [ADD] wrapper node for more field types bzr revid: tde@openerp.com-20130923143416-hmxdll0gi0w0un2j --- openerp/tools/qweb.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openerp/tools/qweb.py b/openerp/tools/qweb.py index 86a69e2b52b..e68ea3b8a28 100644 --- a/openerp/tools/qweb.py +++ b/openerp/tools/qweb.py @@ -358,7 +358,14 @@ DEFAULT_TAG_BY_TYPE = { 'integer': 'span', 'float': 'span', 'char': 'span', + 'date': 'span', + 'datetime': 'span', + 'time': 'span', 'many2one': 'span', + + 'text': 'p', + + 'html': 'div', } # leave this, al. From 2939c58e72c517965cbf14bdca097a066bfadba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Mon, 23 Sep 2013 16:48:30 +0200 Subject: [PATCH 27/28] [CLEAN] ir_ui_view: removed print statement bzr revid: tde@openerp.com-20130923144830-i5iqzfc1pydm9pge --- openerp/addons/base/ir/ir_ui_view.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 79f8a562f47..84e466d0a4d 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -254,7 +254,6 @@ class view(osv.osv): return None def inherit_branding(self, specs_tree, view_id): - print map(etree.tostring, specs_tree) for node in specs_tree.iterchildren(tag=etree.Element): xpath = node.getroottree().getpath(node) if node.tag == 'data' or node.tag == 'xpath': From 71653438eb47d5b7468e3e4055184af493f28993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 24 Sep 2013 11:56:02 +0200 Subject: [PATCH 28/28] [IMP] res_partner: demo data: have one partner coming from LA, to be able to use this town in various demo data bzr revid: tde@openerp.com-20130924095602-u6q5u0c0jpnlc6nk --- openerp/addons/base/res/res_partner_demo.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openerp/addons/base/res/res_partner_demo.xml b/openerp/addons/base/res/res_partner_demo.xml index a8f0aa4eec7..a79623c5917 100644 --- a/openerp/addons/base/res/res_partner_demo.xml +++ b/openerp/addons/base/res/res_partner_demo.xml @@ -142,18 +142,18 @@ - Elec Import + OpenElec Applications 1 - Chicago - 60623 + 90001 + Los Angeles - + 23 Rockwell Lane - info@elecimport.com - +1 773 439 3000 + info@openelecapplications.com + +1 312 349 2121 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