[FIX] P3: hash collections iteration issues
See various commits for information Closes #17111
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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">',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user