[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
This commit is contained in:
Xavier Morel 2013-08-12 09:45:09 +02:00
parent 2a2398eaf9
commit db5e36efa3
2 changed files with 207 additions and 38 deletions

View File

@ -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):

View File

@ -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"}))
))
)
)