[FIX] P3: hash collections iteration issues

See various commits for information

Closes #17111
This commit is contained in:
Xavier Morel
2017-06-12 12:08:39 +02:00
15 changed files with 174 additions and 138 deletions
@@ -14,11 +14,8 @@ class Company(models.Model):
'project.task', string="Leave Task",
domain="[('project_id', '=', leave_timesheet_project_id)]")
def _init_column(self, name):
super(Company, self)._init_column(name)
if name == 'leave_timesheet_project_id':
self.search([('leave_timesheet_project_id', '=', False)])._create_leave_project_task()
def init(self):
self.search([('leave_timesheet_project_id', '=', False)])._create_leave_project_task()
@api.model
def create(self, values):
+7 -7
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import OrderedDict
from odoo.addons.account.tests.account_test_classes import AccountingTestCase
@@ -27,11 +27,11 @@ class TestSale(AccountingTestCase):
'groups_id': [(6, 0, [group_user.id])]
})
# create quotation with differend kinds of products (all possible combinations)
self.products = {
'prod_order': self.env.ref('product.product_order_01'),
'prod_del': self.env.ref('product.product_delivery_01'),
'serv_order': self.env.ref('product.service_order_01'),
'serv_del': self.env.ref('product.service_delivery'),
}
self.products = OrderedDict([
('prod_order', self.env.ref('product.product_order_01')),
('serv_del', self.env.ref('product.service_delivery')),
('serv_order', self.env.ref('product.service_order_01')),
('prod_del', self.env.ref('product.product_delivery_01')),
])
self.partner = self.env.ref('base.res_partner_1')
+10 -8
View File
@@ -20,7 +20,7 @@ from odoo.http import request
from odoo.addons.website.models.website import slug
from odoo.addons.web.controllers.main import WebClient, Binary, Home
from odoo.tools import pycompat
from odoo.tools import pycompat, OrderedSet
logger = logging.getLogger(__name__)
@@ -34,27 +34,29 @@ class QueryURL(object):
def __init__(self, path='', path_args=None, **args):
self.path = path
self.args = args
self.path_args = set(path_args or [])
self.path_args = OrderedSet(path_args or [])
def __call__(self, path=None, path_args=None, **kw):
path = path or self.path
for key, value in pycompat.items(self.args):
kw.setdefault(key, value)
path_args = set(path_args or []).union(self.path_args)
paths, fragments = [], []
path_args = OrderedSet(path_args or []) | self.path_args
paths, fragments = {}, []
for key, value in pycompat.items(kw):
if value and key in path_args:
if isinstance(value, models.BaseModel):
paths.append((key, slug(value)))
paths[key] = slug(value)
else:
paths.append((key, value))
paths[key] = u"%s" % value
elif value:
if isinstance(value, list) or isinstance(value, set):
fragments.append(werkzeug.url_encode([(key, item) for item in value]))
else:
fragments.append(werkzeug.url_encode([(key, value)]))
for key, value in paths:
path += '/' + key + '/%s' % value
for key in path_args:
value = paths.get(key)
if value is not None:
path += '/' + key + '/' + value
if fragments:
path += '?' + '&'.join(fragments)
return path
+1 -1
View File
@@ -8,7 +8,7 @@ Sell digital product using attachments to virtual products
""",
'depends': [
'document',
'website_sale',
'website_portal_sale',
],
'installable': True,
'data': [
@@ -7,7 +7,7 @@ from werkzeug.utils import redirect
from odoo import http
from odoo.http import request
from odoo.addons.website_portal.controllers.main import website_account
from odoo.addons.website_portal_sale.controllers.main import website_account
from odoo.addons.website_sale.controllers.main import WebsiteSale
class WebsiteSaleDigitalConfirmation(WebsiteSale):
+31
View File
@@ -61,6 +61,35 @@ features whereas:
generator delegation, pathlib, ...), you must *not *use them in Odoo
until Python 2 support is dropped
Semantics changes
=================
Dict & set iteration order ("Hash Randomisation")
-------------------------------------------------
In Python 2, the iteration order depends on the value's hash (modulo the
collection's capacity and conflict resolution), which provides a
spec-undefined but implementation-defined order. While that's not supposed to
happen, it turns out code may depend on the specific order of iteration over
a hash collection (``dict`` or ``set``).
Python 3.3 enables `hash randomisation`_ by default (this can be optionally
enabled on previous versions including Python 2 by providing the ``-R``
command-line parameter), which means *the order of iteration changes from one
run to the next*.
When discovered, this can be fixed by one of:
* making iteration steps properly independent (removing the dependency of
order of iteration)
* using different checking method (e.g. when serialising sets or dictionaries
and checking against the specific serialised value)
* fixing dependencies
* using a ``collections.OrderedDict`` or ``odoo.tools.misc.OrderedSet`` instead
of a regular one, they guarantee order of iteration is order of insertion
* sorting the collection's items before iterating over them (this may require
adding some sort of iteration key to the items)
Moved and removed
=================
@@ -340,6 +369,8 @@ Minor syntax changes
error, octal literals now follow the hexadecimal convention with a ``0o``
prefix.
.. _hash randomisation: http://bugs.python.org/issue13703
.. _requests: http://docs.python-requests.org/
.. _werkzeug: http://werkzeug.pocoo.org/docs/urls/
+1 -1
View File
@@ -27,6 +27,7 @@ class Company(models.Model):
return currency_id or self._get_euro()
name = fields.Char(related='partner_id.name', string='Company Name', required=True, store=True)
sequence = fields.Integer(help='Used to order Companies in the company switcher', default=10)
parent_id = fields.Many2one('res.company', string='Parent Company', index=True)
child_ids = fields.One2many('res.company', 'parent_id', string='Child Companies')
partner_id = fields.Many2one('res.partner', string='Partner', required=True)
@@ -52,7 +53,6 @@ class Company(models.Model):
website = fields.Char(related='partner_id.website')
vat = fields.Char(related='partner_id.vat', string="TIN")
company_registry = fields.Char()
sequence = fields.Integer(help='Used to order Companies in the company switcher', default=10)
paperformat_id = fields.Many2one('report.paperformat', 'Paper format', default=lambda self: self.env.ref('report.paperformat_euro', raise_if_not_found=False))
external_report_layout = fields.Selection([
('background', 'Background'),
+1 -1
View File
@@ -510,7 +510,7 @@ REMOVE_CLASS = """
</div>
"""
REMOVE_CLASS_IN = [
'<div style="color: #000000; font-size: 12pt; font-family: \'Times New Roman\'">',
'<div style="font-size: 12pt; font-family: \'Times New Roman\'; color: #000000">',
'An error occured in a modal and I will send you back the html to try opening one on your end']
REMOVE_CLASS_OUT = [
'<div class="modal-backdrop in">',
+67 -68
View File
@@ -14,10 +14,6 @@ from odoo.addons.base.ir.ir_qweb import QWebException
from odoo.tools import pycompat, misc, ustr
def dedent_and_strip(string):
return ''.join([line.strip() for line in string.splitlines()])
class TestQWebTField(TransactionCase):
def setUp(self):
super(TestQWebTField, self).setUp()
@@ -30,13 +26,13 @@ class TestQWebTField(TransactionCase):
result = self.engine.render(field, {'company': company})
self.assertEqual(
result,
'<span data-oe-model="res.company" data-oe-id="%d" '
'data-oe-field="name" data-oe-type="char" '
'data-oe-expression="company.name">%s</span>' % (
etree.fromstring(result),
etree.fromstring(u'<span data-oe-model="res.company" data-oe-id="%d" '
u'data-oe-field="name" data-oe-type="char" '
u'data-oe-expression="company.name">%s</span>' % (
company.id,
"My Test Company",
),
u"My Test Company",
)),
)
def test_i18n(self):
@@ -46,13 +42,13 @@ class TestQWebTField(TransactionCase):
result = self.engine.render(field, {'company': company})
self.assertEqual(
ustr(result),
'<span data-oe-model="res.company" data-oe-id="%d" '
'data-oe-field="name" data-oe-type="char" '
'data-oe-expression="company.name">%s</span>' % (
etree.fromstring(result),
etree.fromstring(u'<span data-oe-model="res.company" data-oe-id="%d" '
u'data-oe-field="name" data-oe-type="char" '
u'data-oe-expression="company.name">%s</span>' % (
company.id,
misc.html_escape(s),
),
)),
)
def test_reject_crummy_tags(self):
@@ -72,7 +68,7 @@ class TestQWebNS(TransactionCase):
def test_render_static_xml_with_namespace(self):
""" Test the rendering on a namespaced view with no static content. The resulting string should be untouched.
"""
expected_result = """
expected_result = u"""
<root>
<h:table xmlns:h="http://www.example.org/table">
<h:tr>
@@ -89,17 +85,17 @@ class TestQWebNS(TransactionCase):
view1 = self.env['ir.ui.view'].create({
'name': "dummy",
'type': 'qweb',
'arch': """
'arch': u"""
<t t-name="base.dummy">%s</t>
""" % expected_result
})
self.assertEquals(dedent_and_strip(view1.render()), dedent_and_strip(expected_result))
self.assertEqual(etree.fromstring(view1.render()), etree.fromstring(expected_result))
def test_render_static_xml_with_namespace_2(self):
""" Test the rendering on a namespaced view with no static content. The resulting string should be untouched.
"""
expected_result = """
expected_result = u"""
<html xmlns="http://www.w3.org/HTML/1998/html4" xmlns:xdc="http://www.xml.com/books">
<head>
<title>Book Review</title>
@@ -127,12 +123,12 @@ class TestQWebNS(TransactionCase):
view1 = self.env['ir.ui.view'].create({
'name': "dummy",
'type': 'qweb',
'arch': """
'arch': u"""
<t t-name="base.dummy">%s</t>
""" % expected_result
})
self.assertEquals(dedent_and_strip(view1.render()), dedent_and_strip(expected_result))
self.assertEqual(etree.fromstring(view1.render()), etree.fromstring(expected_result))
def test_render_static_xml_with_useless_distributed_namespace(self):
""" Test that redundant namespaces are stripped upon rendering.
@@ -140,7 +136,7 @@ class TestQWebNS(TransactionCase):
view1 = self.env['ir.ui.view'].create({
'name': "dummy",
'type': 'qweb',
'arch': """
'arch': u"""
<t t-name="base.dummy">
<root>
<h:table xmlns:h="http://www.example.org/table">
@@ -154,7 +150,7 @@ class TestQWebNS(TransactionCase):
"""
})
expected_result = """
expected_result = etree.fromstring(u"""
<root>
<h:table xmlns:h="http://www.example.org/table">
<h:tr>
@@ -163,24 +159,24 @@ class TestQWebNS(TransactionCase):
</h:tr>
</h:table>
</root>
"""
""")
self.assertEquals(dedent_and_strip(view1.render()), dedent_and_strip(expected_result))
self.assertEqual(etree.fromstring(view1.render()), expected_result)
def test_render_static_xml_with_namespace_3(self):
expected_result = """
expected_result = u"""
<cfdi:Comprobante xmlns:cfdi="http://www.sat.gob.mx/cfd/3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv32.xsd"></cfdi:Comprobante>
"""
view1 = self.env['ir.ui.view'].create({
'name': "dummy",
'type': 'qweb',
'arch': """
'arch': u"""
<t t-name="base.dummy">%s</t>
""" % expected_result
})
self.assertEquals(dedent_and_strip(view1.render()), dedent_and_strip(expected_result))
self.assertEqual(etree.fromstring(view1.render()), etree.fromstring(expected_result))
def test_render_dynamic_xml_with_namespace_t_esc(self):
""" Test that rendering a template containing a node having both an ns declaration and a t-esc attribute correctly
@@ -189,16 +185,16 @@ class TestQWebNS(TransactionCase):
view1 = self.env['ir.ui.view'].create({
'name': "dummy",
'type': 'qweb',
'arch': """
'arch': u"""
<t t-name="base.dummy">
<Invoice xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" t-esc="'test'"/>
</t>
"""
})
expected_result = """<Invoice xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">test</Invoice>"""
expected_result = etree.fromstring(u"""<Invoice xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">test</Invoice>""")
self.assertEquals(dedent_and_strip(view1.render()), dedent_and_strip(expected_result))
self.assertEqual(etree.fromstring(view1.render()), expected_result)
def test_render_dynamic_xml_with_namespace_t_esc_with_useless_distributed_namespace(self):
""" Test that rendering a template containing a node having both an ns declaration and a t-esc attribute correctly
@@ -207,7 +203,7 @@ class TestQWebNS(TransactionCase):
view1 = self.env['ir.ui.view'].create({
'name': "dummy",
'type': 'qweb',
'arch': """
'arch': u"""
<t t-name="base.dummy">
<Invoice xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" t-attf-test="test">
<cac:Test xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">blabla</cac:Test>
@@ -216,13 +212,13 @@ class TestQWebNS(TransactionCase):
"""
})
expected_result = """
expected_result = etree.fromstring(u"""
<Invoice xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" test="test">
<cac:Test>blabla</cac:Test>
</Invoice>
"""
""")
self.assertEquals(dedent_and_strip(view1.render()), dedent_and_strip(expected_result))
self.assertEqual(etree.fromstring(view1.render()), expected_result)
def test_render_dynamic_xml_with_namespace_t_attf(self):
""" Test that rendering a template containing a node having both an ns declaration and a t-attf attribute correctly
@@ -231,7 +227,7 @@ class TestQWebNS(TransactionCase):
view1 = self.env['ir.ui.view'].create({
'name': "dummy",
'type': 'qweb',
'arch': """
'arch': u"""
<t t-name="base.dummy">
<root>
<h:table xmlns:h="http://www.example.org/table">
@@ -248,7 +244,7 @@ class TestQWebNS(TransactionCase):
"""
})
expected_result = """
expected_result = etree.fromstring(u"""
<root>
<h:table xmlns:h="http://www.example.org/table">
<h:tr>
@@ -260,9 +256,9 @@ class TestQWebNS(TransactionCase):
<f:width test="1">80</f:width>
</f:table>
</root>
"""
""")
self.assertEquals(dedent_and_strip(view1.render()), dedent_and_strip(expected_result))
self.assertEqual(etree.fromstring(view1.render()), expected_result)
def test_render_dynamic_xml_with_namespace_t_attf_with_useless_distributed_namespace(self):
""" Test that rendering a template containing a node having both an ns declaration and a t-attf attribute correctly
@@ -271,7 +267,7 @@ class TestQWebNS(TransactionCase):
view1 = self.env['ir.ui.view'].create({
'name': "dummy",
'type': 'qweb',
'arch': """
'arch': u"""
<t t-name="base.dummy">
<root>
<h:table xmlns:h="http://www.example.org/table">
@@ -289,7 +285,7 @@ class TestQWebNS(TransactionCase):
"""
})
expected_result = """
expected_result = etree.fromstring(u"""
<root>
<h:table xmlns:h="http://www.example.org/table">
<h:tr>
@@ -302,15 +298,15 @@ class TestQWebNS(TransactionCase):
</f:table>
</root>
"""
""")
self.assertEquals(dedent_and_strip(view1.render()), dedent_and_strip(expected_result))
self.assertEqual(etree.fromstring(view1.render()), expected_result)
def test_render_dynamic_xml_with_namespace_2(self):
view1 = self.env['ir.ui.view'].create({
'name': "dummy",
'type': 'qweb',
'arch': """
'arch': u"""
<t t-name="base.dummy">
<Invoice xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2" xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<cbc:UBLVersionID t-esc="version_id"/>
@@ -322,7 +318,7 @@ class TestQWebNS(TransactionCase):
"""
})
expected_result = """
expected_result = etree.fromstring(u"""
<Invoice xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2" xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<cbc:UBLVersionID>1.0</cbc:UBLVersionID>
@@ -335,45 +331,45 @@ class TestQWebNS(TransactionCase):
Oasis <cac:Test>4</cac:Test>
</Invoice>
"""
""")
self.assertEquals(dedent_and_strip(view1.render(dict(version_id=1.0))), dedent_and_strip(expected_result))
self.assertEqual(etree.fromstring(view1.render({'version_id': 1.0})), expected_result)
def test_render_static_xml_with_namespaced_attributes(self):
view1 = self.env['ir.ui.view'].create({
'name': "dummy",
'type': 'qweb',
'arch': """
'arch': u"""
<t t-name="base.dummy">
<cfdi:Comprobante xmlns:cfdi="http://www.sat.gob.mx/cfd/3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv32.xsd">abc</cfdi:Comprobante>
</t>
"""
})
expected_result = """<cfdi:Comprobante xmlns:cfdi="http://www.sat.gob.mx/cfd/3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv32.xsd">abc</cfdi:Comprobante>"""
expected_result = etree.fromstring(u"""<cfdi:Comprobante xmlns:cfdi="http://www.sat.gob.mx/cfd/3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv32.xsd">abc</cfdi:Comprobante>""")
self.assertEquals(dedent_and_strip(view1.render()), dedent_and_strip(expected_result))
self.assertEqual(etree.fromstring(view1.render()), expected_result)
def test_render_dynamic_xml_with_namespaced_attributes(self):
view1 = self.env['ir.ui.view'].create({
'name': "dummy",
'type': 'qweb',
'arch': """
'arch': u"""
<t t-name="base.dummy">
<cfdi:Comprobante xmlns:cfdi="http://www.sat.gob.mx/cfd/3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv32.xsd" t-esc="'abc'"/>
</t>
"""
})
expected_result = """<cfdi:Comprobante xmlns:cfdi="http://www.sat.gob.mx/cfd/3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv32.xsd">abc</cfdi:Comprobante>"""
expected_result = etree.fromstring("""<cfdi:Comprobante xmlns:cfdi="http://www.sat.gob.mx/cfd/3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv32.xsd">abc</cfdi:Comprobante>""")
self.assertEquals(dedent_and_strip(view1.render()), dedent_and_strip(expected_result))
self.assertEqual(etree.fromstring(view1.render()), expected_result)
def test_render_static_xml_with_t_call(self):
view1 = self.env['ir.ui.view'].create({
'name': "dummy",
'type': 'qweb',
'arch': """
'arch': u"""
<t t-name="base.dummy">
<cac:fruit xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
@@ -386,13 +382,13 @@ class TestQWebNS(TransactionCase):
"""
})
self.env.cr.execute("INSERT INTO ir_model_data(name, model, res_id, module)"
"VALUES ('dummy', 'ir.ui.view', %s, 'base')" % view1.id)
"VALUES ('dummy', 'ir.ui.view', %s, 'base')", [view1.id])
# view2 will t-call view1
view2 = self.env['ir.ui.view'].create({
'name': "dummy2",
'type': 'qweb',
'arch': """
'arch': u"""
<t t-name="base.dummy2">
<root xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2" xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cac:line t-foreach="[1, 2]" t-as="i" t-call="base.dummy"/>
@@ -406,19 +402,19 @@ class TestQWebNS(TransactionCase):
# check that the root tag has all its xmlns
expected_ns = {
None: 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2',
'cac': 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2',
'cbc': 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2'
(None, 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2'),
('cac', 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2'),
('cbc', 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2'),
}
self.assertSetEqual(set(pycompat.items(expected_ns)) - set(pycompat.items(result_etree.nsmap)), set())
self.assertEqual(set(pycompat.items(result_etree.nsmap)), expected_ns)
# check that the t-call did its work
cac_lines = result_etree.findall('.//cac:line', namespaces={'cac': 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2'})
self.assertEquals(len(cac_lines), 2)
self.assertEquals(result.count('Appel'), 2)
self.assertEqual(len(cac_lines), 2)
self.assertEqual(result.count(b'Appel'), 2)
# check that the t-call dit not output again the xmlns declaration
self.assertEquals(result.count('xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"'), 1)
self.assertEqual(result.count(b'xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"'), 1)
def test_render_static_xml_with_extension(self):
""" Test the extension of a view by an xpath expression on a ns prefixed element.
@@ -427,7 +423,7 @@ class TestQWebNS(TransactionCase):
view1 = self.env['ir.ui.view'].create({
'name': "dummy",
'type': 'qweb',
'arch': """
'arch': u"""
<t t-name="base.dummy">
<root>
<h:table xmlns:h="http://www.example.org/table">
@@ -445,14 +441,14 @@ class TestQWebNS(TransactionCase):
'name': "dummy_ext",
'type': 'qweb',
'inherit_id': view1.id,
'arch': """
'arch': u"""
<xpath expr="//{http://www.example.org/table}table/{http://www.example.org/table}tr">
<h:td xmlns:h="http://www.example.org/table">Oranges</h:td>
</xpath>
"""
})
expected_result = """
expected_result = etree.fromstring(u"""
<root>
<h:table xmlns:h="http://www.example.org/table">
<h:tr>
@@ -462,9 +458,12 @@ class TestQWebNS(TransactionCase):
</h:tr>
</h:table>
</root>
"""
""")
self.assertEquals(dedent_and_strip(view1.with_context(check_view_ids=[view1.id, view2.id]).render()), dedent_and_strip(expected_result))
self.assertEqual(
etree.fromstring(view1.with_context(check_view_ids=[view1.id, view2.id]).render()),
expected_result
)
from copy import deepcopy
-18
View File
@@ -2,10 +2,6 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from functools import partial
try:
from itertools import zip_longest as izip_longuest
except ImportError:
from itertools import izip_longest
from lxml import etree
from lxml.builder import E
@@ -28,22 +24,8 @@ class ViewXMLID(common.TransactionCase):
class ViewCase(common.TransactionCase):
def setUp(self):
super(ViewCase, self).setUp()
self.addTypeEqualityFunc(etree._Element, self.assertTreesEqual)
self.View = self.env['ir.ui.view']
def assertTreesEqual(self, n1, n2, msg=None):
self.assertEqual(n1.tag, n2.tag, msg)
self.assertEqual((n1.text or '').strip(), (n2.text or '').strip(), msg)
self.assertEqual((n1.tail or '').strip(), (n2.tail or '').strip(), msg)
# Because lxml uses ordereddicts in which order is important to
# equality (!?!?!?!)
self.assertEqual(dict(n1.attrib), dict(n2.attrib), msg)
for c1, c2 in izip_longest(n1, n2):
self.assertEqual(c1, c2, msg)
class TestNodeLocator(common.TransactionCase):
"""
The node locator returns None when it can not find a node, and the first
+5 -1
View File
@@ -7,7 +7,9 @@ from collections import OrderedDict, defaultdict
from datetime import date, datetime
from functools import partial
from operator import attrgetter
import itertools
import logging
import pytz
try:
from xmlrpc.client import MAXINT
@@ -119,7 +121,7 @@ class MetaField(type):
elif attr.startswith('_description_'):
cls.description_attrs.append((attr[13:], attr))
_global_seq = iter(itertools.count())
class Field(MetaField('DummyField', (object,), {})):
""" The field descriptor contains the field definition, and manages accesses
and assignments of the corresponding field on records. The following
@@ -296,6 +298,7 @@ class Field(MetaField('DummyField', (object,), {})):
'_attrs': EMPTY_DICT, # the field's non-slot attributes
'_module': None, # the field's module name
'_setup_done': None, # the field's setup state: None, 'base' or 'full'
'_sequence': None, # absolute ordering of the field
'automatic': False, # whether the field is automatically created ("magic" field)
'inherited': False, # whether the field is inherited (_inherits)
@@ -336,6 +339,7 @@ class Field(MetaField('DummyField', (object,), {})):
def __init__(self, string=Default, **kwargs):
kwargs['string'] = string
self._sequence = kwargs['_sequence'] = next(_global_seq)
args = {key: val for key, val in pycompat.items(kwargs) if val is not Default}
self.args = args or EMPTY_DICT
self._setup_done = None
+7 -7
View File
@@ -425,7 +425,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
'_original_module': cls._module,
'_inherit_children': OrderedSet(), # names of children models
'_inherits_children': set(), # names of children models
'_fields': {}, # populated in _setup_base()
'_fields': OrderedDict(), # populated in _setup_base()
})
check_parent = cls._build_model_check_parent
@@ -2272,7 +2272,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
# therefore two registries are never set up at the same time.
# remove fields that are not proper to cls
for name in set(cls._fields) - cls0._proper_fields:
for name in OrderedSet(cls._fields) - cls0._proper_fields:
delattr(cls, name)
cls._fields.pop(name, None)
# collect proper fields on cls0, and add them on cls
@@ -2283,20 +2283,20 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
self._add_field(name, field)
else:
self._add_field(name, field.new(**field.args))
cls._proper_fields = set(cls._fields)
cls._proper_fields = OrderedSet(cls._fields)
else:
# retrieve fields from parent classes, and duplicate them on cls to
# avoid clashes with inheritance between different models
for name in cls._fields:
delattr(cls, name)
cls._fields = {}
for name, field in getmembers(cls, Field.__instancecheck__):
cls._fields = OrderedDict()
for name, field in sorted(getmembers(cls, Field.__instancecheck__), key=lambda f: f[1]._sequence):
# do not retrieve magic, custom and inherited fields
if not any(field.args.get(k) for k in ('automatic', 'manual', 'inherited')):
self._add_field(name, field.new())
self._add_magic_fields()
cls._proper_fields = set(cls._fields)
cls._proper_fields = OrderedSet(cls._fields)
cls.pool.model_cache[cls._model_cache_key] = cls
@@ -3423,7 +3423,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
if not key.startswith('default_')}
# call the 'write' method of fields which are not columns
for name in upd_todo:
for name in sorted(upd_todo, key=lambda name: self._fields[name]._sequence):
field = self._fields[name]
field.write(self.with_context(rel_context), vals[name])
+16 -18
View File
@@ -129,32 +129,30 @@ def test_modifiers(what, expected):
node = etree.fromstring(what)
transfer_node_to_modifiers(node, modifiers)
simplify_modifiers(modifiers)
dump = json.dumps(modifiers)
assert dump == expected, "%s != %s" % (dump, expected)
assert modifiers == expected, "%s != %s" % (modifiers, expected)
elif isinstance(what, dict):
transfer_field_to_modifiers(what, modifiers)
simplify_modifiers(modifiers)
dump = json.dumps(modifiers)
assert dump == expected, "%s != %s" % (dump, expected)
assert modifiers == expected, "%s != %s" % (modifiers, expected)
# To use this test:
# import odoo
# odoo.osv.orm.modifiers_tests()
def modifiers_tests():
test_modifiers('<field name="a"/>', '{}')
test_modifiers('<field name="a" invisible="1"/>', '{"invisible": true}')
test_modifiers('<field name="a" readonly="1"/>', '{"readonly": true}')
test_modifiers('<field name="a" required="1"/>', '{"required": true}')
test_modifiers('<field name="a" invisible="0"/>', '{}')
test_modifiers('<field name="a" readonly="0"/>', '{}')
test_modifiers('<field name="a" required="0"/>', '{}')
test_modifiers('<field name="a" invisible="1" required="1"/>', '{"invisible": true, "required": true}') # TODO order is not guaranteed
test_modifiers('<field name="a" invisible="1" required="0"/>', '{"invisible": true}')
test_modifiers('<field name="a" invisible="0" required="1"/>', '{"required": true}')
test_modifiers("""<field name="a" attrs="{'invisible': [('b', '=', 'c')]}"/>""", '{"invisible": [["b", "=", "c"]]}')
test_modifiers('<field name="a"/>', {})
test_modifiers('<field name="a" invisible="1"/>', {"invisible": True})
test_modifiers('<field name="a" readonly="1"/>', {"readonly": True})
test_modifiers('<field name="a" required="1"/>', {"required": True})
test_modifiers('<field name="a" invisible="0"/>', {})
test_modifiers('<field name="a" readonly="0"/>', {})
test_modifiers('<field name="a" required="0"/>', {})
test_modifiers('<field name="a" invisible="1" required="1"/>', {"invisible": True, "required": True}) # TODO order is not guaranteed
test_modifiers('<field name="a" invisible="1" required="0"/>', {"invisible": True})
test_modifiers('<field name="a" invisible="0" required="1"/>', {"required": True})
test_modifiers("""<field name="a" attrs="{'invisible': [['b', '=', 'c']]}"/>""", {"invisible": [["b", "=", "c"]]})
# The dictionary is supposed to be the result of fields_get().
test_modifiers({}, '{}')
test_modifiers({"invisible": True}, '{"invisible": true}')
test_modifiers({"invisible": False}, '{}')
test_modifiers({}, {})
test_modifiers({"invisible": True}, {"invisible": True})
test_modifiers({"invisible": False}, {})
+23 -1
View File
@@ -18,10 +18,15 @@ import itertools
import unittest
from contextlib import contextmanager
from datetime import datetime, timedelta
from lxml import etree
from pprint import pformat
import requests
try:
from itertools import zip_longest as izip_longest
except ImportError:
from itertools import izip_longest
try:
from xmlrpc import client as xmlrpclib
except ImportError:
@@ -82,7 +87,24 @@ def post_install(flag):
return obj
return decorator
class BaseCase(unittest.TestCase):
class TreeCase(unittest.TestCase):
def __init__(self, methodName='runTest'):
super(TreeCase, self).__init__(methodName)
self.addTypeEqualityFunc(etree._Element, self.assertTreesEqual)
def assertTreesEqual(self, n1, n2, msg=None):
self.assertEqual(n1.tag, n2.tag, msg)
# Because lxml.attrib is an ordereddict for which order is important
# to equality, even though *we* don't care
self.assertEqual(dict(n1.attrib), dict(n2.attrib), msg)
self.assertEqual((n1.text or u'').strip(), (n2.text or u'').strip(), msg)
self.assertEqual((n1.tail or u'').strip(), (n2.tail or u'').strip(), msg)
for c1, c2 in izip_longest(n1, n2):
self.assertEqual(c1, c2, msg)
class BaseCase(TreeCase):
"""
Subclass of TestCase for common OpenERP-specific code.
+2 -1
View File
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import collections
import logging
from lxml.html import clean
import random
@@ -147,7 +148,7 @@ class _Cleaner(clean.Cleaner):
attributes = el.attrib
styling = attributes.get('style')
if styling:
valid_styles = {}
valid_styles = collections.OrderedDict()
styles = self._style_re.findall(styling)
for style in styles:
if style[0].lower() in self._style_whitelist: