From bbe0fbe2b9b09102b25b59c8f847588aa9aac8c1 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 15 Apr 2013 12:08:58 +0200 Subject: [PATCH 001/421] [IMP] use ORM reads when loading views direct SQL query in core ORM means we can't override view loading to get them from other sources than the database. By using read() this can be done by overriding ir_ui_view.read. bzr revid: xmo@openerp.com-20130415100858-ucm97f318mjn5ugq --- openerp/osv/orm.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index a451fbf6582..27df0792f69 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -2055,6 +2055,7 @@ class BaseModel(object): """ if context is None: context = {} + View = self.pool['ir.ui.view'] def encode(s): if isinstance(s, unicode): @@ -2197,6 +2198,9 @@ class BaseModel(object): sql_res = False parent_view_model = None view_ref = context.get(view_type + '_view_ref') + + view_fields = ['arch', 'name', 'field_parent', 'id', + 'type', 'inherit_id', 'model'] # Search for a root (i.e. without any parent) view. while True: if view_ref and not view_id: @@ -2206,22 +2210,25 @@ class BaseModel(object): view_ref_res = cr.fetchone() if view_ref_res: view_id = view_ref_res[0] - if view_id: - cr.execute("""SELECT arch,name,field_parent,id,type,inherit_id,model - FROM ir_ui_view - WHERE id=%s""", (view_id,)) + ids = [view_id] else: - cr.execute("""SELECT arch,name,field_parent,id,type,inherit_id,model - FROM ir_ui_view - WHERE model=%s AND type=%s AND inherit_id IS NULL - ORDER BY priority""", (self._name, view_type)) - sql_res = cr.dictfetchone() + # read does not guarantee ordering so directly take just first + # search'ed id and read that, this way we don't care + ids = View.search(cr, user, [ + ['model', '=', self._name], + ['type', '=', view_type], + ['inherit_id', '=', False], + ], context=context, order='priority')[:1] + views = View.read(cr, user, ids, view_fields, context=context) + sql_res = views[0] if views else False if not sql_res: break view_id = sql_res['inherit_id'] or sql_res['id'] + # due to read() inherit_id may be a name_get pair, unpack id + if isinstance(view_id, tuple): view_id, _name = view_id parent_view_model = sql_res['model'] if not sql_res['inherit_id']: break From 6a8d05b77e6d44851169dea1616b0d551091b3cc Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 19 Apr 2013 14:37:12 +0200 Subject: [PATCH 002/421] [IMP] move raw SQL to ORM Improves maintainability, required adding a fake-ish o2m field to ir.model.data to handle join. bzr revid: xmo@openerp.com-20130419123712-6r6q1e8h93wly66y --- openerp/addons/base/ir/ir_ui_view.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 1d666f2f5c8..6e8af66ff7a 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -80,6 +80,7 @@ class view(osv.osv): help="ID of the view defined in xml file"), 'groups_id': fields.many2many('res.groups', 'ir_ui_view_group_rel', 'view_id', 'group_id', string='Groups', help="If this field is empty, the view applies to all users. Otherwise, the view applies to the users of those groups only."), + 'model_ids': fields.one2many('ir.model.data', 'res_id', auto_join=True), } _defaults = { 'arch': '\n\n\t\n', @@ -176,20 +177,17 @@ class view(osv.osv): :return: [(view_arch,view_id), ...] """ user_groups = frozenset(self.pool.get('res.users').browse(cr, 1, uid, context).groups_id) + + conditions = [['inherit_id', '=', view_id], ['model', '=', model]] if self.pool._init: - # Module init currently in progress, only consider views from modules whose code was already loaded - query = """SELECT v.id FROM ir_ui_view v LEFT JOIN ir_model_data md ON (md.model = 'ir.ui.view' AND md.res_id = v.id) - WHERE v.inherit_id=%s AND v.model=%s AND md.module in %s - ORDER BY priority""" - query_params = (view_id, model, tuple(self.pool._init_modules)) - else: - # Modules fully loaded, consider all views - query = """SELECT v.id FROM ir_ui_view v - WHERE v.inherit_id=%s AND v.model=%s - ORDER BY priority""" - query_params = (view_id, model) - cr.execute(query, query_params) - view_ids = [v[0] for v in cr.fetchall()] + # Module init currently in progress, only consider views from + # modules whose code is already loaded + conditions.extend([ + ['model_ids.model', '=', 'ir.ui.view'], + ['model_ids.module', 'in', tuple(self.pool._init_modules)], + ]) + view_ids = self.search(cr, uid, conditions, context=context) + # filter views based on user groups return [(view.arch, view.id) for view in self.browse(cr, 1, view_ids, context) From aa9406e28b509f186b9cf1731834ee7252092530 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 19 Apr 2013 16:14:09 +0200 Subject: [PATCH 003/421] [IMP] move locate inner function to ir.ui.view method bzr revid: xmo@openerp.com-20130419141409-a2jqq5maszc53e6s --- openerp/addons/base/ir/ir_ui_view.py | 40 +++++++++++++++++++++++++++- openerp/osv/orm.py | 40 +--------------------------- 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 6e8af66ff7a..5259964c35a 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -25,7 +25,7 @@ import os from openerp import tools from openerp.osv import fields,osv -from openerp.tools import graph +from openerp.tools import graph, SKIPPED_ELEMENT_TYPES from openerp.tools.safe_eval import safe_eval as eval from openerp.tools.view_validation import valid_view @@ -163,6 +163,44 @@ class view(osv.osv): if not cr.fetchone(): cr.execute('CREATE INDEX ir_ui_view_model_type_inherit_id ON ir_ui_view (model, inherit_id)') + def locate_node(self, arch, spec): + """ Locate a node in a source (parent) architecture. + + Given a complete source (parent) architecture (i.e. the field + `arch` in a view), and a 'spec' node (a node in an inheriting + view that specifies the location in the source view of what + should be changed), return (if it exists) the node in the + source view matching the specification. + + :param arch: a parent architecture to modify + :param spec: a modifying node in an inheriting view + :return: a node in the source matching the spec + + """ + if spec.tag == 'xpath': + nodes = arch.xpath(spec.get('expr')) + return nodes[0] if nodes else None + elif spec.tag == 'field': + # Only compare the field name: a field can be only once in a given view + # at a given level (and for multilevel expressions, we should use xpath + # inheritance spec anyway). + for node in arch.getiterator('field'): + if node.get('name') == spec.get('name'): + return node + return None + + for node in arch.getiterator(spec.tag): + if isinstance(node, SKIPPED_ELEMENT_TYPES): + continue + if all(node.get(attr) == spec.get(attr) \ + for attr in spec.attrib + if attr not in ('position','version')): + # Version spec should match parent's root element's version + if spec.get('version') and spec.get('version') != arch.get('version'): + return None + return node + return None + def get_inheriting_views_arch(self, cr, uid, view_id, model, context=None): """Retrieves the architecture of views that inherit from the given view, from the sets of views that should currently be used in the system. During the module upgrade phase it diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 27df0792f69..84ba1d1d2d0 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -2068,44 +2068,6 @@ class BaseModel(object): raise AttributeError("View definition error for inherited view '%s' on model '%s': %s" % (child_view.xml_id, self._name, error_msg)) - def locate(source, spec): - """ Locate a node in a source (parent) architecture. - - Given a complete source (parent) architecture (i.e. the field - `arch` in a view), and a 'spec' node (a node in an inheriting - view that specifies the location in the source view of what - should be changed), return (if it exists) the node in the - source view matching the specification. - - :param source: a parent architecture to modify - :param spec: a modifying node in an inheriting view - :return: a node in the source matching the spec - - """ - if spec.tag == 'xpath': - nodes = source.xpath(spec.get('expr')) - return nodes[0] if nodes else None - elif spec.tag == 'field': - # Only compare the field name: a field can be only once in a given view - # at a given level (and for multilevel expressions, we should use xpath - # inheritance spec anyway). - for node in source.getiterator('field'): - if node.get('name') == spec.get('name'): - return node - return None - - for node in source.getiterator(spec.tag): - if isinstance(node, SKIPPED_ELEMENT_TYPES): - continue - if all(node.get(attr) == spec.get(attr) \ - for attr in spec.attrib - if attr not in ('position','version')): - # Version spec should match parent's root element's version - if spec.get('version') and spec.get('version') != source.get('version'): - return None - return node - return None - def apply_inheritance_specs(source, specs_arch, inherit_id=None): """ Apply an inheriting view. @@ -2131,7 +2093,7 @@ class BaseModel(object): if spec.tag == 'data': specs += [ c for c in specs_tree ] continue - node = locate(source, spec) + node = View.locate_node(source, spec) if node is not None: pos = spec.get('position', 'inside') if pos == 'replace': From c9a6e74bf4cf25194807329ce74c3e08bf3ea912 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 22 Apr 2013 11:10:14 +0200 Subject: [PATCH 004/421] [IMP] add tests for locate_node bzr revid: xmo@openerp.com-20130422091014-qbd4jun4n9u4402x --- openerp/addons/base/ir/ir_ui_view.py | 5 +- openerp/tests/__init__.py | 2 + openerp/tests/test_views.py | 96 ++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 openerp/tests/test_views.py diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 5259964c35a..15a57017203 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -192,9 +192,8 @@ class view(osv.osv): for node in arch.getiterator(spec.tag): if isinstance(node, SKIPPED_ELEMENT_TYPES): continue - if all(node.get(attr) == spec.get(attr) \ - for attr in spec.attrib - if attr not in ('position','version')): + if all(node.get(attr) == spec.get(attr) for attr in spec.attrib + if attr not in ('position','version')): # Version spec should match parent's root element's version if spec.get('version') and spec.get('version') != arch.get('version'): return None diff --git a/openerp/tests/__init__.py b/openerp/tests/__init__.py index e6a2eacf386..d330cffe350 100644 --- a/openerp/tests/__init__.py +++ b/openerp/tests/__init__.py @@ -22,6 +22,7 @@ import test_osv import test_translate import test_uninstall import test_view_validation +import test_views # This need a change in `oe run-tests` to only run fast_suite + checks by default. # import test_xmlrpc @@ -42,6 +43,7 @@ checks = [ test_misc, test_osv, test_translate, + test_views, ] # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/tests/test_views.py b/openerp/tests/test_views.py new file mode 100644 index 00000000000..8ee07d5523d --- /dev/null +++ b/openerp/tests/test_views.py @@ -0,0 +1,96 @@ +from lxml import etree as ET +from lxml.builder import E + +from . import common + +Field = E.field + +class TestNodeLocator(common.BaseCase): + """ + The node locator returns None when it can not find a node, and the first + match when it finds something (no jquery-style node sets) + """ + def setUp(self): + super(TestNodeLocator, self).setUp() + self.Views = self.registry('ir.ui.view') + + def test_no_match_xpath(self): + """ + xpath simply uses the provided @expr pattern to find a node + """ + node = self.Views.locate_node( + E.root(E.foo(), E.bar(), E.baz()), + E.xpath(expr="//qux")) + self.assertIsNone(node) + + def test_match_xpath(self): + bar = E.bar() + node = self.Views.locate_node( + E.root(E.foo(), bar, E.baz()), + E.xpath(expr="//bar")) + self.assertIs(node, bar) + + + def test_no_match_field(self): + """ + A field spec will match by @name against all fields of the view + """ + node = self.Views.locate_node( + E.root(E.foo(), E.bar(), E.baz()), + Field(name="qux")) + self.assertIsNone(node) + + node = self.Views.locate_node( + E.root(Field(name="foo"), Field(name="bar"), Field(name="baz")), + Field(name="qux")) + self.assertIsNone(node) + + def test_match_field(self): + bar = Field(name="bar") + node = self.Views.locate_node( + E.root(Field(name="foo"), bar, Field(name="baz")), + Field(name="bar")) + self.assertIs(node, bar) + + + def test_no_match_other(self): + """ + Non-xpath non-fields are matched by node name first + """ + node = self.Views.locate_node( + E.root(E.foo(), E.bar(), E.baz()), + E.qux()) + self.assertIsNone(node) + + def test_match_other(self): + bar = E.bar() + node = self.Views.locate_node( + E.root(E.foo(), bar, E.baz()), + E.bar()) + self.assertIs(bar, node) + + def test_attribute_mismatch(self): + """ + Non-xpath non-field are filtered by matching attributes on spec and + matched nodes + """ + node = self.Views.locate_node( + E.root(E.foo(attr='1'), E.bar(attr='2'), E.baz(attr='3')), + E.bar(attr='5')) + self.assertIsNone(node) + + def test_attribute_filter(self): + match = E.bar(attr='2') + node = self.Views.locate_node( + E.root(E.bar(attr='1'), match, E.root(E.bar(attr='3'))), + E.bar(attr='2')) + self.assertIs(node, match) + + def test_version_mismatch(self): + """ + A @version on the spec will be matched against the view's version + """ + node = self.Views.locate_node( + E.root(E.foo(attr='1'), version='4'), + E.foo(attr='1', version='3')) + self.assertIsNone(node) From 96832248301d52e7a693ec1898dbf777eb82d6c2 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 22 Apr 2013 11:19:58 +0200 Subject: [PATCH 005/421] [IMP] Element.getiterator -> Element.iter > Note that this method is deprecated as of ElementTree 1.3 and lxml > 2.0. It returns an iterator in lxml, which diverges from the > original ElementTree behaviour. If you want an efficient iterator, > use the element.iter() method instead. You should only use this > method in new code if you require backwards compatibility with older > versions of lxml or ElementTree. bzr revid: xmo@openerp.com-20130422091958-413qo439qqgv296u --- openerp/addons/base/ir/ir_ui_view.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 15a57017203..b12f09b4224 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -184,12 +184,12 @@ class view(osv.osv): # Only compare the field name: a field can be only once in a given view # at a given level (and for multilevel expressions, we should use xpath # inheritance spec anyway). - for node in arch.getiterator('field'): + for node in arch.iter('field'): if node.get('name') == spec.get('name'): return node return None - for node in arch.getiterator(spec.tag): + for node in arch.iter(spec.tag): if isinstance(node, SKIPPED_ELEMENT_TYPES): continue if all(node.get(attr) == spec.get(attr) for attr in spec.attrib From cdc81103632b35dc6f8a336746f91ba1d7d0b1fc Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 22 Apr 2013 14:27:39 +0200 Subject: [PATCH 006/421] Temporarily disable XML validity check (calls fields_view_get, PITA) bzr revid: xmo@openerp.com-20130422122739-gx72s6lxna0nk5ug --- 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 b12f09b4224..69d77e58b44 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -154,7 +154,7 @@ class view(osv.osv): return True _constraints = [ - (_check_xml, 'Invalid XML for View Architecture!', ['arch']) + #(_check_xml, 'Invalid XML for View Architecture!', ['arch']) ] def _auto_init(self, cr, context=None): From bad87b4245ae4532f9cdaca19b513db1cf27bfc9 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 22 Apr 2013 14:28:12 +0200 Subject: [PATCH 007/421] [ADD] some tests for get_inheriting_view_arch Tests are way incomplete: no test for partial loading or groups filtering bzr revid: xmo@openerp.com-20130422122812-c1d385zwau9wlmci --- openerp/tests/test_views.py | 45 +++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/openerp/tests/test_views.py b/openerp/tests/test_views.py index 8ee07d5523d..9c964551c0c 100644 --- a/openerp/tests/test_views.py +++ b/openerp/tests/test_views.py @@ -94,3 +94,48 @@ class TestNodeLocator(common.BaseCase): E.root(E.foo(attr='1'), version='4'), E.foo(attr='1', version='3')) self.assertIsNone(node) + +class TestViewInheritance(common.TransactionCase): + def view_for(self, name): + return ET.tostring(ET.Element('form', string=name)) + + def makeView(self, name, parent=None): + view_id = self.View.create(self.cr, self.uid, { + 'model': self.model, + 'name': name, + 'arch': self.view_for(name), + 'inherit_id': parent, }) + self.ids[name] = view_id + return view_id + + def setUp(self): + super(TestViewInheritance, self).setUp() + + self.model = 'dummy' + self.View = self.registry('ir.ui.view') + self.ids = {} + + a = self.makeView("A") + a1 = self.makeView("A1", a) + a11 = self.makeView("A11", a1) + self.makeView("A111", a11) + self.makeView("A12", a1) + a2 = self.makeView("A2", a) + self.makeView("A21", a2) + a22 = self.makeView("A22", a2) + self.makeView("A221", a22) + + def test_get_children(self): + self.assertEqual(self.View.get_inheriting_views_arch( + self.cr, self.uid, self.ids['A'], self.model), [ + (self.view_for('A1'), self.ids['A1']), + (self.view_for('A2'), self.ids['A2']), + ]) + + self.assertEqual(self.View.get_inheriting_views_arch( + self.cr, self.uid, self.ids['A21'], self.model), + []) + + self.assertEqual(self.View.get_inheriting_views_arch( + self.cr, self.uid, self.ids['A11'], self.model), + [(self.view_for('A111'), self.ids['A111'])]) From edf1f23a6e1ec6508dac9ca3c502f99ee1c0097a Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 22 Apr 2013 14:50:00 +0200 Subject: [PATCH 008/421] [ADD] depth-first iterator on views, refactor apply_view_inheritance to use it bzr revid: xmo@openerp.com-20130422125000-pg60ovrxejegenka --- openerp/addons/base/ir/ir_ui_view.py | 25 +++++++++++++++++++++++++ openerp/osv/orm.py | 21 ++++++++++----------- openerp/tests/test_views.py | 13 +++++++++++++ 3 files changed, 48 insertions(+), 11 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 69d77e58b44..13afd096b38 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -230,6 +230,31 @@ class view(osv.osv): for view in self.browse(cr, 1, view_ids, context) if not (view.groups_id and user_groups.isdisjoint(view.groups_id))] + def iter(self, cr, uid, view_id, model, exclude_base=False, context=None): + """ iterates on all of `view_id`'s descendants tree depth-first. + + If `exclude_base` is `False`, also yields `view_id` itself. It is + `False` by default to match the behavior of etree's Element.iter. + + :param int view_id: database id of the root view + :param str model: name of the view's related model (for filtering) + :param boolean exclude_base: whether `view_id` should be excluded from the iteration + :param context: + :return: iterator of (database_id, arch_field) pairs for all + descendants of `view_id` (including `view_id` itself if + `exclude_base` is `False`, the default) + """ + if not exclude_base: + [base] = self.browse(cr, uid, [view_id], context=context) + yield base.id, base.arch + + for arch, id in self.get_inheriting_views_arch( + cr, uid, view_id, model, context=context): + yield id, arch + for info in self.iter( + cr, uid, id, model, exclude_base=True, context=None): + yield info + def write(self, cr, uid, ids, vals, context=None): if not isinstance(ids, (list, tuple)): ids = [ids] diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 84ba1d1d2d0..69cb422e6c4 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -2068,16 +2068,16 @@ class BaseModel(object): raise AttributeError("View definition error for inherited view '%s' on model '%s': %s" % (child_view.xml_id, self._name, error_msg)) - def apply_inheritance_specs(source, specs_arch, inherit_id=None): - """ Apply an inheriting view. + def apply_inheritance_specs(source, descendant_id, specs_arch): + """ Apply an inheriting view (a descendant of the base view) Apply to a source architecture all the spec nodes (i.e. nodes describing where and what changes to apply to some parent architecture) given by an inheriting view. :param source: a parent architecture to modify + :param descendant_id: the database id of the descendant :param specs_arch: a modifying architecture in an inheriting view - :param inherit_id: the database id of the inheriting view :return: a modified source where the specs are applied """ @@ -2124,7 +2124,7 @@ class BaseModel(object): elif pos == 'before': node.addprevious(child) else: - raise_view_error("Invalid position value: '%s'" % pos, inherit_id) + raise_view_error("Invalid position value: '%s'" % pos, descendant_id) else: attrs = ''.join([ ' %s="%s"' % (attr, spec.get(attr)) @@ -2134,8 +2134,8 @@ class BaseModel(object): tag = "<%s%s>" % (spec.tag, attrs) if spec.get('version') and spec.get('version') != source.get('version'): raise_view_error("Mismatching view API version for element '%s': %r vs %r in parent view '%%(parent_xml_id)s'" % \ - (tag, spec.get('version'), source.get('version')), inherit_id) - raise_view_error("Element '%s' not found in parent view '%%(parent_xml_id)s'" % tag, inherit_id) + (tag, spec.get('version'), source.get('version')), descendant_id) + raise_view_error("Element '%s' not found in parent view '%%(parent_xml_id)s'" % tag, descendant_id) return source @@ -2149,11 +2149,10 @@ class BaseModel(object): are applied """ - sql_inherit = self.pool.get('ir.ui.view').get_inheriting_views_arch(cr, user, inherit_id, self._name) - for (view_arch, view_id) in sql_inherit: - source = apply_inheritance_specs(source, view_arch, view_id) - source = apply_view_inheritance(cr, user, source, view_id) - return source + return reduce( + lambda s, descendant: apply_inheritance_specs(s, *descendant), + self.pool['ir.ui.view'].iter(cr, user, inherit_id, self._name), + source) result = {'type': view_type, 'model': self._name} diff --git a/openerp/tests/test_views.py b/openerp/tests/test_views.py index 9c964551c0c..8c3653eceda 100644 --- a/openerp/tests/test_views.py +++ b/openerp/tests/test_views.py @@ -139,3 +139,16 @@ class TestViewInheritance(common.TransactionCase): self.assertEqual(self.View.get_inheriting_views_arch( self.cr, self.uid, self.ids['A11'], self.model), [(self.view_for('A111'), self.ids['A111'])]) + + def test_iterate_descendents(self): + descendents = list(self.View.iter(self.cr, self.uid, self.ids['A1'], self.model)) + self.assertEqual(descendents, [ + (self.ids[name], self.view_for(name)) + for name in ['A1', 'A11', 'A111', 'A12'] + ]) + descendents = list(self.View.iter( + self.cr, self.uid, self.ids['A2'], self.model, exclude_base=True)) + self.assertEqual(descendents, [ + (self.ids[name], self.view_for(name)) + for name in ['A21', 'A22', 'A221'] + ]) From 480bf6a99b315d97e3eee9277b301d33d177d597 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 22 Apr 2013 16:01:16 +0200 Subject: [PATCH 009/421] [FIX] temporarily remove _init flag of pooler during view inheritance test Breaks between in-loading (openerp) and unit test (oe), behavior is crap and can't be arsed to create a dummy ir.model.data for that. bzr revid: xmo@openerp.com-20130422140116-g8mqzvahbv2ag956 --- openerp/tests/test_views.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openerp/tests/test_views.py b/openerp/tests/test_views.py index 8c3653eceda..7a61aab5aa2 100644 --- a/openerp/tests/test_views.py +++ b/openerp/tests/test_views.py @@ -113,6 +113,8 @@ class TestViewInheritance(common.TransactionCase): self.model = 'dummy' self.View = self.registry('ir.ui.view') + self._init = self.View.pool._init + self.View.pool._init = False self.ids = {} a = self.makeView("A") @@ -125,6 +127,10 @@ class TestViewInheritance(common.TransactionCase): a22 = self.makeView("A22", a2) self.makeView("A221", a22) + def tearDown(self): + self.View.pool._init = self._init + super(TestViewInheritance, self).tearDown() + def test_get_children(self): self.assertEqual(self.View.get_inheriting_views_arch( self.cr, self.uid, self.ids['A'], self.model), [ From 980e01d3860bd60d7de2fb61ea1c47c54afdb45d Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 22 Apr 2013 16:24:44 +0200 Subject: [PATCH 010/421] [IMP] attempt to remove lexical dependencies from fvg's nested functions should allow moving most of the view inheritance application into ir.ui.view, where it should belong. bzr revid: xmo@openerp.com-20130422142444-f09sw841jop3t4a4 --- openerp/osv/orm.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 69cb422e6c4..0e50c08ce2e 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -2062,13 +2062,13 @@ class BaseModel(object): return s.encode('utf8') return s - def raise_view_error(error_msg, child_view_id): - view, child_view = self.pool.get('ir.ui.view').browse(cr, user, [view_id, child_view_id], context) + def raise_view_error(cr, uid, model, error_msg, view_id, child_view_id, context=None): + view, child_view = self.pool.get('ir.ui.view').browse(cr, uid, [view_id, child_view_id], context) error_msg = error_msg % {'parent_xml_id': view.xml_id} raise AttributeError("View definition error for inherited view '%s' on model '%s': %s" - % (child_view.xml_id, self._name, error_msg)) + % (child_view.xml_id, model, error_msg)) - def apply_inheritance_specs(source, descendant_id, specs_arch): + def apply_inheritance_specs(cr, uid, model, root_view_id, source, descendant_id, specs_arch, context=None): """ Apply an inheriting view (a descendant of the base view) Apply to a source architecture all the spec nodes (i.e. nodes @@ -2124,7 +2124,7 @@ class BaseModel(object): elif pos == 'before': node.addprevious(child) else: - raise_view_error("Invalid position value: '%s'" % pos, descendant_id) + raise_view_error(cr, uid, model, "Invalid position value: '%s'" % pos, root_view_id, descendant_id, context=context) else: attrs = ''.join([ ' %s="%s"' % (attr, spec.get(attr)) @@ -2133,9 +2133,9 @@ class BaseModel(object): ]) tag = "<%s%s>" % (spec.tag, attrs) if spec.get('version') and spec.get('version') != source.get('version'): - raise_view_error("Mismatching view API version for element '%s': %r vs %r in parent view '%%(parent_xml_id)s'" % \ - (tag, spec.get('version'), source.get('version')), descendant_id) - raise_view_error("Element '%s' not found in parent view '%%(parent_xml_id)s'" % tag, descendant_id) + raise_view_error(cr, uid, model, "Mismatching view API version for element '%s': %r vs %r in parent view '%%(parent_xml_id)s'" % \ + (tag, spec.get('version'), source.get('version')), root_view_id, descendant_id, context=context) + raise_view_error(cr, uid, model, "Element '%s' not found in parent view '%%(parent_xml_id)s'" % tag, root_view_id, descendant_id, context=context) return source @@ -2150,8 +2150,9 @@ class BaseModel(object): """ return reduce( - lambda s, descendant: apply_inheritance_specs(s, *descendant), - self.pool['ir.ui.view'].iter(cr, user, inherit_id, self._name), + lambda s, descendant: apply_inheritance_specs( + cr, user, self._name, inherit_id, s, *descendant, context=context), + self.pool['ir.ui.view'].iter(cr, user, inherit_id, self._name, context=context), source) result = {'type': view_type, 'model': self._name} From b0f2d3044ab1f1dcbc197eb950056828e93e56a1 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 23 Apr 2013 12:08:45 +0200 Subject: [PATCH 011/421] [FIX] don't 'double tap' when applying view inheritance iter(exclude_base=False) would add the base view (being extended) to the sequence, and thus start by extending the base view with itself. bzr revid: xmo@openerp.com-20130423100845-ryumkx12cwv60pc1 --- openerp/osv/orm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 0e50c08ce2e..661262342c0 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -2152,7 +2152,7 @@ class BaseModel(object): return reduce( lambda s, descendant: apply_inheritance_specs( cr, user, self._name, inherit_id, s, *descendant, context=context), - self.pool['ir.ui.view'].iter(cr, user, inherit_id, self._name, context=context), + self.pool['ir.ui.view'].iter(cr, user, inherit_id, self._name, exclude_base=True, context=context), source) result = {'type': view_type, 'model': self._name} From e996f92e6c1ada396c8acc951e856edaa30339dd Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 23 Apr 2013 12:58:56 +0200 Subject: [PATCH 012/421] [REF] move apply_inheritance_specs to ir.ui.view also moved raise_view_error utility function bzr revid: xmo@openerp.com-20130423105856-558rl6ankd4g3qb0 --- openerp/addons/base/ir/ir_ui_view.py | 81 +++++++++++++++++++++++++++ openerp/osv/orm.py | 82 +--------------------------- 2 files changed, 84 insertions(+), 79 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 13afd096b38..06582c51f07 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 copy import logging from lxml import etree @@ -255,6 +256,86 @@ class view(osv.osv): cr, uid, id, model, exclude_base=True, context=None): yield info + + def raise_view_error(self, cr, uid, model, error_msg, view_id, child_view_id, context=None): + view, child_view = self.browse(cr, uid, [view_id, child_view_id], context) + error_msg = error_msg % {'parent_xml_id': view.xml_id} + raise AttributeError("View definition error for inherited view '%s' on model '%s': %s" + % (child_view.xml_id, model, error_msg)) + + def apply_inheritance_specs(self, cr, uid, model, root_view_id, source, descendant_id, specs_arch, context=None): + """ Apply an inheriting view (a descendant of the base view) + + Apply to a source architecture all the spec nodes (i.e. nodes + describing where and what changes to apply to some parent + architecture) given by an inheriting view. + + :param source: a parent architecture to modify + :param descendant_id: the database id of the descendant + :param specs_arch: a modifying architecture in an inheriting view + :return: a modified source where the specs are applied + + """ + if isinstance(specs_arch, unicode): + specs_arch = specs_arch.encode('utf-8') + specs_tree = etree.fromstring(specs_arch) + # Queue of specification nodes (i.e. nodes describing where and + # changes to apply to some parent architecture). + specs = [specs_tree] + + while len(specs): + spec = specs.pop(0) + if isinstance(spec, SKIPPED_ELEMENT_TYPES): + continue + if spec.tag == 'data': + specs += [ c for c in specs_tree ] + continue + node = self.locate_node(source, spec) + if node is not None: + pos = spec.get('position', 'inside') + if pos == 'replace': + if node.getparent() is None: + source = copy.deepcopy(spec[0]) + else: + for child in spec: + node.addprevious(child) + node.getparent().remove(node) + elif pos == 'attributes': + for child in spec.getiterator('attribute'): + attribute = (child.get('name'), child.text and child.text.encode('utf8') or None) + if attribute[1]: + node.set(attribute[0], attribute[1]) + else: + del(node.attrib[attribute[0]]) + else: + sib = node.getnext() + for child in spec: + if pos == 'inside': + node.append(child) + elif pos == 'after': + if sib is None: + node.addnext(child) + node = child + else: + sib.addprevious(child) + elif pos == 'before': + node.addprevious(child) + else: + self.raise_view_error(cr, uid, model, "Invalid position value: '%s'" % pos, root_view_id, descendant_id, context=context) + else: + attrs = ''.join([ + ' %s="%s"' % (attr, spec.get(attr)) + for attr in spec.attrib + if attr != 'position' + ]) + tag = "<%s%s>" % (spec.tag, attrs) + if spec.get('version') and spec.get('version') != source.get('version'): + self.raise_view_error(cr, uid, model, "Mismatching view API version for element '%s': %r vs %r in parent view '%%(parent_xml_id)s'" % \ + (tag, spec.get('version'), source.get('version')), root_view_id, descendant_id, context=context) + self.raise_view_error(cr, uid, model, "Element '%s' not found in parent view '%%(parent_xml_id)s'" % tag, root_view_id, descendant_id, context=context) + + return source + def write(self, cr, uid, ids, vals, context=None): if not isinstance(ids, (list, tuple)): ids = [ids] diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 661262342c0..50bdd7dfc2c 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -2062,83 +2062,6 @@ class BaseModel(object): return s.encode('utf8') return s - def raise_view_error(cr, uid, model, error_msg, view_id, child_view_id, context=None): - view, child_view = self.pool.get('ir.ui.view').browse(cr, uid, [view_id, child_view_id], context) - error_msg = error_msg % {'parent_xml_id': view.xml_id} - raise AttributeError("View definition error for inherited view '%s' on model '%s': %s" - % (child_view.xml_id, model, error_msg)) - - def apply_inheritance_specs(cr, uid, model, root_view_id, source, descendant_id, specs_arch, context=None): - """ Apply an inheriting view (a descendant of the base view) - - Apply to a source architecture all the spec nodes (i.e. nodes - describing where and what changes to apply to some parent - architecture) given by an inheriting view. - - :param source: a parent architecture to modify - :param descendant_id: the database id of the descendant - :param specs_arch: a modifying architecture in an inheriting view - :return: a modified source where the specs are applied - - """ - specs_tree = etree.fromstring(encode(specs_arch)) - # Queue of specification nodes (i.e. nodes describing where and - # changes to apply to some parent architecture). - specs = [specs_tree] - - while len(specs): - spec = specs.pop(0) - if isinstance(spec, SKIPPED_ELEMENT_TYPES): - continue - if spec.tag == 'data': - specs += [ c for c in specs_tree ] - continue - node = View.locate_node(source, spec) - if node is not None: - pos = spec.get('position', 'inside') - if pos == 'replace': - if node.getparent() is None: - source = copy.deepcopy(spec[0]) - else: - for child in spec: - node.addprevious(child) - node.getparent().remove(node) - elif pos == 'attributes': - for child in spec.getiterator('attribute'): - attribute = (child.get('name'), child.text and child.text.encode('utf8') or None) - if attribute[1]: - node.set(attribute[0], attribute[1]) - else: - del(node.attrib[attribute[0]]) - else: - sib = node.getnext() - for child in spec: - if pos == 'inside': - node.append(child) - elif pos == 'after': - if sib is None: - node.addnext(child) - node = child - else: - sib.addprevious(child) - elif pos == 'before': - node.addprevious(child) - else: - raise_view_error(cr, uid, model, "Invalid position value: '%s'" % pos, root_view_id, descendant_id, context=context) - else: - attrs = ''.join([ - ' %s="%s"' % (attr, spec.get(attr)) - for attr in spec.attrib - if attr != 'position' - ]) - tag = "<%s%s>" % (spec.tag, attrs) - if spec.get('version') and spec.get('version') != source.get('version'): - raise_view_error(cr, uid, model, "Mismatching view API version for element '%s': %r vs %r in parent view '%%(parent_xml_id)s'" % \ - (tag, spec.get('version'), source.get('version')), root_view_id, descendant_id, context=context) - raise_view_error(cr, uid, model, "Element '%s' not found in parent view '%%(parent_xml_id)s'" % tag, root_view_id, descendant_id, context=context) - - return source - def apply_view_inheritance(cr, user, source, inherit_id): """ Apply all the (directly and indirectly) inheriting views. @@ -2149,10 +2072,11 @@ class BaseModel(object): are applied """ + View = self.pool['ir.ui.view'] return reduce( - lambda s, descendant: apply_inheritance_specs( + lambda s, descendant: View.apply_inheritance_specs( cr, user, self._name, inherit_id, s, *descendant, context=context), - self.pool['ir.ui.view'].iter(cr, user, inherit_id, self._name, exclude_base=True, context=context), + View.iter(cr, user, inherit_id, self._name, exclude_base=True, context=context), source) result = {'type': view_type, 'model': self._name} From c212710f255690dfce23d28e925732cf5ad925da Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 23 Apr 2013 13:19:29 +0200 Subject: [PATCH 013/421] [IMP] move resolution of view_ref outside of root-ancestor-searching loop bzr revid: xmo@openerp.com-20130423111929-fs49v0xrahqsi48u --- openerp/osv/orm.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 50bdd7dfc2c..3a7215faae8 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -2085,17 +2085,16 @@ class BaseModel(object): parent_view_model = None view_ref = context.get(view_type + '_view_ref') + if view_ref and not view_id and '.' in view_ref: + module, view_ref = view_ref.split('.', 1) + cr.execute("SELECT res_id FROM ir_model_data WHERE model='ir.ui.view' AND module=%s AND name=%s", (module, view_ref)) + view_ref_res = cr.fetchone() + if view_ref_res: + view_id = view_ref_res[0] view_fields = ['arch', 'name', 'field_parent', 'id', 'type', 'inherit_id', 'model'] # Search for a root (i.e. without any parent) view. while True: - if view_ref and not view_id: - if '.' in view_ref: - module, view_ref = view_ref.split('.', 1) - cr.execute("SELECT res_id FROM ir_model_data WHERE model='ir.ui.view' AND module=%s AND name=%s", (module, view_ref)) - view_ref_res = cr.fetchone() - if view_ref_res: - view_id = view_ref_res[0] if view_id: ids = [view_id] else: From aef4ce51a72b277e6d710158eb5988d0b0725243 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 23 Apr 2013 14:46:57 +0200 Subject: [PATCH 014/421] [IMP] move fetching of root view into ir.ui.view bzr revid: xmo@openerp.com-20130423124657-zkogrodgdw0ymf2f --- openerp/addons/base/ir/ir_ui_view.py | 47 +++++++++++++++++++++++++ openerp/osv/orm.py | 52 +++++++--------------------- 2 files changed, 59 insertions(+), 40 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 06582c51f07..31bc307c3f1 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -256,6 +256,53 @@ class view(osv.osv): cr, uid, id, model, exclude_base=True, context=None): yield info + def get_root_ancestor(self, cr, uid, view_id=None, + model=None, view_type=None, context=None): + """ + Fetches the root of the view tree specified by the id or (type, model) + parameters. + + If view_id is specified, view_type and model aren't needed (and the + other way around) + + :param view_id: id of view to search the root ancestor of + :param str model: model to use the view for + :param str view_type: expected view type + :return: + """ + assert view_id or (model and view_type),\ + "caller must provide either a view_id or a model and a view_type"\ + " to be able to fetch a root view" + + view = False + # Search for a root (i.e. without any parent) view. + while True: + if view_id: + ids = [view_id] + else: + # read does not guarantee ordering so directly take just first + # search'ed id and read that, this way we don't care + ids = self.search(cr, uid, [ + ['model', '=', model], + ['type', '=', view_type], + ['inherit_id', '=', False], + ], context=context, order='priority')[:1] + views = self.read(cr, uid, ids,[ + 'arch', 'name', 'field_parent', + 'id', 'type', 'inherit_id', 'model' + ], context=context) + view = views[0] if views else False + + if not views: + break + + view_id = view['inherit_id'] or view['id'] + # due to read() inherit_id may be a name_get pair, unpack id + if isinstance(view_id, tuple): view_id, _name = view_id + if not view['inherit_id']: + break + + return view def raise_view_error(self, cr, uid, model, error_msg, view_id, child_view_id, context=None): view, child_view = self.browse(cr, uid, [view_id, child_view_id], context) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 3a7215faae8..4f71f11f47e 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -2032,9 +2032,6 @@ class BaseModel(object): return view - # - # if view_id, view_type is not required - # def fields_view_get(self, cr, user, view_id=None, view_type='form', context=None, toolbar=False, submenu=False): """ Get the detailed composition of the requested view like fields, model, view architecture @@ -2081,8 +2078,6 @@ class BaseModel(object): result = {'type': view_type, 'model': self._name} - sql_res = False - parent_view_model = None view_ref = context.get(view_type + '_view_ref') if view_ref and not view_id and '.' in view_ref: @@ -2091,42 +2086,17 @@ class BaseModel(object): view_ref_res = cr.fetchone() if view_ref_res: view_id = view_ref_res[0] - view_fields = ['arch', 'name', 'field_parent', 'id', - 'type', 'inherit_id', 'model'] - # Search for a root (i.e. without any parent) view. - while True: - if view_id: - ids = [view_id] - else: - # read does not guarantee ordering so directly take just first - # search'ed id and read that, this way we don't care - ids = View.search(cr, user, [ - ['model', '=', self._name], - ['type', '=', view_type], - ['inherit_id', '=', False], - ], context=context, order='priority')[:1] - views = View.read(cr, user, ids, view_fields, context=context) - sql_res = views[0] if views else False - if not sql_res: - break - - view_id = sql_res['inherit_id'] or sql_res['id'] - # due to read() inherit_id may be a name_get pair, unpack id - if isinstance(view_id, tuple): view_id, _name = view_id - parent_view_model = sql_res['model'] - if not sql_res['inherit_id']: - break - - # if a view was found - if sql_res: - source = etree.fromstring(encode(sql_res['arch'])) + root_view = View.get_root_ancestor( + cr, user, view_id, self._name, view_type, context=context) + if root_view: + source = etree.fromstring(encode(root_view['arch'])) result.update( - arch=apply_view_inheritance(cr, user, source, sql_res['id']), - type=sql_res['type'], - view_id=sql_res['id'], - name=sql_res['name'], - field_parent=sql_res['field_parent'] or False) + arch=apply_view_inheritance(cr, user, source, root_view['id']), + type=root_view['type'], + view_id=root_view['id'], + name=root_view['name'], + field_parent=root_view['field_parent'] or False) else: # otherwise, build some kind of default view try: @@ -2142,12 +2112,14 @@ class BaseModel(object): field_parent=False, view_id=0) + parent_view_model = root_view['model'] if root_view else None if parent_view_model != self._name: ctx = context.copy() ctx['base_model_name'] = parent_view_model else: ctx = context - xarch, xfields = self.__view_look_dom_arch(cr, user, result['arch'], view_id, context=ctx) + xarch, xfields = self.__view_look_dom_arch( + cr, user, result['arch'], result['view_id'], context=ctx) result['arch'] = xarch result['fields'] = xfields From dd43214fe169e467f1a0643ef95610187ded8a17 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 23 Apr 2013 15:25:23 +0200 Subject: [PATCH 015/421] [ADD] tests to get_root_ancestor use a default view_type of form in the tests, because whatever bzr revid: xmo@openerp.com-20130423132523-upbgbt2a3r7cgs08 --- openerp/osv/orm.py | 2 -- openerp/tests/test_views.py | 23 ++++++++++++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 4f71f11f47e..15091e9383b 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -2067,9 +2067,7 @@ class BaseModel(object): :param inherit_id: the database view_id of the parent view :return: a modified source where all the modifying architecture are applied - """ - View = self.pool['ir.ui.view'] return reduce( lambda s, descendant: View.apply_inheritance_specs( cr, user, self._name, inherit_id, s, *descendant, context=context), diff --git a/openerp/tests/test_views.py b/openerp/tests/test_views.py index 7a61aab5aa2..7c6e2a347db 100644 --- a/openerp/tests/test_views.py +++ b/openerp/tests/test_views.py @@ -104,7 +104,9 @@ class TestViewInheritance(common.TransactionCase): 'model': self.model, 'name': name, 'arch': self.view_for(name), - 'inherit_id': parent, }) + 'inherit_id': parent, + 'type': 'form', + }) self.ids[name] = view_id return view_id @@ -158,3 +160,22 @@ class TestViewInheritance(common.TransactionCase): (self.ids[name], self.view_for(name)) for name in ['A21', 'A22', 'A221'] ]) + + def test_find_root(self): + A_id = self.ids['A'] + root = self.View.get_root_ancestor(self.cr, self.uid, view_id=A_id) + self.assertEqual(root['id'], A_id, + "when given a root view, operation should be id") + + root = self.View.get_root_ancestor( + self.cr, self.uid, view_id=self.ids['A11']) + self.assertEqual(root['id'], A_id) + + root = self.View.get_root_ancestor( + self.cr, self.uid, view_id=self.ids['A221']) + self.assertEqual(root['id'], A_id) + + # search by model + root = self.View.get_root_ancestor( + self.cr, self.uid, model=self.model, view_type='form') + self.assertEqual(root['id'], A_id) From 1dac8a7a34aced0abd4097602ecfd6d498f8a02d Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 23 Apr 2013 15:36:17 +0200 Subject: [PATCH 016/421] [ADD] tests for *not* finding root views bzr revid: xmo@openerp.com-20130423133617-c4eszsf09b32hzl1 --- openerp/tests/test_views.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/openerp/tests/test_views.py b/openerp/tests/test_views.py index 7c6e2a347db..ba85b25f782 100644 --- a/openerp/tests/test_views.py +++ b/openerp/tests/test_views.py @@ -179,3 +179,16 @@ class TestViewInheritance(common.TransactionCase): root = self.View.get_root_ancestor( self.cr, self.uid, model=self.model, view_type='form') self.assertEqual(root['id'], A_id) + + def test_no_root(self): + root = self.View.get_root_ancestor( + self.cr, self.uid, model='does.not.exist', view_type='form') + self.assertFalse(root) + + root = self.View.get_root_ancestor( + self.cr, self.uid, model=self.model, view_type='tree') + self.assertFalse(root) + + root = self.View.get_root_ancestor( + self.cr, self.uid, view_id=12345678) + self.assertFalse(root) From 0676c84be548050524ad41d133355caa40c9c560 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 23 Apr 2013 15:36:40 +0200 Subject: [PATCH 017/421] [IMP] move view_id initialization out of loop, make flow simpler bzr revid: xmo@openerp.com-20130423133640-lci2djdj45ztdtu8 --- openerp/addons/base/ir/ir_ui_view.py | 38 +++++++++++++--------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 31bc307c3f1..8a806ce048f 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -274,35 +274,33 @@ class view(osv.osv): "caller must provide either a view_id or a model and a view_type"\ " to be able to fetch a root view" - view = False + if not view_id: + ids = self.search(cr, uid, [ + ['model', '=', model], + ['type', '=', view_type], + ['inherit_id', '=', False], + ], limit=1, order='priority', context=context)[:1] + if not ids: return False + [view_id] = ids + # Search for a root (i.e. without any parent) view. while True: - if view_id: - ids = [view_id] - else: - # read does not guarantee ordering so directly take just first - # search'ed id and read that, this way we don't care - ids = self.search(cr, uid, [ - ['model', '=', model], - ['type', '=', view_type], - ['inherit_id', '=', False], - ], context=context, order='priority')[:1] - views = self.read(cr, uid, ids,[ + views = self.read(cr, uid, [view_id],[ 'arch', 'name', 'field_parent', 'id', 'type', 'inherit_id', 'model' ], context=context) + if not views: return False + view = views[0] if views else False - if not views: - break - - view_id = view['inherit_id'] or view['id'] - # due to read() inherit_id may be a name_get pair, unpack id - if isinstance(view_id, tuple): view_id, _name = view_id if not view['inherit_id']: - break + return view + + view_id = view['inherit_id'] + # due to read() inherit_id may be a name_get pair, unpack id + if isinstance(view_id, tuple): + view_id, _name = view_id - return view def raise_view_error(self, cr, uid, model, error_msg, view_id, child_view_id, context=None): view, child_view = self.browse(cr, uid, [view_id, child_view_id], context) From 8914886040eda65a669a852eb4eb505e21615562 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 23 Apr 2013 15:57:39 +0200 Subject: [PATCH 018/421] [IMP] simplify ir.ui.view.get_root_ancestor by using browse records Also only case which should result in the id not existing is the initial record, if a view_id is explicitly provided. So the loop can avoid it's, it's traversing through an m2o so if the m2o value is not null the next record in the chain should always exist. bzr revid: xmo@openerp.com-20130423135739-jve1fe2it8q4gkwh --- openerp/addons/base/ir/ir_ui_view.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 8a806ce048f..c31320eb63e 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -283,23 +283,19 @@ class view(osv.osv): if not ids: return False [view_id] = ids + view = self.browse(cr, uid, view_id, context=context) + if not view.exists(): + return False + # Search for a root (i.e. without any parent) view. - while True: - views = self.read(cr, uid, [view_id],[ - 'arch', 'name', 'field_parent', - 'id', 'type', 'inherit_id', 'model' - ], context=context) - if not views: return False + while view.inherit_id: + view = view.inherit_id - view = views[0] if views else False - - if not view['inherit_id']: - return view - - view_id = view['inherit_id'] - # due to read() inherit_id may be a name_get pair, unpack id - if isinstance(view_id, tuple): - view_id, _name = view_id + views = self.read(cr, uid, [view.id],[ + 'arch', 'name', 'field_parent', + 'id', 'type', 'inherit_id', 'model' + ], context=context) + return views[0] def raise_view_error(self, cr, uid, model, error_msg, view_id, child_view_id, context=None): From d40ec67c2db57375516da621f6c0e2e630034c69 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 23 Apr 2013 17:08:44 +0200 Subject: [PATCH 019/421] [IMP] move application of view inheritance outside of fields_view_get and into View. change get_root_ancestor to return just the id of the ancestor view bzr revid: xmo@openerp.com-20130423150844-eubxue9vbtc6jhof --- openerp/addons/base/ir/ir_ui_view.py | 44 ++++++++++++++++++++++------ openerp/osv/orm.py | 28 ++++-------------- openerp/tests/test_views.py | 16 +++++----- 3 files changed, 48 insertions(+), 40 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index c31320eb63e..ef202c534ae 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -21,6 +21,7 @@ import copy import logging +import itertools from lxml import etree import os @@ -164,6 +165,35 @@ class view(osv.osv): if not cr.fetchone(): cr.execute('CREATE INDEX ir_ui_view_model_type_inherit_id ON ir_ui_view (model, inherit_id)') + def read_combined(self, cr, uid, view_id, fields=None, model=None, context=None): + """ + Reads the specified view and returns it after applying all inheriting + views upon it (essentially reads the "currently final" view) + + Returns `False` if the provided id is not a valid view, or if `False` + is provided as an id. + + The `arch` of the view is always read (regardless of its presence in + `fields`), and is returned as an lxml tree + + :param list(str) fields: same as in BaseModel.read() + :param str model: + """ + if fields and 'arch' not in fields: + fields = list(itertools.chain(['arch'], fields)) + if not view_id: return False + views = self.read(cr, uid, [view_id], fields=fields, context=context) + if not views: return False + + base_arch = views[0]['arch'] + base_arch = base_arch.encode('utf-8') if isinstance(base_arch, unicode) else base_arch + return dict(views[0], arch=reduce( + lambda current_arch, descendant: self.apply_inheritance_specs( + cr, uid, model, view_id, current_arch, *descendant, context=context), + self.iter(cr, uid, view_id, model, exclude_base=True, context=context), + etree.fromstring(base_arch) + )) + def locate_node(self, arch, spec): """ Locate a node in a source (parent) architecture. @@ -246,7 +276,7 @@ class view(osv.osv): `exclude_base` is `False`, the default) """ if not exclude_base: - [base] = self.browse(cr, uid, [view_id], context=context) + base = self.browse(cr, uid, view_id, context=context) yield base.id, base.arch for arch, id in self.get_inheriting_views_arch( @@ -259,8 +289,8 @@ class view(osv.osv): def get_root_ancestor(self, cr, uid, view_id=None, model=None, view_type=None, context=None): """ - Fetches the root of the view tree specified by the id or (type, model) - parameters. + Fetches the id of the root of the view tree specified by the id or + (type, model) parameters. If view_id is specified, view_type and model aren't needed (and the other way around) @@ -268,7 +298,7 @@ class view(osv.osv): :param view_id: id of view to search the root ancestor of :param str model: model to use the view for :param str view_type: expected view type - :return: + :return: id of the root view for the tree """ assert view_id or (model and view_type),\ "caller must provide either a view_id or a model and a view_type"\ @@ -291,11 +321,7 @@ class view(osv.osv): while view.inherit_id: view = view.inherit_id - views = self.read(cr, uid, [view.id],[ - 'arch', 'name', 'field_parent', - 'id', 'type', 'inherit_id', 'model' - ], context=context) - return views[0] + return view.id def raise_view_error(self, cr, uid, model, error_msg, view_id, child_view_id, context=None): diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 15091e9383b..2ec9025b297 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -2054,26 +2054,6 @@ class BaseModel(object): context = {} View = self.pool['ir.ui.view'] - def encode(s): - if isinstance(s, unicode): - return s.encode('utf8') - return s - - def apply_view_inheritance(cr, user, source, inherit_id): - """ Apply all the (directly and indirectly) inheriting views. - - :param source: a parent architecture to modify (with parent - modifications already applied) - :param inherit_id: the database view_id of the parent view - :return: a modified source where all the modifying architecture - are applied - """ - return reduce( - lambda s, descendant: View.apply_inheritance_specs( - cr, user, self._name, inherit_id, s, *descendant, context=context), - View.iter(cr, user, inherit_id, self._name, exclude_base=True, context=context), - source) - result = {'type': view_type, 'model': self._name} view_ref = context.get(view_type + '_view_ref') @@ -2085,12 +2065,14 @@ class BaseModel(object): if view_ref_res: view_id = view_ref_res[0] - root_view = View.get_root_ancestor( + root_view_id = View.get_root_ancestor( cr, user, view_id, self._name, view_type, context=context) + root_view = View.read_combined(cr, user, root_view_id, fields=[ + 'id', 'name', 'field_parent', 'type', 'model' + ], model=self._name, context=context) if root_view: - source = etree.fromstring(encode(root_view['arch'])) result.update( - arch=apply_view_inheritance(cr, user, source, root_view['id']), + arch=root_view['arch'], type=root_view['type'], view_id=root_view['id'], name=root_view['name'], diff --git a/openerp/tests/test_views.py b/openerp/tests/test_views.py index ba85b25f782..a7cb126fbdf 100644 --- a/openerp/tests/test_views.py +++ b/openerp/tests/test_views.py @@ -163,22 +163,22 @@ class TestViewInheritance(common.TransactionCase): def test_find_root(self): A_id = self.ids['A'] - root = self.View.get_root_ancestor(self.cr, self.uid, view_id=A_id) - self.assertEqual(root['id'], A_id, + root_id = self.View.get_root_ancestor(self.cr, self.uid, view_id=A_id) + self.assertEqual(root_id, A_id, "when given a root view, operation should be id") - root = self.View.get_root_ancestor( + root_id = self.View.get_root_ancestor( self.cr, self.uid, view_id=self.ids['A11']) - self.assertEqual(root['id'], A_id) + self.assertEqual(root_id, A_id) - root = self.View.get_root_ancestor( + root_id = self.View.get_root_ancestor( self.cr, self.uid, view_id=self.ids['A221']) - self.assertEqual(root['id'], A_id) + self.assertEqual(root_id, A_id) # search by model - root = self.View.get_root_ancestor( + root_id = self.View.get_root_ancestor( self.cr, self.uid, model=self.model, view_type='form') - self.assertEqual(root['id'], A_id) + self.assertEqual(root_id, A_id) def test_no_root(self): root = self.View.get_root_ancestor( From 3ac47a587e103dbf8a647d51e5a72b6f13414880 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 23 Apr 2013 17:44:56 +0200 Subject: [PATCH 020/421] [IMP] remove deprecated explicit setting of type on views bzr revid: xmo@openerp.com-20130423154456-wt4aeb6yr963jbye --- openerp/osv/orm.py | 2 +- openerp/tests/test_views.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 2ec9025b297..f356e731143 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -2068,7 +2068,7 @@ class BaseModel(object): root_view_id = View.get_root_ancestor( cr, user, view_id, self._name, view_type, context=context) root_view = View.read_combined(cr, user, root_view_id, fields=[ - 'id', 'name', 'field_parent', 'type', 'model' + 'id', 'name', 'field_parent', 'type', 'model', 'arch' ], model=self._name, context=context) if root_view: result.update( diff --git a/openerp/tests/test_views.py b/openerp/tests/test_views.py index a7cb126fbdf..b7a454f1f27 100644 --- a/openerp/tests/test_views.py +++ b/openerp/tests/test_views.py @@ -105,7 +105,6 @@ class TestViewInheritance(common.TransactionCase): 'name': name, 'arch': self.view_for(name), 'inherit_id': parent, - 'type': 'form', }) self.ids[name] = view_id return view_id From 871574667261c2a4d676fac419b2ebd52780e729 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Wed, 24 Apr 2013 11:51:14 +0200 Subject: [PATCH 021/421] [IMP] move stuff around, try building flatter top-down API with read_combined as utility 'do all the things' hook bzr revid: xmo@openerp.com-20130424095114-4nw0nfuxzx4n1ltv --- openerp/addons/base/ir/ir_ui_view.py | 130 ++++++++++++++++----------- openerp/osv/orm.py | 9 +- openerp/tests/test_views.py | 84 +++++++++++------ 3 files changed, 138 insertions(+), 85 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index caba7dd59a1..259405288d9 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -165,34 +165,47 @@ class view(osv.osv): if not cr.fetchone(): cr.execute('CREATE INDEX ir_ui_view_model_type_inherit_id ON ir_ui_view (model, inherit_id)') - def read_combined(self, cr, uid, view_id, fields=None, model=None, context=None): + def read_combined(self, cr, uid, view_id, view_type, model, + fields=None, context=None): """ - Reads the specified view and returns it after applying all inheriting - views upon it (essentially reads the "currently final" view) + Utility function stringing together all method calls necessary to get + a full, final view: - Returns `False` if the provided id is not a valid view, or if `False` - is provided as an id. + * Gets the default view if no view_id is provided + * Gets the top of the view tree if a sub-view is requested + * Applies all inherited archs on the root view + * Applies post-processing both generic (ir.ui.view) and specific + (ir.ui.view.$type) + * Returns the view with all requested fields - The `arch` of the view is always read (regardless of its presence in - `fields`), and is returned as an lxml tree + .. note:: ``arch`` is always added to the fields list even if not + requested (similar to ``id``) - :param list(str) fields: same as in BaseModel.read() - :param str model: + If no view is available (no view_id or invalid view_id provided, or + no view stored for (model, view_type)) a view record will be fetched + from the ``defaults`` mapping. """ + if not view_id: + view_id = self.default_view(cr, uid, model, view_type, context=context) + root_id = self.root_ancestor(cr, uid, view_id, context=context) + if fields and 'arch' not in fields: fields = list(itertools.chain(['arch'], fields)) - if not view_id: return False - views = self.read(cr, uid, [view_id], fields=fields, context=context) - if not views: return False - base_arch = views[0]['arch'] - base_arch = base_arch.encode('utf-8') if isinstance(base_arch, unicode) else base_arch - return dict(views[0], arch=reduce( - lambda current_arch, descendant: self.apply_inheritance_specs( - cr, uid, model, view_id, current_arch, *descendant, context=context), - self.iter(cr, uid, view_id, model, exclude_base=True, context=context), - etree.fromstring(base_arch) - )) + [view] = self.read(cr, uid, [root_id], fields=fields, context=context) + + arch_tree = etree.fromstring( + view['arch'].encode('utf-8') if isinstance(view['arch'], unicode) + else view['arch']) + descendants = self.iter( + cr, uid, view['id'], model, exclude_base=True, context=context) + arch = self.apply_inherited_archs( + cr, uid, arch_tree, descendants, + model, view['id'], context=context) + + # TODO: post-processing + + return dict(view, arch=arch)#etree.tostring(arch, 'utf-8')) def locate_node(self, arch, spec): """ Locate a node in a source (parent) architecture. @@ -262,18 +275,18 @@ class view(osv.osv): if not (view.groups_id and user_groups.isdisjoint(view.groups_id))] def iter(self, cr, uid, view_id, model, exclude_base=False, context=None): - """ iterates on all of `view_id`'s descendants tree depth-first. + """ iterates on all of ``view_id``'s descendants tree depth-first. - If `exclude_base` is `False`, also yields `view_id` itself. It is - `False` by default to match the behavior of etree's Element.iter. + If ``exclude_base`` is ``False``, also yields ``view_id`` itself. It is + ``False`` by default to match the behavior of etree's Element.iter. :param int view_id: database id of the root view :param str model: name of the view's related model (for filtering) - :param boolean exclude_base: whether `view_id` should be excluded from the iteration - :param context: - :return: iterator of (database_id, arch_field) pairs for all - descendants of `view_id` (including `view_id` itself if - `exclude_base` is `False`, the default) + :param bool exclude_base: whether ``view_id`` should be excluded + from the iteration + :return: iterator of (database_id, arch_string) pairs for all + descendants of ``view_id`` (including ``view_id`` itself if + ``exclude_base`` is ``False``, the default) """ if not exclude_base: base = self.browse(cr, uid, view_id, context=context) @@ -286,36 +299,32 @@ class view(osv.osv): cr, uid, id, model, exclude_base=True, context=None): yield info - def get_root_ancestor(self, cr, uid, view_id=None, - model=None, view_type=None, context=None): + def default_view(self, cr, uid, model, view_type, context=None): + """ Fetches the default view for the provided (model, view_type) pair: + view with no parent (inherit_id=Fase) with the lowest priority. + + :param str model: + :param int view_type: + :return: id of the default view for the (model, view_type) pair + :rtype: int """ - Fetches the id of the root of the view tree specified by the id or - (type, model) parameters. + return self.search(cr, uid, [ + ['model', '=', model], + ['type', '=', view_type], + ['inherit_id', '=', False], + ], limit=1, order='priority', context=context)[0] + + def root_ancestor(self, cr, uid, view_id, context=None): + """ + Fetches the id of the root of the view tree of which view_id is part If view_id is specified, view_type and model aren't needed (and the other way around) :param view_id: id of view to search the root ancestor of - :param str model: model to use the view for - :param str view_type: expected view type :return: id of the root view for the tree """ - assert view_id or (model and view_type),\ - "caller must provide either a view_id or a model and a view_type"\ - " to be able to fetch a root view" - - if not view_id: - ids = self.search(cr, uid, [ - ['model', '=', model], - ['type', '=', view_type], - ['inherit_id', '=', False], - ], limit=1, order='priority', context=context)[:1] - if not ids: return False - [view_id] = ids - view = self.browse(cr, uid, view_id, context=context) - if not view.exists(): - return False # Search for a root (i.e. without any parent) view. while view.inherit_id: @@ -323,6 +332,25 @@ class view(osv.osv): return view.id + def apply_inherited_archs(self, cr, uid, source, descendants, + model, source_view_id, context=None): + """ Applies descendants to the ``source`` view, returns the result of + the application. + + :param Element source: source arch to apply descendant on + :param descendants: iterable of (id, arch_string) pairs of all + descendants in the view tree, depth-first, + excluding the base view + :type descendants: iter((int, str)) + :return: new architecture etree produced by applying all descendants + on ``source`` + :rtype: Element + """ + return reduce( + lambda current_arch, descendant: self.apply_inheritance_specs( + cr, uid, model, source_view_id, current_arch, + *descendant, context=context), + descendants, source) def raise_view_error(self, cr, uid, model, error_msg, view_id, child_view_id, context=None): view, child_view = self.browse(cr, uid, [view_id, child_view_id], context) @@ -337,11 +365,11 @@ class view(osv.osv): describing where and what changes to apply to some parent architecture) given by an inheriting view. - :param source: a parent architecture to modify + :param Element source: a parent architecture to modify :param descendant_id: the database id of the descendant :param specs_arch: a modifying architecture in an inheriting view :return: a modified source where the specs are applied - + :rtype: Element """ if isinstance(specs_arch, unicode): specs_arch = specs_arch.encode('utf-8') diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index b8bdb050bab..1794a6646fc 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -2065,11 +2065,10 @@ class BaseModel(object): if view_ref_res: view_id = view_ref_res[0] - root_view_id = View.get_root_ancestor( - cr, user, view_id, self._name, view_type, context=context) - root_view = View.read_combined(cr, user, root_view_id, fields=[ - 'id', 'name', 'field_parent', 'type', 'model', 'arch' - ], model=self._name, context=context) + root_view = View.read_combined( + cr, user, view_id, view_type, self._name, fields=[ + 'id', 'name', 'field_parent', 'type', 'model', 'arch' + ], context=context) if root_view: result.update( arch=root_view['arch'], diff --git a/openerp/tests/test_views.py b/openerp/tests/test_views.py index b7a454f1f27..6a555ed089b 100644 --- a/openerp/tests/test_views.py +++ b/openerp/tests/test_views.py @@ -2,6 +2,7 @@ from lxml import etree as ET from lxml.builder import E from . import common +import unittest2 Field = E.field @@ -96,14 +97,14 @@ class TestNodeLocator(common.BaseCase): self.assertIsNone(node) class TestViewInheritance(common.TransactionCase): - def view_for(self, name): - return ET.tostring(ET.Element('form', string=name)) + def arch_for(self, name, view_type='form'): + return ET.tostring(ET.Element(view_type, string=name)) - def makeView(self, name, parent=None): + def makeView(self, name, parent=None, arch=None): view_id = self.View.create(self.cr, self.uid, { 'model': self.model, 'name': name, - 'arch': self.view_for(name), + 'arch': arch or self.arch_for(name), 'inherit_id': parent, }) self.ids[name] = view_id @@ -128,15 +129,20 @@ class TestViewInheritance(common.TransactionCase): a22 = self.makeView("A22", a2) self.makeView("A221", a22) + b = self.makeView('B', arch=self.arch_for("B", 'tree')) + self.makeView('B1', b, arch=self.arch_for("B1", 'tree')) + c = self.makeView('C', arch=self.arch_for("C", 'tree')) + self.View.write(self.cr, self.uid, c, {'priority': 1}) + def tearDown(self): self.View.pool._init = self._init super(TestViewInheritance, self).tearDown() - def test_get_children(self): + def test_get_inheriting_views_arch(self): self.assertEqual(self.View.get_inheriting_views_arch( self.cr, self.uid, self.ids['A'], self.model), [ - (self.view_for('A1'), self.ids['A1']), - (self.view_for('A2'), self.ids['A2']), + (self.arch_for('A1'), self.ids['A1']), + (self.arch_for('A2'), self.ids['A2']), ]) self.assertEqual(self.View.get_inheriting_views_arch( @@ -145,49 +151,69 @@ class TestViewInheritance(common.TransactionCase): self.assertEqual(self.View.get_inheriting_views_arch( self.cr, self.uid, self.ids['A11'], self.model), - [(self.view_for('A111'), self.ids['A111'])]) + [(self.arch_for('A111'), self.ids['A111'])]) - def test_iterate_descendents(self): + def test_iter(self): descendents = list(self.View.iter(self.cr, self.uid, self.ids['A1'], self.model)) self.assertEqual(descendents, [ - (self.ids[name], self.view_for(name)) + (self.ids[name], self.arch_for(name)) for name in ['A1', 'A11', 'A111', 'A12'] ]) descendents = list(self.View.iter( self.cr, self.uid, self.ids['A2'], self.model, exclude_base=True)) self.assertEqual(descendents, [ - (self.ids[name], self.view_for(name)) + (self.ids[name], self.arch_for(name)) for name in ['A21', 'A22', 'A221'] ]) - def test_find_root(self): + def test_root_ancestor(self): A_id = self.ids['A'] - root_id = self.View.get_root_ancestor(self.cr, self.uid, view_id=A_id) + root_id = self.View.root_ancestor(self.cr, self.uid, view_id=A_id) self.assertEqual(root_id, A_id, "when given a root view, operation should be id") - root_id = self.View.get_root_ancestor( + root_id = self.View.root_ancestor( self.cr, self.uid, view_id=self.ids['A11']) self.assertEqual(root_id, A_id) - root_id = self.View.get_root_ancestor( + root_id = self.View.root_ancestor( self.cr, self.uid, view_id=self.ids['A221']) self.assertEqual(root_id, A_id) - # search by model - root_id = self.View.get_root_ancestor( - self.cr, self.uid, model=self.model, view_type='form') - self.assertEqual(root_id, A_id) + root_id = self.View.root_ancestor( + self.cr, self.uid, view_id=self.ids['B1']) + self.assertEqual(root_id, self.ids['B']) - def test_no_root(self): - root = self.View.get_root_ancestor( - self.cr, self.uid, model='does.not.exist', view_type='form') - self.assertFalse(root) - - root = self.View.get_root_ancestor( - self.cr, self.uid, model=self.model, view_type='tree') - self.assertFalse(root) - - root = self.View.get_root_ancestor( + @unittest2.skip("What should the behavior be when no ancestor is found " + "because view_id is invalid?") + def test_no_root_ancestor(self): + root = self.View.root_ancestor( self.cr, self.uid, view_id=12345678) self.assertFalse(root) + + def test_default_view(self): + default = self.View.default_view( + self.cr, self.uid, model=self.model, view_type='form') + self.assertEqual(default, self.ids['A']) + + default_tree = self.View.default_view( + self.cr, self.uid, model=self.model, view_type='tree') + self.assertEqual(default_tree, self.ids['C']) + + @unittest2.skip("What should the behavior be when no default is found " + "because model does not exist or no view for model?") + def test_no_default_view(self): + default = self.View.default_view( + self.cr, self.uid, model='does.not.exist', view_type='form') + self.assertFalse(default) + + default = self.View.default_view( + self.cr, self.uid, model=self.model, view_type='graph') + self.assertFalse(default) + +class TestViewCombined(common.TransactionCase): + """ + Test fallback operations of View.read_combined: + * defaults mapping + * ? + """ From fb492d98baec3ac8ebc06930801ae6736b109c79 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Wed, 24 Apr 2013 15:09:07 +0200 Subject: [PATCH 022/421] [ADD] behavior and tests for default & ancestors, fallback handling in read_combined bzr revid: xmo@openerp.com-20130424130907-6d6lkrkrgqv0hsfq --- openerp/addons/base/ir/ir_ui_view.py | 71 ++++++++++++++++++++-------- openerp/osv/orm.py | 65 +++++++++++++------------ openerp/tests/test_views.py | 29 +++++++----- 3 files changed, 101 insertions(+), 64 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 259405288d9..c91245930b0 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -29,6 +29,7 @@ from openerp import tools from openerp.osv import fields,osv from openerp.tools import graph, SKIPPED_ELEMENT_TYPES from openerp.tools.safe_eval import safe_eval as eval +from openerp.tools.translate import _ from openerp.tools.view_validation import valid_view _logger = logging.getLogger(__name__) @@ -51,6 +52,9 @@ class view_custom(osv.osv): class view(osv.osv): _name = 'ir.ui.view' + class NoViewError(Exception): pass + class NoDefaultError(NoViewError): pass + def _type_field(self, cr, uid, ids, name, args, context=None): result = {} for record in self.browse(cr, uid, ids, context): @@ -166,7 +170,7 @@ class view(osv.osv): cr.execute('CREATE INDEX ir_ui_view_model_type_inherit_id ON ir_ui_view (model, inherit_id)') def read_combined(self, cr, uid, view_id, view_type, model, - fields=None, context=None): + fields=None, fallback=None, context=None): """ Utility function stringing together all method calls necessary to get a full, final view: @@ -174,8 +178,7 @@ class view(osv.osv): * Gets the default view if no view_id is provided * Gets the top of the view tree if a sub-view is requested * Applies all inherited archs on the root view - * Applies post-processing both generic (ir.ui.view) and specific - (ir.ui.view.$type) + * Applies post-processing * Returns the view with all requested fields .. note:: ``arch`` is always added to the fields list even if not @@ -183,29 +186,47 @@ class view(osv.osv): If no view is available (no view_id or invalid view_id provided, or no view stored for (model, view_type)) a view record will be fetched - from the ``defaults`` mapping. + from the ``defaults`` mapping? + + :param fallback: a mapping of {view_type: view_dict}, if no view can + be found (read) will be used to provide a default + before post-processing + :type fallback: mapping """ - if not view_id: - view_id = self.default_view(cr, uid, model, view_type, context=context) - root_id = self.root_ancestor(cr, uid, view_id, context=context) + if context is None: context = {} + try: + if not view_id: + view_id = self.default_view(cr, uid, model, view_type, context=context) + root_id = self.root_ancestor(cr, uid, view_id, context=context) - if fields and 'arch' not in fields: - fields = list(itertools.chain(['arch'], fields)) + if fields and 'arch' not in fields: + fields = list(itertools.chain(['arch'], fields)) - [view] = self.read(cr, uid, [root_id], fields=fields, context=context) + [view] = self.read(cr, uid, [root_id], fields=fields, context=context) - arch_tree = etree.fromstring( - view['arch'].encode('utf-8') if isinstance(view['arch'], unicode) - else view['arch']) - descendants = self.iter( - cr, uid, view['id'], model, exclude_base=True, context=context) - arch = self.apply_inherited_archs( - cr, uid, arch_tree, descendants, - model, view['id'], context=context) + arch_tree = etree.fromstring( + view['arch'].encode('utf-8') if isinstance(view['arch'], unicode) + else view['arch']) + descendants = self.iter( + cr, uid, view['id'], model, exclude_base=True, context=context) + arch = self.apply_inherited_archs( + cr, uid, arch_tree, descendants, + model, view['id'], context=context) + + if view['model'] != model: + context = dict(context, base_model_name=view['model']) + except self.NoViewError: + # defaultdict is "empty" until first __getattr__ + if fallback is None: raise + view = fallback[view_type] + arch = view['arch'] + if isinstance(arch, basestring): + arch = etree.fromstring( + arch.encode('utf-8') if isinstance(arch, unicode) else arch) # TODO: post-processing - return dict(view, arch=arch)#etree.tostring(arch, 'utf-8')) + return dict(view, arch=etree.tostring(arch, encoding='utf-8')) def locate_node(self, arch, spec): """ Locate a node in a source (parent) architecture. @@ -308,11 +329,16 @@ class view(osv.osv): :return: id of the default view for the (model, view_type) pair :rtype: int """ - return self.search(cr, uid, [ + ids = self.search(cr, uid, [ ['model', '=', model], ['type', '=', view_type], ['inherit_id', '=', False], - ], limit=1, order='priority', context=context)[0] + ], limit=1, order='priority', context=context) + if not ids: + raise self.NoDefaultError( + _("No default view of type %s for model %s") % ( + view_type, model)) + return ids[0] def root_ancestor(self, cr, uid, view_id, context=None): """ @@ -325,6 +351,9 @@ class view(osv.osv): :return: id of the root view for the tree """ view = self.browse(cr, uid, view_id, context=context) + if not view.exists(): + raise self.NoViewError( + _("No view for id %s, root ancestor not available") % view_id) # Search for a root (i.e. without any parent) view. while view.inherit_id: diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 1794a6646fc..ec161ad8573 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -631,6 +631,27 @@ class MetaModel(type): if not self._custom: self.module_to_models.setdefault(self._module, []).append(self) +class FallbackViewMapping(collections.defaultdict): + def __init__(self, cr, uid, model, context=None): + super(FallbackViewMapping, self).__init__() + self.cr = cr + self.uid = uid + self.model = model + self.context = context + + def __missing__(self, view_type): + try: + arch = getattr(self.model, '_get_default_%s_view' % view_type)( + self.cr, self.uid, self.context) + except AttributeError: + raise except_orm(_('Invalid Architecture!'), _("There is no view of type '%s' defined for the structure!") % view_type) + return { + 'id': 0, + 'type': view_type, + 'name': 'default', + 'field_parent': False, + 'arch': arch, + } # Definition of log access columns, automatically added to models if # self._log_access is True @@ -2054,8 +2075,6 @@ class BaseModel(object): context = {} View = self.pool['ir.ui.view'] - result = {'type': view_type, 'model': self._name} - view_ref = context.get(view_type + '_view_ref') if view_ref and not view_id and '.' in view_ref: @@ -2068,37 +2087,23 @@ class BaseModel(object): root_view = View.read_combined( cr, user, view_id, view_type, self._name, fields=[ 'id', 'name', 'field_parent', 'type', 'model', 'arch' - ], context=context) - if root_view: - result.update( - arch=root_view['arch'], - type=root_view['type'], - view_id=root_view['id'], - name=root_view['name'], - field_parent=root_view['field_parent'] or False) - else: - # otherwise, build some kind of default view - try: - view = getattr(self, '_get_default_%s_view' % view_type)( - cr, user, context) - except AttributeError: - # what happens here, graph case? - raise except_orm(_('Invalid Architecture!'), _("There is no view of type '%s' defined for the structure!") % view_type) + ], fallback=FallbackViewMapping(cr, user, self, context=context), + context=context) - result.update( - arch=view, - name='default', - field_parent=False, - view_id=0) + result = { + 'model': self._name, + 'arch': root_view['arch'], + 'type': root_view['type'], + 'view_id': root_view['id'], + 'name': root_view['name'], + 'field_parent': root_view['field_parent'] or False + } - parent_view_model = root_view['model'] if root_view else None - if parent_view_model != self._name: - ctx = context.copy() - ctx['base_model_name'] = parent_view_model - else: - ctx = context + ctx = context + if root_view.get('model') != self._name: + ctx = dict(context, base_model_name=root_view.get('model')) xarch, xfields = self.__view_look_dom_arch( - cr, user, result['arch'], result['view_id'], context=ctx) + cr, user, etree.fromstring(result['arch']), result['view_id'], context=ctx) result['arch'] = xarch result['fields'] = xfields diff --git a/openerp/tests/test_views.py b/openerp/tests/test_views.py index 6a555ed089b..bedb3e78b19 100644 --- a/openerp/tests/test_views.py +++ b/openerp/tests/test_views.py @@ -184,12 +184,9 @@ class TestViewInheritance(common.TransactionCase): self.cr, self.uid, view_id=self.ids['B1']) self.assertEqual(root_id, self.ids['B']) - @unittest2.skip("What should the behavior be when no ancestor is found " - "because view_id is invalid?") def test_no_root_ancestor(self): - root = self.View.root_ancestor( - self.cr, self.uid, view_id=12345678) - self.assertFalse(root) + with self.assertRaises(self.View.NoViewError): + self.View.root_ancestor(self.cr, self.uid, view_id=12345678) def test_default_view(self): default = self.View.default_view( @@ -200,16 +197,22 @@ class TestViewInheritance(common.TransactionCase): self.cr, self.uid, model=self.model, view_type='tree') self.assertEqual(default_tree, self.ids['C']) - @unittest2.skip("What should the behavior be when no default is found " - "because model does not exist or no view for model?") def test_no_default_view(self): - default = self.View.default_view( - self.cr, self.uid, model='does.not.exist', view_type='form') - self.assertFalse(default) + with self.assertRaises(self.View.NoDefaultError): + self.View.default_view( + self.cr, self.uid, model='does.not.exist', view_type='form') - default = self.View.default_view( - self.cr, self.uid, model=self.model, view_type='graph') - self.assertFalse(default) + with self.assertRaises(self.View.NoDefaultError): + self.View.default_view( + self.cr, self.uid, model=self.model, view_type='graph') + + @unittest2.skip("Not tested") + def test_apply_inherited_archs(self): + self.fail() + + @unittest2.skip("Not tested") + def test_apply_inheritance_specs(self): + self.fail() class TestViewCombined(common.TransactionCase): """ From 66121aaec7804a8eb24963ca9f3ec12babfe6fd9 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Wed, 24 Apr 2013 15:37:16 +0200 Subject: [PATCH 023/421] [IMP] try to make __view_look_dom_arch independent from model bzr revid: xmo@openerp.com-20130424133716-6qm0n00yiussz1ra --- openerp/osv/orm.py | 54 +++++++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index ec161ad8573..cd1a7f024ae 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -1697,7 +1697,7 @@ class BaseModel(object): return any([self.pool.get('res.users').has_group(cr, uid, group_ext_id) for group_ext_id in groups.split(',')]) - def __view_look_dom(self, cr, user, node, view_id, in_tree_view, model_fields, context=None): + def __view_look_dom(self, cr, user, model, node, view_id, in_tree_view, model_fields, context=None): """Return the description of the fields in the node. In a normal call to this method, node is a complete view architecture @@ -1716,6 +1716,7 @@ class BaseModel(object): children = True modifiers = {} + Model = self.pool[model] def encode(s): if isinstance(s, unicode): @@ -1732,8 +1733,8 @@ class BaseModel(object): :return: True if field should be included in the result of fields_view_get """ - if node.tag == 'field' and node.get('name') in self._all_columns: - column = self._all_columns[node.get('name')].column + if node.tag == 'field' and node.get('name') in Model._all_columns: + column = Model._all_columns[node.get('name')].column if column.groups and not self.user_has_groups(cr, user, groups=column.groups, context=context): @@ -1764,8 +1765,8 @@ class BaseModel(object): xml += "" new_xml = etree.fromstring(encode(xml)) ctx = context.copy() - ctx['base_model_name'] = self._name - xarch, xfields = self.pool[node.get('object')].__view_look_dom_arch(cr, user, new_xml, view_id, ctx) + ctx['base_model_name'] = model + xarch, xfields = self.__view_look_dom_arch(cr, user, node.get('object'), new_xml, view_id, ctx) views['form'] = { 'arch': xarch, 'fields': xfields @@ -1775,10 +1776,10 @@ class BaseModel(object): if node.get('name'): attrs = {} try: - if node.get('name') in self._columns: - column = self._columns[node.get('name')] + if node.get('name') in Model._columns: + column = Model._columns[node.get('name')] else: - column = self._inherit_fields[node.get('name')][2] + column = Model._inherit_fields[node.get('name')][2] except Exception: column = False @@ -1791,8 +1792,8 @@ class BaseModel(object): if f.tag in ('form', 'tree', 'graph', 'kanban'): node.remove(f) ctx = context.copy() - ctx['base_model_name'] = self._name - xarch, xfields = relation.__view_look_dom_arch(cr, user, f, view_id, ctx) + ctx['base_model_name'] = Model + xarch, xfields = self.__view_look_dom_arch(cr, user, column._obj or None, f, view_id, ctx) views[str(f.tag)] = { 'arch': xarch, 'fields': xfields @@ -1826,7 +1827,7 @@ class BaseModel(object): elif node.tag in ('form', 'tree'): - result = self.view_header_get(cr, user, False, node.tag, context) + result = Model.view_header_get(cr, user, False, node.tag, context) if result: node.set('string', result) in_tree_view = node.tag == 'tree' @@ -1848,39 +1849,40 @@ class BaseModel(object): # translate view if 'lang' in context: + Translations = self.pool['ir.translation'] if node.text and node.text.strip(): - trans = self.pool.get('ir.translation')._get_source(cr, user, self._name, 'view', context['lang'], node.text.strip()) + trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.text.strip()) if trans: node.text = node.text.replace(node.text.strip(), trans) if node.tail and node.tail.strip(): - trans = self.pool.get('ir.translation')._get_source(cr, user, self._name, 'view', context['lang'], node.tail.strip()) + trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.tail.strip()) if trans: node.tail = node.tail.replace(node.tail.strip(), trans) if node.get('string') and not result: - trans = self.pool.get('ir.translation')._get_source(cr, user, self._name, 'view', context['lang'], node.get('string')) + trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.get('string')) if trans == node.get('string') and ('base_model_name' in context): # If translation is same as source, perhaps we'd have more luck with the alternative model name # (in case we are in a mixed situation, such as an inherited view where parent_view.model != model - trans = self.pool.get('ir.translation')._get_source(cr, user, context['base_model_name'], 'view', context['lang'], node.get('string')) + trans = Translations._get_source(cr, user, context['base_model_name'], 'view', context['lang'], node.get('string')) if trans: node.set('string', trans) for attr_name in ('confirm', 'sum', 'avg', 'help', 'placeholder'): attr_value = node.get(attr_name) if attr_value: - trans = self.pool.get('ir.translation')._get_source(cr, user, self._name, 'view', context['lang'], attr_value) + trans = Translations._get_source(cr, user, model, 'view', context['lang'], attr_value) if trans: node.set(attr_name, trans) for f in node: if children or (node.tag == 'field' and f.tag in ('filter','separator')): - fields.update(self.__view_look_dom(cr, user, f, view_id, in_tree_view, model_fields, context)) + fields.update(self.__view_look_dom(cr, user, model, f, view_id, in_tree_view, model_fields, context)) transfer_modifiers_to_node(modifiers, node) return fields - def _disable_workflow_buttons(self, cr, user, node): + def _disable_workflow_buttons(self, cr, user, model, node): """ Set the buttons in node to readonly if the user can't activate them. """ if user == 1: # admin user can always activate workflow buttons @@ -1899,13 +1901,13 @@ class BaseModel(object): WHERE wkf.osv = %s AND t.signal = %s AND t.group_id is NOT NULL - """, (self._name, button.get('name'))) + """, (model, button.get('name'))) group_ids = [x[0] for x in cr.fetchall() if x[0]] can_click = not group_ids or bool(set(user_groups).intersection(group_ids)) button.set('readonly', str(int(not can_click))) return node - def __view_look_dom_arch(self, cr, user, node, view_id, context=None): + def __view_look_dom_arch(self, cr, user, model, node, view_id, context=None): """ Return an architecture and a description of all the fields. The field description combines the result of fields_get() and @@ -1917,6 +1919,7 @@ class BaseModel(object): """ fields = {} + Model = self.pool[model] if node.tag == 'diagram': if node.getchildren()[0].tag == 'node': node_model = self.pool[node.getchildren()[0].get('object')] @@ -1928,12 +1931,12 @@ class BaseModel(object): arrow_fields = self.pool[node.getchildren()[1].get('object')].fields_get(cr, user, None, context) fields.update(arrow_fields) else: - fields = self.fields_get(cr, user, None, context) - fields_def = self.__view_look_dom(cr, user, node, view_id, False, fields, context=context) - node = self._disable_workflow_buttons(cr, user, node) + fields = Model.fields_get(cr, user, None, context) + fields_def = self.__view_look_dom(cr, user, model, node, view_id, False, fields, context=context) + node = self._disable_workflow_buttons(cr, user, model, node) if node.tag in ('kanban', 'tree', 'form', 'gantt'): for action, operation in (('create', 'create'), ('delete', 'unlink'), ('edit', 'write')): - if not node.get(action) and not self.check_access_rights(cr, user, operation, raise_exception=False): + if not node.get(action) and not Model.check_access_rights(cr, user, operation, raise_exception=False): node.set(action, 'false') arch = etree.tostring(node, encoding="utf-8").replace('\t', '') for k in fields.keys(): @@ -2103,7 +2106,8 @@ class BaseModel(object): if root_view.get('model') != self._name: ctx = dict(context, base_model_name=root_view.get('model')) xarch, xfields = self.__view_look_dom_arch( - cr, user, etree.fromstring(result['arch']), result['view_id'], context=ctx) + cr, user, self._name, etree.fromstring(result['arch']), + result['view_id'], context=ctx) result['arch'] = xarch result['fields'] = xfields From c08513763cec5b73fce895a628c73cc5fbe172ff Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Wed, 24 Apr 2013 15:58:25 +0200 Subject: [PATCH 024/421] [NOTSURE] move __view_look_dom_arch into ir.ui.view Also alter BaseModel._view_look_dom_arch to proxy to method above. bzr revid: xmo@openerp.com-20130424135825-grjfgbjuc4ozzdqy --- openerp/addons/base/ir/ir_ui_view.py | 266 +++++++++++++++++++++++++- openerp/osv/orm.py | 268 +-------------------------- 2 files changed, 269 insertions(+), 265 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index c91245930b0..db6447bdeac 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -24,9 +24,10 @@ import logging import itertools from lxml import etree import os +import time from openerp import tools -from openerp.osv import fields,osv +from openerp.osv import fields, osv, orm from openerp.tools import graph, SKIPPED_ELEMENT_TYPES from openerp.tools.safe_eval import safe_eval as eval from openerp.tools.translate import _ @@ -539,6 +540,269 @@ class view(osv.osv): 'blank_nodes': blank_nodes, 'node_parent_field': _Model_Field,} + # this really needs to be refixtored + def __view_look_dom(self, cr, user, model, node, view_id, in_tree_view, model_fields, context=None): + """Return the description of the fields in the node. + + In a normal call to this method, node is a complete view architecture + but it is actually possible to give some sub-node (this is used so + that the method can call itself recursively). + + Originally, the field descriptions are drawn from the node itself. + But there is now some code calling fields_get() in order to merge some + of those information in the architecture. + + """ + if context is None: + context = {} + result = False + fields = {} + children = True + + modifiers = {} + Model = self.pool[model] + + def encode(s): + if isinstance(s, unicode): + return s.encode('utf8') + return s + + def check_group(node): + """Apply group restrictions, may be set at view level or model level:: + * at view level this means the element should be made invisible to + people who are not members + * at model level (exclusively for fields, obviously), this means + the field should be completely removed from the view, as it is + completely unavailable for non-members + + :return: True if field should be included in the result of fields_view_get + """ + if node.tag == 'field' and node.get('name') in Model._all_columns: + column = Model._all_columns[node.get('name')].column + if column.groups and not self.user_has_groups(cr, user, + groups=column.groups, + context=context): + node.getparent().remove(node) + fields.pop(node.get('name'), None) + # no point processing view-level ``groups`` anymore, return + return False + if node.get('groups'): + can_see = self.user_has_groups(cr, user, + groups=node.get('groups'), + context=context) + if not can_see: + node.set('invisible', '1') + modifiers['invisible'] = True + if 'attrs' in node.attrib: + del(node.attrib['attrs']) #avoid making field visible later + del(node.attrib['groups']) + return True + + if node.tag in ('field', 'node', 'arrow'): + if node.get('object'): + attrs = {} + views = {} + xml = "
" + for f in node: + if f.tag == 'field': + xml += etree.tostring(f, encoding="utf-8") + xml += "
" + new_xml = etree.fromstring(encode(xml)) + ctx = context.copy() + ctx['base_model_name'] = model + xarch, xfields = self.__view_look_dom_arch(cr, user, node.get('object'), new_xml, view_id, ctx) + views['form'] = { + 'arch': xarch, + 'fields': xfields + } + attrs = {'views': views} + fields = xfields + if node.get('name'): + attrs = {} + try: + if node.get('name') in Model._columns: + column = Model._columns[node.get('name')] + else: + column = Model._inherit_fields[node.get('name')][2] + except Exception: + column = False + + if column: + relation = self.pool[column._obj] if column._obj else None + + children = False + views = {} + for f in node: + if f.tag in ('form', 'tree', 'graph', 'kanban'): + node.remove(f) + ctx = context.copy() + ctx['base_model_name'] = Model + xarch, xfields = self.__view_look_dom_arch(cr, user, column._obj or None, f, view_id, ctx) + views[str(f.tag)] = { + 'arch': xarch, + 'fields': xfields + } + attrs = {'views': views} + if node.get('widget') and node.get('widget') == 'selection': + # Prepare the cached selection list for the client. This needs to be + # done even when the field is invisible to the current user, because + # other events could need to change its value to any of the selectable ones + # (such as on_change events, refreshes, etc.) + + # If domain and context are strings, we keep them for client-side, otherwise + # we evaluate them server-side to consider them when generating the list of + # possible values + # TODO: find a way to remove this hack, by allow dynamic domains + dom = [] + if column._domain and not isinstance(column._domain, basestring): + dom = list(column._domain) + dom += eval(node.get('domain', '[]'), {'uid': user, 'time': time}) + search_context = dict(context) + if column._context and not isinstance(column._context, basestring): + search_context.update(column._context) + attrs['selection'] = relation._name_search(cr, user, '', dom, context=search_context, limit=None, name_get_uid=1) + if (node.get('required') and not int(node.get('required'))) or not column.required: + attrs['selection'].append((False, '')) + fields[node.get('name')] = attrs + + field = model_fields.get(node.get('name')) + if field: + orm.transfer_field_to_modifiers(field, modifiers) + + + elif node.tag in ('form', 'tree'): + result = Model.view_header_get(cr, user, False, node.tag, context) + if result: + node.set('string', result) + in_tree_view = node.tag == 'tree' + + elif node.tag == 'calendar': + for additional_field in ('date_start', 'date_delay', 'date_stop', 'color'): + if node.get(additional_field): + fields[node.get(additional_field)] = {} + + if not check_group(node): + # node must be removed, no need to proceed further with its children + return fields + + # The view architeture overrides the python model. + # Get the attrs before they are (possibly) deleted by check_group below + orm.transfer_node_to_modifiers(node, modifiers, context, in_tree_view) + + # TODO remove attrs couterpart in modifiers when invisible is true ? + + # translate view + if 'lang' in context: + Translations = self.pool['ir.translation'] + if node.text and node.text.strip(): + trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.text.strip()) + if trans: + node.text = node.text.replace(node.text.strip(), trans) + if node.tail and node.tail.strip(): + trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.tail.strip()) + if trans: + node.tail = node.tail.replace(node.tail.strip(), trans) + + if node.get('string') and not result: + trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.get('string')) + if trans == node.get('string') and ('base_model_name' in context): + # If translation is same as source, perhaps we'd have more luck with the alternative model name + # (in case we are in a mixed situation, such as an inherited view where parent_view.model != model + trans = Translations._get_source(cr, user, context['base_model_name'], 'view', context['lang'], node.get('string')) + if trans: + node.set('string', trans) + + for attr_name in ('confirm', 'sum', 'avg', 'help', 'placeholder'): + attr_value = node.get(attr_name) + if attr_value: + trans = Translations._get_source(cr, user, model, 'view', context['lang'], attr_value) + if trans: + node.set(attr_name, trans) + + for f in node: + if children or (node.tag == 'field' and f.tag in ('filter','separator')): + fields.update(self.__view_look_dom(cr, user, model, f, view_id, in_tree_view, model_fields, context)) + + orm.transfer_modifiers_to_node(modifiers, node) + return fields + + def _disable_workflow_buttons(self, cr, user, model, node): + """ Set the buttons in node to readonly if the user can't activate them. """ + if user == 1: + # admin user can always activate workflow buttons + return node + + # TODO handle the case of more than one workflow for a model or multiple + # transitions with different groups and same signal + usersobj = self.pool.get('res.users') + buttons = (n for n in node.getiterator('button') if n.get('type') != 'object') + for button in buttons: + user_groups = usersobj.read(cr, user, [user], ['groups_id'])[0]['groups_id'] + cr.execute("""SELECT DISTINCT t.group_id + FROM wkf + INNER JOIN wkf_activity a ON a.wkf_id = wkf.id + INNER JOIN wkf_transition t ON (t.act_to = a.id) + WHERE wkf.osv = %s + AND t.signal = %s + AND t.group_id is NOT NULL + """, (model, button.get('name'))) + group_ids = [x[0] for x in cr.fetchall() if x[0]] + can_click = not group_ids or bool(set(user_groups).intersection(group_ids)) + button.set('readonly', str(int(not can_click))) + return node + + def __view_look_dom_arch(self, cr, user, model, node, view_id, context=None): + """ Return an architecture and a description of all the fields. + + The field description combines the result of fields_get() and + __view_look_dom(). + + :param node: the architecture as as an etree + :return: a tuple (arch, fields) where arch is the given node as a + string and fields is the description of all the fields. + + """ + fields = {} + Model = self.pool[model] + if node.tag == 'diagram': + if node.getchildren()[0].tag == 'node': + node_model = self.pool[node.getchildren()[0].get('object')] + node_fields = node_model.fields_get(cr, user, None, context) + fields.update(node_fields) + if not node.get("create") and not node_model.check_access_rights(cr, user, 'create', raise_exception=False): + node.set("create", 'false') + if node.getchildren()[1].tag == 'arrow': + arrow_fields = self.pool[node.getchildren()[1].get('object')].fields_get(cr, user, None, context) + fields.update(arrow_fields) + else: + fields = Model.fields_get(cr, user, None, context) + fields_def = self.__view_look_dom(cr, user, model, node, view_id, False, fields, context=context) + node = self._disable_workflow_buttons(cr, user, model, node) + if node.tag in ('kanban', 'tree', 'form', 'gantt'): + for action, operation in (('create', 'create'), ('delete', 'unlink'), ('edit', 'write')): + if not node.get(action) and not Model.check_access_rights(cr, user, operation, raise_exception=False): + node.set(action, 'false') + arch = etree.tostring(node, encoding="utf-8").replace('\t', '') + for k in fields.keys(): + if k not in fields_def: + del fields[k] + for field in fields_def: + if field == 'id': + # sometime, the view may contain the (invisible) field 'id' needed for a domain (when 2 objects have cross references) + fields['id'] = {'readonly': True, 'type': 'integer', 'string': 'ID'} + elif field in fields: + fields[field].update(fields_def[field]) + else: + cr.execute('select name, model from ir_ui_view where (id=%s or inherit_id=%s) and arch like %s', (view_id, view_id, '%%%s%%' % field)) + res = cr.fetchall()[:] + model = res[0][1] + res.insert(0, ("Can't find field '%s' in the following view parts composing the view of object model '%s':" % (field, model), None)) + msg = "\n * ".join([r[0] for r in res]) + msg += "\n\nEither you wrongly customized this view, or some modules bringing those views are not compatible with your current data model" + _logger.error(msg) + raise orm.except_orm('View error', msg) + return arch, fields + class view_sc(osv.osv): _name = 'ir.ui.view_sc' _columns = { diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index cd1a7f024ae..3eea19cbfd1 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -1697,268 +1697,6 @@ class BaseModel(object): return any([self.pool.get('res.users').has_group(cr, uid, group_ext_id) for group_ext_id in groups.split(',')]) - def __view_look_dom(self, cr, user, model, node, view_id, in_tree_view, model_fields, context=None): - """Return the description of the fields in the node. - - In a normal call to this method, node is a complete view architecture - but it is actually possible to give some sub-node (this is used so - that the method can call itself recursively). - - Originally, the field descriptions are drawn from the node itself. - But there is now some code calling fields_get() in order to merge some - of those information in the architecture. - - """ - if context is None: - context = {} - result = False - fields = {} - children = True - - modifiers = {} - Model = self.pool[model] - - def encode(s): - if isinstance(s, unicode): - return s.encode('utf8') - return s - - def check_group(node): - """Apply group restrictions, may be set at view level or model level:: - * at view level this means the element should be made invisible to - people who are not members - * at model level (exclusively for fields, obviously), this means - the field should be completely removed from the view, as it is - completely unavailable for non-members - - :return: True if field should be included in the result of fields_view_get - """ - if node.tag == 'field' and node.get('name') in Model._all_columns: - column = Model._all_columns[node.get('name')].column - if column.groups and not self.user_has_groups(cr, user, - groups=column.groups, - context=context): - node.getparent().remove(node) - fields.pop(node.get('name'), None) - # no point processing view-level ``groups`` anymore, return - return False - if node.get('groups'): - can_see = self.user_has_groups(cr, user, - groups=node.get('groups'), - context=context) - if not can_see: - node.set('invisible', '1') - modifiers['invisible'] = True - if 'attrs' in node.attrib: - del(node.attrib['attrs']) #avoid making field visible later - del(node.attrib['groups']) - return True - - if node.tag in ('field', 'node', 'arrow'): - if node.get('object'): - attrs = {} - views = {} - xml = "
" - for f in node: - if f.tag == 'field': - xml += etree.tostring(f, encoding="utf-8") - xml += "
" - new_xml = etree.fromstring(encode(xml)) - ctx = context.copy() - ctx['base_model_name'] = model - xarch, xfields = self.__view_look_dom_arch(cr, user, node.get('object'), new_xml, view_id, ctx) - views['form'] = { - 'arch': xarch, - 'fields': xfields - } - attrs = {'views': views} - fields = xfields - if node.get('name'): - attrs = {} - try: - if node.get('name') in Model._columns: - column = Model._columns[node.get('name')] - else: - column = Model._inherit_fields[node.get('name')][2] - except Exception: - column = False - - if column: - relation = self.pool[column._obj] if column._obj else None - - children = False - views = {} - for f in node: - if f.tag in ('form', 'tree', 'graph', 'kanban'): - node.remove(f) - ctx = context.copy() - ctx['base_model_name'] = Model - xarch, xfields = self.__view_look_dom_arch(cr, user, column._obj or None, f, view_id, ctx) - views[str(f.tag)] = { - 'arch': xarch, - 'fields': xfields - } - attrs = {'views': views} - if node.get('widget') and node.get('widget') == 'selection': - # Prepare the cached selection list for the client. This needs to be - # done even when the field is invisible to the current user, because - # other events could need to change its value to any of the selectable ones - # (such as on_change events, refreshes, etc.) - - # If domain and context are strings, we keep them for client-side, otherwise - # we evaluate them server-side to consider them when generating the list of - # possible values - # TODO: find a way to remove this hack, by allow dynamic domains - dom = [] - if column._domain and not isinstance(column._domain, basestring): - dom = list(column._domain) - dom += eval(node.get('domain', '[]'), {'uid': user, 'time': time}) - search_context = dict(context) - if column._context and not isinstance(column._context, basestring): - search_context.update(column._context) - attrs['selection'] = relation._name_search(cr, user, '', dom, context=search_context, limit=None, name_get_uid=1) - if (node.get('required') and not int(node.get('required'))) or not column.required: - attrs['selection'].append((False, '')) - fields[node.get('name')] = attrs - - field = model_fields.get(node.get('name')) - if field: - transfer_field_to_modifiers(field, modifiers) - - - elif node.tag in ('form', 'tree'): - result = Model.view_header_get(cr, user, False, node.tag, context) - if result: - node.set('string', result) - in_tree_view = node.tag == 'tree' - - elif node.tag == 'calendar': - for additional_field in ('date_start', 'date_delay', 'date_stop', 'color'): - if node.get(additional_field): - fields[node.get(additional_field)] = {} - - if not check_group(node): - # node must be removed, no need to proceed further with its children - return fields - - # The view architeture overrides the python model. - # Get the attrs before they are (possibly) deleted by check_group below - transfer_node_to_modifiers(node, modifiers, context, in_tree_view) - - # TODO remove attrs couterpart in modifiers when invisible is true ? - - # translate view - if 'lang' in context: - Translations = self.pool['ir.translation'] - if node.text and node.text.strip(): - trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.text.strip()) - if trans: - node.text = node.text.replace(node.text.strip(), trans) - if node.tail and node.tail.strip(): - trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.tail.strip()) - if trans: - node.tail = node.tail.replace(node.tail.strip(), trans) - - if node.get('string') and not result: - trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.get('string')) - if trans == node.get('string') and ('base_model_name' in context): - # If translation is same as source, perhaps we'd have more luck with the alternative model name - # (in case we are in a mixed situation, such as an inherited view where parent_view.model != model - trans = Translations._get_source(cr, user, context['base_model_name'], 'view', context['lang'], node.get('string')) - if trans: - node.set('string', trans) - - for attr_name in ('confirm', 'sum', 'avg', 'help', 'placeholder'): - attr_value = node.get(attr_name) - if attr_value: - trans = Translations._get_source(cr, user, model, 'view', context['lang'], attr_value) - if trans: - node.set(attr_name, trans) - - for f in node: - if children or (node.tag == 'field' and f.tag in ('filter','separator')): - fields.update(self.__view_look_dom(cr, user, model, f, view_id, in_tree_view, model_fields, context)) - - transfer_modifiers_to_node(modifiers, node) - return fields - - def _disable_workflow_buttons(self, cr, user, model, node): - """ Set the buttons in node to readonly if the user can't activate them. """ - if user == 1: - # admin user can always activate workflow buttons - return node - - # TODO handle the case of more than one workflow for a model or multiple - # transitions with different groups and same signal - usersobj = self.pool.get('res.users') - buttons = (n for n in node.getiterator('button') if n.get('type') != 'object') - for button in buttons: - user_groups = usersobj.read(cr, user, [user], ['groups_id'])[0]['groups_id'] - cr.execute("""SELECT DISTINCT t.group_id - FROM wkf - INNER JOIN wkf_activity a ON a.wkf_id = wkf.id - INNER JOIN wkf_transition t ON (t.act_to = a.id) - WHERE wkf.osv = %s - AND t.signal = %s - AND t.group_id is NOT NULL - """, (model, button.get('name'))) - group_ids = [x[0] for x in cr.fetchall() if x[0]] - can_click = not group_ids or bool(set(user_groups).intersection(group_ids)) - button.set('readonly', str(int(not can_click))) - return node - - def __view_look_dom_arch(self, cr, user, model, node, view_id, context=None): - """ Return an architecture and a description of all the fields. - - The field description combines the result of fields_get() and - __view_look_dom(). - - :param node: the architecture as as an etree - :return: a tuple (arch, fields) where arch is the given node as a - string and fields is the description of all the fields. - - """ - fields = {} - Model = self.pool[model] - if node.tag == 'diagram': - if node.getchildren()[0].tag == 'node': - node_model = self.pool[node.getchildren()[0].get('object')] - node_fields = node_model.fields_get(cr, user, None, context) - fields.update(node_fields) - if not node.get("create") and not node_model.check_access_rights(cr, user, 'create', raise_exception=False): - node.set("create", 'false') - if node.getchildren()[1].tag == 'arrow': - arrow_fields = self.pool[node.getchildren()[1].get('object')].fields_get(cr, user, None, context) - fields.update(arrow_fields) - else: - fields = Model.fields_get(cr, user, None, context) - fields_def = self.__view_look_dom(cr, user, model, node, view_id, False, fields, context=context) - node = self._disable_workflow_buttons(cr, user, model, node) - if node.tag in ('kanban', 'tree', 'form', 'gantt'): - for action, operation in (('create', 'create'), ('delete', 'unlink'), ('edit', 'write')): - if not node.get(action) and not Model.check_access_rights(cr, user, operation, raise_exception=False): - node.set(action, 'false') - arch = etree.tostring(node, encoding="utf-8").replace('\t', '') - for k in fields.keys(): - if k not in fields_def: - del fields[k] - for field in fields_def: - if field == 'id': - # sometime, the view may contain the (invisible) field 'id' needed for a domain (when 2 objects have cross references) - fields['id'] = {'readonly': True, 'type': 'integer', 'string': 'ID'} - elif field in fields: - fields[field].update(fields_def[field]) - else: - cr.execute('select name, model from ir_ui_view where (id=%s or inherit_id=%s) and arch like %s', (view_id, view_id, '%%%s%%' % field)) - res = cr.fetchall()[:] - model = res[0][1] - res.insert(0, ("Can't find field '%s' in the following view parts composing the view of object model '%s':" % (field, model), None)) - msg = "\n * ".join([r[0] for r in res]) - msg += "\n\nEither you wrongly customized this view, or some modules bringing those views are not compatible with your current data model" - _logger.error(msg) - raise except_orm('View error', msg) - return arch, fields - def _get_default_form_view(self, cr, user, context=None): """ Generates a default single-line form view using all fields of the current model except the m2m and o2m ones. @@ -2105,7 +1843,7 @@ class BaseModel(object): ctx = context if root_view.get('model') != self._name: ctx = dict(context, base_model_name=root_view.get('model')) - xarch, xfields = self.__view_look_dom_arch( + xarch, xfields = View._view__view_look_dom_arch( cr, user, self._name, etree.fromstring(result['arch']), result['view_id'], context=ctx) result['arch'] = xarch @@ -2149,7 +1887,9 @@ class BaseModel(object): } return result - _view_look_dom_arch = __view_look_dom_arch + def _view_look_dom_arch(self, cr, uid, node, view_id, context=None): + return self['ir.ui.view']._view__view_look_dom_arch( + cr, uid, self._name, node, view_id, context=context) def search_count(self, cr, user, args, context=None): if not context: From 4923d1b82d9523e6c5ddbf05c0fb1ae7a0b8b011 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Wed, 24 Apr 2013 17:43:10 +0200 Subject: [PATCH 025/421] [MOVE] ir.ui.view tests inside base tests bzr revid: xmo@openerp.com-20130424154310-vdygcckrz6jm578p --- openerp/addons/base/tests/__init__.py | 2 ++ openerp/{ => addons/base}/tests/test_views.py | 2 +- openerp/tests/__init__.py | 3 --- 3 files changed, 3 insertions(+), 4 deletions(-) rename openerp/{ => addons/base}/tests/test_views.py (99%) diff --git a/openerp/addons/base/tests/__init__.py b/openerp/addons/base/tests/__init__.py index e54fc892282..e3a07ec532c 100644 --- a/openerp/addons/base/tests/__init__.py +++ b/openerp/addons/base/tests/__init__.py @@ -6,6 +6,7 @@ import test_menu import test_res_config import test_res_lang import test_search +import test_views checks = [ test_base, @@ -16,4 +17,5 @@ checks = [ test_res_config, test_res_lang, test_search, + test_views, ] diff --git a/openerp/tests/test_views.py b/openerp/addons/base/tests/test_views.py similarity index 99% rename from openerp/tests/test_views.py rename to openerp/addons/base/tests/test_views.py index bedb3e78b19..358d028073f 100644 --- a/openerp/tests/test_views.py +++ b/openerp/addons/base/tests/test_views.py @@ -1,7 +1,7 @@ from lxml import etree as ET from lxml.builder import E -from . import common +from openerp.tests import common import unittest2 Field = E.field diff --git a/openerp/tests/__init__.py b/openerp/tests/__init__.py index d330cffe350..3d129e06924 100644 --- a/openerp/tests/__init__.py +++ b/openerp/tests/__init__.py @@ -7,7 +7,6 @@ This module groups a few sub-modules containing unittest2 test cases. Tests can be explicitely added to the `fast_suite` or `checks` lists or not. See the :ref:`test-framework` section in the :ref:`features` list. """ - import test_acl import test_basecase import test_db_cursor @@ -22,7 +21,6 @@ import test_osv import test_translate import test_uninstall import test_view_validation -import test_views # This need a change in `oe run-tests` to only run fast_suite + checks by default. # import test_xmlrpc @@ -43,7 +41,6 @@ checks = [ test_misc, test_osv, test_translate, - test_views, ] # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From e30b6edb41abbd062605b75029a19e253960cb6e Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 25 Apr 2013 11:37:39 +0200 Subject: [PATCH 026/421] [ADD] model-less fields & views munging thing bzr revid: xmo@openerp.com-20130425093739-v0kya956yzcy5wp3 --- openerp/addons/base/ir/ir_ui_view.py | 22 ++++---- openerp/addons/base/tests/test_views.py | 72 ++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 12 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index db6447bdeac..f1dd0508dbe 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -560,7 +560,7 @@ class view(osv.osv): children = True modifiers = {} - Model = self.pool[model] + Model = self.pool.get(model) def encode(s): if isinstance(s, unicode): @@ -577,19 +577,17 @@ class view(osv.osv): :return: True if field should be included in the result of fields_view_get """ - if node.tag == 'field' and node.get('name') in Model._all_columns: + if Model and node.tag == 'field' and node.get('name') in Model._all_columns: column = Model._all_columns[node.get('name')].column - if column.groups and not self.user_has_groups(cr, user, - groups=column.groups, - context=context): + if column.groups and not self.user_has_groups( + cr, user, groups=column.groups, context=context): node.getparent().remove(node) fields.pop(node.get('name'), None) # no point processing view-level ``groups`` anymore, return return False if node.get('groups'): - can_see = self.user_has_groups(cr, user, - groups=node.get('groups'), - context=context) + can_see = self.user_has_groups( + cr, user, groups=node.get('groups'), context=context) if not can_see: node.set('invisible', '1') modifiers['invisible'] = True @@ -728,7 +726,7 @@ class view(osv.osv): def _disable_workflow_buttons(self, cr, user, model, node): """ Set the buttons in node to readonly if the user can't activate them. """ - if user == 1: + if model is None or user == 1: # admin user can always activate workflow buttons return node @@ -763,7 +761,8 @@ class view(osv.osv): """ fields = {} - Model = self.pool[model] + Model = self.pool.get(model) + if node.tag == 'diagram': if node.getchildren()[0].tag == 'node': node_model = self.pool[node.getchildren()[0].get('object')] @@ -774,8 +773,9 @@ class view(osv.osv): if node.getchildren()[1].tag == 'arrow': arrow_fields = self.pool[node.getchildren()[1].get('object')].fields_get(cr, user, None, context) fields.update(arrow_fields) - else: + elif Model: fields = Model.fields_get(cr, user, None, context) + fields_def = self.__view_look_dom(cr, user, model, node, view_id, False, fields, context=context) node = self._disable_workflow_buttons(cr, user, model, node) if node.tag in ('kanban', 'tree', 'form', 'gantt'): diff --git a/openerp/addons/base/tests/test_views.py b/openerp/addons/base/tests/test_views.py index 358d028073f..6bde7ec04c4 100644 --- a/openerp/addons/base/tests/test_views.py +++ b/openerp/addons/base/tests/test_views.py @@ -1,5 +1,6 @@ +# -*- encoding: utf-8 -*- from lxml import etree as ET -from lxml.builder import E +from lxml.builder import E, ElementMaker from openerp.tests import common import unittest2 @@ -220,3 +221,72 @@ class TestViewCombined(common.TransactionCase): * defaults mapping * ? """ + +class TestNoModel(common.TransactionCase): + def test_create_view_nomodel(self): + View = self.registry('ir.ui.view') + view_id = View.create(self.cr, self.uid, { + 'name': 'dummy', + 'arch': '
', + 'inherit_id': False + }) + [view] = View.read(self.cr, self.uid, [view_id]) + self.assertEqual(view, { + 'id': view_id, + 'name': 'dummy', + 'arch': '', + 'type': 'form', + 'priority': 16, + 'inherit_id': False, + 'field_parent': False, + 'groups_id': [], + 'xml_id': '', + 'model_ids': [], + 'model': '', + }) + + arch = E.body( + E.div( + E.h1("Title"), + id="header"), + E.p("Welcome!"), + E.div( + E.hr(), + E.p("Copyright copyrighter", {'class': 'legalese'}), + id="footer"), + {'class': "index"},) + def test_fields_mess(self): + """ + Try to call __view_look_dom_arch without a model provided, will need + to be altered once it's broken up into sane components + """ + View = self.registry('ir.ui.view') + + sarch, fields = View._view__view_look_dom_arch( + self.cr, self.uid, None, self.arch, None) + + self.assertEqual(sarch, ET.tostring(self.arch, encoding='utf-8')) + self.assertEqual(fields, {}) + + def test_mess_translation(self): + """ + Test if translations work correctly without a model + """ + View = self.registry('ir.ui.view') + self.registry('res.lang').load_lang(self.cr, self.uid, 'fr_FR') + self.registry('ir.translation').create(self.cr, self.uid, { + 'name': '', + 'type': 'view', + 'lang': 'fr_FR', + 'src': 'Copyright copyrighter', + 'value': u"Copyrighter, tous droits réservés", + }) + sarch, fields = View._view__view_look_dom_arch( + self.cr, self.uid, None,self.arch, None, {'lang': 'fr_FR'}) + self.assertEqual( + sarch, + ET.tostring(self.arch, encoding='utf-8') + .replace('Copyright copyrighter', + 'Copyrighter, tous droits réservés')) + self.assertEqual(fields, {}) + From 7883944c19c3b8a04242618a0c97f267375f51cc Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 25 Apr 2013 12:02:47 +0200 Subject: [PATCH 027/421] [FIX] restrict number of fields read in model-less view test model_ids not filtered in regular read, so get semi-random list of ids bzr revid: xmo@openerp.com-20130425100247-2v043bbgpvcj3pv9 --- openerp/addons/base/tests/test_views.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openerp/addons/base/tests/test_views.py b/openerp/addons/base/tests/test_views.py index 6bde7ec04c4..d7f54140bfe 100644 --- a/openerp/addons/base/tests/test_views.py +++ b/openerp/addons/base/tests/test_views.py @@ -230,7 +230,8 @@ class TestNoModel(common.TransactionCase): 'arch': '', 'inherit_id': False }) - [view] = View.read(self.cr, self.uid, [view_id]) + fields = ['name', 'arch', 'type', 'priority', 'inherit_id', 'model'] + [view] = View.read(self.cr, self.uid, [view_id], fields) self.assertEqual(view, { 'id': view_id, 'name': 'dummy', @@ -238,10 +239,6 @@ class TestNoModel(common.TransactionCase): 'type': 'form', 'priority': 16, 'inherit_id': False, - 'field_parent': False, - 'groups_id': [], - 'xml_id': '', - 'model_ids': [], 'model': '', }) From 9d06d987352364e218d66b4e69eec1ce4776eff7 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 26 Apr 2013 11:02:37 +0200 Subject: [PATCH 028/421] [FIX] read_combined and get_inheriting_views_arch when no model provided * view['model'] required for base_model_name handling, force its read as with arch (nb: arch could actually be made optional, if it's not being read can skip most of the inheritance complexity no?) * if model=None in get_inheriting_views_arch, it generates domain to SQL `model is NULL`, except model required on ir.ui.view so unspec'd models are `''` (the empty string) not NULL => inherited views never found. Righter move might be to formally make model non-required on view. bzr revid: xmo@openerp.com-20130426090237-u3rojvx4gow6uue1 --- openerp/addons/base/ir/ir_ui_view.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index f1dd0508dbe..79dbc3ff419 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -200,8 +200,11 @@ class view(osv.osv): view_id = self.default_view(cr, uid, model, view_type, context=context) root_id = self.root_ancestor(cr, uid, view_id, context=context) - if fields and 'arch' not in fields: - fields = list(itertools.chain(['arch'], fields)) + if fields: + needed_fields = ['arch', 'model'] + fields = list(itertools.chain( + [field for field in needed_fields if field not in fields], + fields)) [view] = self.read(cr, uid, [root_id], fields=fields, context=context) @@ -281,7 +284,7 @@ class view(osv.osv): """ user_groups = frozenset(self.pool.get('res.users').browse(cr, 1, uid, context).groups_id) - conditions = [['inherit_id', '=', view_id], ['model', '=', model]] + conditions = [['inherit_id', '=', view_id], ['model', '=', model or '']] if self.pool._init: # Module init currently in progress, only consider views from # modules whose code is already loaded From 282a7180caad706aaeca229be8b9e13b37ae73a6 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 26 Apr 2013 16:51:13 +0200 Subject: [PATCH 029/421] [IMP] remove SQL definition of ir.ui.view, schema alterations * Formally make model not required * Remove idiotic default values on type and arch * Make type not required (it's a function field!) bzr revid: xmo@openerp.com-20130426145113-cf0t0xx24lk9mtgs --- openerp/addons/base/base.sql | 14 +------------- openerp/addons/base/ir/ir_ui_view.py | 8 +++----- openerp/addons/base/tests/test_views.py | 2 +- 3 files changed, 5 insertions(+), 19 deletions(-) diff --git a/openerp/addons/base/base.sql b/openerp/addons/base/base.sql index b1ddb2eec70..757b5731f8b 100644 --- a/openerp/addons/base/base.sql +++ b/openerp/addons/base/base.sql @@ -113,18 +113,6 @@ CREATE TABLE ir_act_client ( ) INHERITS (ir_actions); - -CREATE TABLE ir_ui_view ( - id serial NOT NULL, - name varchar(64) DEFAULT ''::varchar NOT NULL, - model varchar(64) DEFAULT ''::varchar NOT NULL, - "type" varchar(64) DEFAULT 'form'::varchar NOT NULL, - arch text NOT NULL, - field_parent varchar(64), - priority integer DEFAULT 5 NOT NULL, - primary key(id) -); - CREATE TABLE ir_ui_menu ( id serial NOT NULL, parent_id int references ir_ui_menu on delete set null, @@ -410,4 +398,4 @@ insert into ir_model_data (name,module,model,noupdate,res_id) VALUES ('main_comp select setval('res_company_id_seq', 2); select setval('res_users_id_seq', 2); select setval('res_partner_id_seq', 2); -select setval('res_currency_id_seq', 2); \ No newline at end of file +select setval('res_currency_id_seq', 2); diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 79dbc3ff419..839bbc1406f 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -68,7 +68,7 @@ class view(osv.osv): _columns = { 'name': fields.char('View Name', required=True), - 'model': fields.char('Object', size=64, required=True, select=True), + 'model': fields.char('Object', size=64, select=True), 'priority': fields.integer('Sequence', required=True), 'type': fields.function(_type_field, type='selection', selection=[ ('tree','Tree'), @@ -79,7 +79,7 @@ class view(osv.osv): ('diagram','Diagram'), ('gantt', 'Gantt'), ('kanban', 'Kanban'), - ('search','Search')], string='View Type', required=True, select=True, store=True), + ('search','Search')], string='View Type', select=True, store=True), 'arch': fields.text('View Architecture', required=True), 'inherit_id': fields.many2one('ir.ui.view', 'Inherited View', ondelete='cascade', select=True), 'field_parent': fields.char('Child Field',size=64), @@ -90,9 +90,7 @@ class view(osv.osv): 'model_ids': fields.one2many('ir.model.data', 'res_id', auto_join=True), } _defaults = { - 'arch': '\n\n\t\n', 'priority': 16, - 'type': 'tree', } _order = "priority,name" @@ -284,7 +282,7 @@ class view(osv.osv): """ user_groups = frozenset(self.pool.get('res.users').browse(cr, 1, uid, context).groups_id) - conditions = [['inherit_id', '=', view_id], ['model', '=', model or '']] + conditions = [['inherit_id', '=', view_id], ['model', '=', model]] if self.pool._init: # Module init currently in progress, only consider views from # modules whose code is already loaded diff --git a/openerp/addons/base/tests/test_views.py b/openerp/addons/base/tests/test_views.py index d7f54140bfe..9006a86500b 100644 --- a/openerp/addons/base/tests/test_views.py +++ b/openerp/addons/base/tests/test_views.py @@ -239,7 +239,7 @@ class TestNoModel(common.TransactionCase): 'type': 'form', 'priority': 16, 'inherit_id': False, - 'model': '', + 'model': False, }) arch = E.body( From 45bb6ec7f19316b5e2e0ddc18c8bd3acab653724 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 29 Apr 2013 13:24:59 +0200 Subject: [PATCH 030/421] [ADD] file field type in xml_convert, tests bzr revid: xmo@openerp.com-20130429112459-ol1ayn2z34j32r54 --- doc/03_module_dev_01.rst | 81 ++++++++++++------ openerp/tests/__init__.py | 2 + openerp/tests/addons/test_convert/__init__.py | 1 + .../tests/addons/test_convert/__openerp__.py | 5 ++ .../tests/addons/test_convert/test_file.txt | 1 + openerp/tests/test_convert.py | 83 +++++++++++++++++++ openerp/tools/convert.py | 6 ++ 7 files changed, 155 insertions(+), 24 deletions(-) create mode 100644 openerp/tests/addons/test_convert/__init__.py create mode 100644 openerp/tests/addons/test_convert/__openerp__.py create mode 100644 openerp/tests/addons/test_convert/test_file.txt create mode 100644 openerp/tests/test_convert.py diff --git a/doc/03_module_dev_01.rst b/doc/03_module_dev_01.rst index 0fbca9d14b4..adc32c4a4e9 100644 --- a/doc/03_module_dev_01.rst +++ b/doc/03_module_dev_01.rst @@ -172,40 +172,73 @@ is as follows: -Record Tag -////////// +```` +//////////// -**Description** +Defines a new record in a specified OpenERP model. -The addition of new data is made with the record tag. This one takes a -mandatory attribute : model. Model is the object name where the insertion has -to be done. The tag record can also take an optional attribute: id. If this -attribute is given, a variable of this name can be used later on, in the same -file, to make reference to the newly created resource ID. +``@model`` (required) -A record tag may contain field tags. They indicate the record's fields value. -If a field is not specified the default value will be used. + Name of the model in which this record will be created/inserted. -The Record Field tag -//////////////////// +``@id`` (optional) -The attributes for the field tag are the following: + :term:`external ID` for the record, also allows referring to this record in + the rest of this file or in other files (through ``field/@ref`` or the + :py:func:`ref() ` function) -name : mandatory - the field name +A record tag generally contains multiple ``field`` tags specifying the values +set on the record's fields when creating it. Fields left out will be set to +their default value unless required. -eval : optional - python expression that indicating the value to add - -ref - reference to an id defined in this file +```` +/////////// -model - model to be looked up in the search +In its most basic use, the ``field`` tag will set its body (as a string) as +the value of the corresponding ``record``'s ``@name`` field. -search - a query +Extra attributes can either preprocess the body or replace its use entirely: +``@name`` (mandatory) + + Name of the field in the containing ``record``'s model + +``@type`` (optional) + + One of ``char``, ``int``, ``float``, ``list``, ``tuple``, ``xml`` or + ``html`` or ``file``. Converts the ``field``'s body to the specified type + (or validates the body's content) + + * ``xml`` will join multiple XML nodes under a single ```` root + * in ``xml`` and ``html``, external ids can be referenced using + ``%(id_name)s`` + * ``list`` and ``tuple``'s element are specified using ```` + sub-nodes with the same attributes as ``field``. + * ``file`` expects a module-local path and will save the path prefixed with + the current module's name, separated by a ``,`` (comma). For use with + :py:func:`~openerp.modules.module.get_module_resource`. + +``@model`` + + Model used for ``@search``'s search, or registry object put in context for + ``@eval``. Required if ``@search`` but optional if ``@eval``. + +``@eval`` (optional) + + A Python expression evaluated to obtain the value to set on the record + +``@ref`` (optional) + + Links to an other record through its :term:`external id`. The module prefix + may be ommitted to link to a record defined in the same module. + +``@search`` (optional) + + Search domain (evaluated Python expression) into ``@model`` to get the + records to set on the field. + + Sets all the matches found for m2m fields, the first id for other field + types. **Example** diff --git a/openerp/tests/__init__.py b/openerp/tests/__init__.py index 3d129e06924..5f262374694 100644 --- a/openerp/tests/__init__.py +++ b/openerp/tests/__init__.py @@ -21,6 +21,7 @@ import test_osv import test_translate import test_uninstall import test_view_validation +import test_convert # This need a change in `oe run-tests` to only run fast_suite + checks by default. # import test_xmlrpc @@ -41,6 +42,7 @@ checks = [ test_misc, test_osv, test_translate, + test_convert, ] # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/tests/addons/test_convert/__init__.py b/openerp/tests/addons/test_convert/__init__.py new file mode 100644 index 00000000000..40a96afc6ff --- /dev/null +++ b/openerp/tests/addons/test_convert/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/openerp/tests/addons/test_convert/__openerp__.py b/openerp/tests/addons/test_convert/__openerp__.py new file mode 100644 index 00000000000..40d9eb3a87f --- /dev/null +++ b/openerp/tests/addons/test_convert/__openerp__.py @@ -0,0 +1,5 @@ +{ + 'name': 'test_convert', + 'description': "Data for xml conversion tests", + 'version': '0.0.1', +} diff --git a/openerp/tests/addons/test_convert/test_file.txt b/openerp/tests/addons/test_convert/test_file.txt new file mode 100644 index 00000000000..e69b2e08d55 --- /dev/null +++ b/openerp/tests/addons/test_convert/test_file.txt @@ -0,0 +1 @@ +nothing to see here, move along diff --git a/openerp/tests/test_convert.py b/openerp/tests/test_convert.py new file mode 100644 index 00000000000..87c3000fba9 --- /dev/null +++ b/openerp/tests/test_convert.py @@ -0,0 +1,83 @@ +import collections +import unittest2 +from lxml import etree as ET +from lxml.builder import E + +from . import common + +from openerp.tools.convert import _eval_xml + +Field = E.field +Value = E.value +class TestEvalXML(common.TransactionCase): + def eval_xml(self, node, obj=None, idref=None): + return _eval_xml(obj, node, pool=None, cr=self.cr, uid=self.uid, + idref=idref, context=None) + + def test_char(self): + self.assertEqual( + self.eval_xml(Field("foo")), + "foo") + self.assertEqual( + self.eval_xml(Field("None")), + "None") + + def test_int(self): + self.assertIsNone( + self.eval_xml(Field("None", type='int')), + "what the fuck?") + self.assertEqual( + self.eval_xml(Field(" 42 ", type="int")), + 42) + + with self.assertRaises(ValueError): + self.eval_xml(Field("4.82", type="int")) + + with self.assertRaises(ValueError): + self.eval_xml(Field("Whelp", type="int")) + + def test_float(self): + self.assertEqual( + self.eval_xml(Field("4.78", type="float")), + 4.78) + + with self.assertRaises(ValueError): + self.eval_xml(Field("None", type="float")) + + with self.assertRaises(ValueError): + self.eval_xml(Field("Foo", type="float")) + + def test_list(self): + self.assertEqual( + self.eval_xml(Field(type="list")), + []) + + self.assertEqual( + self.eval_xml(Field( + Value("foo"), + Value("5", type="int"), + Value("4.76", type="float"), + Value("None", type="int"), + type="list" + )), + ["foo", 5, 4.76, None]) + + def test_file(self): + Obj = collections.namedtuple('Obj', 'module') + obj = Obj('test_convert') + self.assertEqual( + self.eval_xml(Field('test_file.txt', type='file'), obj), + 'test_convert,test_file.txt') + + with self.assertRaises(IOError): + self.eval_xml(Field('test_nofile.txt', type='file'), obj) + + @unittest2.skip("not tested") + def test_xml(self): + pass + + @unittest2.skip("not tested") + def test_html(self): + pass + + diff --git a/openerp/tools/convert.py b/openerp/tools/convert.py index cdcaff35b9e..8541550a6d5 100644 --- a/openerp/tools/convert.py +++ b/openerp/tools/convert.py @@ -161,6 +161,12 @@ def _eval_xml(self, node, pool, cr, uid, idref, context=None): if t == 'html': return _process("".join([etree.tostring(n, encoding='utf-8') for n in node]), idref) + if t == 'file': + from ..modules import module + path = node.text.strip() + if not module.get_module_resource(self.module, path): + raise IOError("No such file or directory: '%s'" % path) + return '%s,%s' % (self.module, path) if t in ('char', 'int', 'float'): d = node.text if t == 'int': From 2e8c9a48e172de42c87aeeb3f5933f91f09e1e8e Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 29 Apr 2013 14:23:17 +0200 Subject: [PATCH 031/421] [IMP] move testing of xml conversion alongside test data to ensure tests only run if module on addons_path/loaded bzr revid: xmo@openerp.com-20130429122317-yzv4prie9hc7f7l6 --- openerp/tests/__init__.py | 2 -- openerp/tests/addons/test_convert/__init__.py | 1 - openerp/tests/addons/test_convert/tests/__init__.py | 6 ++++++ .../tests/{ => addons/test_convert/tests}/test_convert.py | 2 +- openerp/tools/convert.py | 3 ++- 5 files changed, 9 insertions(+), 5 deletions(-) create mode 100644 openerp/tests/addons/test_convert/tests/__init__.py rename openerp/tests/{ => addons/test_convert/tests}/test_convert.py (98%) diff --git a/openerp/tests/__init__.py b/openerp/tests/__init__.py index 5f262374694..3d129e06924 100644 --- a/openerp/tests/__init__.py +++ b/openerp/tests/__init__.py @@ -21,7 +21,6 @@ import test_osv import test_translate import test_uninstall import test_view_validation -import test_convert # This need a change in `oe run-tests` to only run fast_suite + checks by default. # import test_xmlrpc @@ -42,7 +41,6 @@ checks = [ test_misc, test_osv, test_translate, - test_convert, ] # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/tests/addons/test_convert/__init__.py b/openerp/tests/addons/test_convert/__init__.py index 40a96afc6ff..e69de29bb2d 100644 --- a/openerp/tests/addons/test_convert/__init__.py +++ b/openerp/tests/addons/test_convert/__init__.py @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- diff --git a/openerp/tests/addons/test_convert/tests/__init__.py b/openerp/tests/addons/test_convert/tests/__init__.py new file mode 100644 index 00000000000..002a0a18d1a --- /dev/null +++ b/openerp/tests/addons/test_convert/tests/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +from . import test_convert + +checks = [ + test_convert +] diff --git a/openerp/tests/test_convert.py b/openerp/tests/addons/test_convert/tests/test_convert.py similarity index 98% rename from openerp/tests/test_convert.py rename to openerp/tests/addons/test_convert/tests/test_convert.py index 87c3000fba9..50d7df4b665 100644 --- a/openerp/tests/test_convert.py +++ b/openerp/tests/addons/test_convert/tests/test_convert.py @@ -3,7 +3,7 @@ import unittest2 from lxml import etree as ET from lxml.builder import E -from . import common +from openerp.tests import common from openerp.tools.convert import _eval_xml diff --git a/openerp/tools/convert.py b/openerp/tools/convert.py index 8541550a6d5..09ba6b98bad 100644 --- a/openerp/tools/convert.py +++ b/openerp/tools/convert.py @@ -165,7 +165,8 @@ def _eval_xml(self, node, pool, cr, uid, idref, context=None): from ..modules import module path = node.text.strip() if not module.get_module_resource(self.module, path): - raise IOError("No such file or directory: '%s'" % path) + raise IOError("No such file or directory: '%s' in %s" % ( + path, self.module)) return '%s,%s' % (self.module, path) if t in ('char', 'int', 'float'): d = node.text From ab8e0f89b427ae1aa9785423022723594bbb674f Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Thu, 20 Jun 2013 17:46:17 +0200 Subject: [PATCH 032/421] use read_combined instead of fields_view_get to check inherited views bzr revid: al@openerp.com-20130620154617-uiceguvzjtu3ab87 --- openerp/addons/base/ir/ir_ui_view.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 540ee3ae2ea..fcb5c2de375 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -122,16 +122,14 @@ class view(osv.osv): def _check_render_view(self, cr, uid, view, context=None): """Verify that the given view's hierarchy is valid for rendering, along with all the changes applied by - its inherited views, by rendering it using ``fields_view_get()``. + its inherited views, by rendering it using ``read_combined()``. @param browse_record view: view to validate @return: the rendered definition (arch) of the view, always utf-8 bytestring (legacy convention) if no error occurred, else False. """ - if view.model and view.model not in self.pool: - return False try: - fvg = self.pool[view.model].fields_view_get(cr, uid, view_id=view.id, view_type=view.type, context=context) + fvg = self.read_combined(cr, uid, view.id, view.type, view.model) return fvg['arch'] except: return False @@ -166,7 +164,7 @@ class view(osv.osv): return True _constraints = [ - #(_check_xml, 'Invalid XML for View Architecture!', ['arch']) + (_check_xml, 'Invalid XML for View Architecture!', ['arch']) #(_check_model, 'The model name does not exist.', ['model']), ] From 9a8ff60da083be07e037a59f4db231a8b69d1987 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Fri, 21 Jun 2013 00:16:48 +0200 Subject: [PATCH 033/421] file based openerp views bzr revid: al@openerp.com-20130620221648-kj53t7vdopfab0c2 --- openerp/addons/base/ir/ir_ui_view.py | 32 +++++++++++++++++++++- openerp/addons/base/ir/ir_ui_view_view.xml | 1 + openerp/tools/convert.py | 5 ++-- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index fcb5c2de375..dac6cc32370 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -27,11 +27,13 @@ import os import time from openerp import tools +from openerp.modules import module from openerp.osv import fields, osv, orm from openerp.tools import graph, SKIPPED_ELEMENT_TYPES from openerp.tools.safe_eval import safe_eval as eval from openerp.tools.translate import _ from openerp.tools.view_validation import valid_view +from openerp.tools import misc _logger = logging.getLogger(__name__) @@ -66,6 +68,32 @@ class view(osv.osv): result[record.id] = etree.fromstring(record.arch.encode('utf8')).tag return result + def _arch_get(self, cr, uid, ids, name, arg, context=None): + """ + For each id being read, return arch_db or the content of arch_file + """ + result = {} + for record in self.read(cr, uid, ids, ['arch_file', 'arch_db'], context=context): + if record['arch_db']: + result[record['id']] = record['arch_db'] + continue + + view_module, path = record['arch_file'].split('/', 1) + arch_path = module.get_module_resource(view_module, path) + if not arch_path: + raise IOError("No file '%s' in module '%s'" % (path, view_module)) + + with misc.file_open(arch_path) as f: + result[record['id']] = f.read().decode('utf-8') + + return result + + def _arch_set(self, cr, uid, id, name, value, arg, context=None): + """ + Forward writing to arch_db + """ + self.write(cr, uid, id, {'arch_db': value}, context=context) + _columns = { 'name': fields.char('View Name', required=True), 'model': fields.char('Object', size=64, select=True), @@ -80,7 +108,9 @@ class view(osv.osv): ('gantt', 'Gantt'), ('kanban', 'Kanban'), ('search','Search')], string='View Type', select=True, store=True), - 'arch': fields.text('View Architecture', required=True), + 'arch_file': fields.char("View path"), + 'arch_db': fields.text("Arch content", oldname='arch'), + 'arch': fields.function(_arch_get, fnct_inv=_arch_set, store=False, string="View Architecture", type='text', nodrop=True), 'inherit_id': fields.many2one('ir.ui.view', 'Inherited View', ondelete='cascade', select=True), 'field_parent': fields.char('Child Field',size=64), 'xml_id': fields.function(osv.osv.get_xml_id, type='char', size=128, string="External ID", diff --git a/openerp/addons/base/ir/ir_ui_view_view.xml b/openerp/addons/base/ir/ir_ui_view_view.xml index 44594c1e616..6ae2cdc4ff3 100644 --- a/openerp/addons/base/ir/ir_ui_view_view.xml +++ b/openerp/addons/base/ir/ir_ui_view_view.xml @@ -15,6 +15,7 @@ + diff --git a/openerp/tools/convert.py b/openerp/tools/convert.py index e3c5311d4ae..893a7afe798 100644 --- a/openerp/tools/convert.py +++ b/openerp/tools/convert.py @@ -615,9 +615,8 @@ form: module.record_id""" % (xml_id,) "Verify that this is a window action or add a type argument." % (a_action,) action_type,action_mode,action_name,view_id,target = rrres if view_id: - cr.execute('SELECT arch FROM ir_ui_view WHERE id=%s', (int(view_id),)) - arch, = cr.fetchone() - action_mode = etree.fromstring(arch.encode('utf8')).tag + view_arch = self.pool['ir.ui.view'].read(cr, 1, [view_id], ['arch']) + action_mode = etree.fromstring(view_arch[0]['arch'].encode('utf8')).tag cr.execute('SELECT view_mode FROM ir_act_window_view WHERE act_window_id=%s ORDER BY sequence LIMIT 1', (int(a_id),)) if cr.rowcount: action_mode, = cr.fetchone() From ca163b58dbbe6453266b9fc20d7a7118683c78c1 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Mon, 24 Jun 2013 18:09:10 +0200 Subject: [PATCH 034/421] fix check_xml, disable wrong tests bzr revid: al@openerp.com-20130624160910-zp898ew6va3lergk --- openerp/addons/base/ir/ir_ui_view.py | 27 ++++++++----------------- openerp/addons/base/tests/test_views.py | 12 +++++++++-- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index dac6cc32370..d70bf4d02c8 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -150,26 +150,15 @@ class view(osv.osv): frng.close() return self._relaxng_validator - def _check_render_view(self, cr, uid, view, context=None): - """Verify that the given view's hierarchy is valid for rendering, along with all the changes applied by - its inherited views, by rendering it using ``read_combined()``. - - @param browse_record view: view to validate - @return: the rendered definition (arch) of the view, always utf-8 bytestring (legacy convention) - if no error occurred, else False. - """ - try: - fvg = self.read_combined(cr, uid, view.id, view.type, view.model) - return fvg['arch'] - except: - return False - def _check_xml(self, cr, uid, ids, context=None): for view in self.browse(cr, uid, ids, context): # Sanity check: the view should not break anything upon rendering! - view_arch_utf8 = self._check_render_view(cr, uid, view, context=context) - # always utf-8 bytestring - legacy convention - if not view_arch_utf8: return False + try: + fvg = self.read_combined(cr, uid, view.id, view.type, view.model, context=context) + view_arch_utf8 = fvg['arch'] + except Exception, e: + _logger.exception(e) + return False # RNG-based validation is not possible anymore with 7.0 forms # TODO 7.0: provide alternative assertion-based validation of view_arch_utf8 @@ -247,12 +236,12 @@ class view(osv.osv): else view['arch']) descendants = self.iter( cr, uid, view['id'], model, exclude_base=True, context=context) + + arch = self.apply_inherited_archs( cr, uid, arch_tree, descendants, model, view['id'], context=context) - if view['model'] != model: - context = dict(context, base_model_name=view['model']) except self.NoViewError: # defaultdict is "empty" until first __getattr__ if fallback is None: raise diff --git a/openerp/addons/base/tests/test_views.py b/openerp/addons/base/tests/test_views.py index 9006a86500b..794f6b2ab7e 100644 --- a/openerp/addons/base/tests/test_views.py +++ b/openerp/addons/base/tests/test_views.py @@ -102,12 +102,13 @@ class TestViewInheritance(common.TransactionCase): return ET.tostring(ET.Element(view_type, string=name)) def makeView(self, name, parent=None, arch=None): - view_id = self.View.create(self.cr, self.uid, { + v = { 'model': self.model, 'name': name, 'arch': arch or self.arch_for(name), 'inherit_id': parent, - }) + } + view_id = self.View.create(self.cr, self.uid, v) self.ids[name] = view_id return view_id @@ -139,6 +140,7 @@ class TestViewInheritance(common.TransactionCase): self.View.pool._init = self._init super(TestViewInheritance, self).tearDown() + @unittest2.skip("Not tested") def test_get_inheriting_views_arch(self): self.assertEqual(self.View.get_inheriting_views_arch( self.cr, self.uid, self.ids['A'], self.model), [ @@ -154,6 +156,7 @@ class TestViewInheritance(common.TransactionCase): self.cr, self.uid, self.ids['A11'], self.model), [(self.arch_for('A111'), self.ids['A111'])]) + @unittest2.skip("Not tested") def test_iter(self): descendents = list(self.View.iter(self.cr, self.uid, self.ids['A1'], self.model)) self.assertEqual(descendents, [ @@ -167,6 +170,7 @@ class TestViewInheritance(common.TransactionCase): for name in ['A21', 'A22', 'A221'] ]) + @unittest2.skip("Not tested") def test_root_ancestor(self): A_id = self.ids['A'] root_id = self.View.root_ancestor(self.cr, self.uid, view_id=A_id) @@ -185,10 +189,12 @@ class TestViewInheritance(common.TransactionCase): self.cr, self.uid, view_id=self.ids['B1']) self.assertEqual(root_id, self.ids['B']) + @unittest2.skip("Not tested") def test_no_root_ancestor(self): with self.assertRaises(self.View.NoViewError): self.View.root_ancestor(self.cr, self.uid, view_id=12345678) + @unittest2.skip("Not tested") def test_default_view(self): default = self.View.default_view( self.cr, self.uid, model=self.model, view_type='form') @@ -198,6 +204,7 @@ class TestViewInheritance(common.TransactionCase): self.cr, self.uid, model=self.model, view_type='tree') self.assertEqual(default_tree, self.ids['C']) + @unittest2.skip("Not tested") def test_no_default_view(self): with self.assertRaises(self.View.NoDefaultError): self.View.default_view( @@ -223,6 +230,7 @@ class TestViewCombined(common.TransactionCase): """ class TestNoModel(common.TransactionCase): + @unittest2.skip("Not tested") def test_create_view_nomodel(self): View = self.registry('ir.ui.view') view_id = View.create(self.cr, self.uid, { From eaf7f87ba2ca009a209a45e176f0a9bbe5b438c6 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Mon, 24 Jun 2013 19:02:43 +0200 Subject: [PATCH 035/421] [MERGE] fix view_validate to match rng bzr revid: al@openerp.com-20130624170243-o9qjfo2s4obvqaea --- openerp/tools/view_validation.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/openerp/tools/view_validation.py b/openerp/tools/view_validation.py index 7e710d55816..27967c42c12 100644 --- a/openerp/tools/view_validation.py +++ b/openerp/tools/view_validation.py @@ -40,11 +40,6 @@ def valid_att_in_label(arch): return not arch.xpath('//label[not ((@for) or (@string))]') -def valid_att_in_form(arch): - """A `string` attribute must be on a `form` node.""" - return not arch.xpath('//form[not (@string)]') - - def valid_type_in_colspan(arch): """A `colspan` attribute must be an `integer` type.""" for attrib in arch.xpath('//*/@colspan'): @@ -67,8 +62,7 @@ def valid_type_in_col(arch): def valid_view(arch): if arch.tag == 'form': - for pred in [valid_page_in_book, valid_att_in_form, valid_type_in_colspan,\ - valid_type_in_col, valid_att_in_field, valid_att_in_label]: + for pred in [valid_page_in_book, valid_type_in_colspan, valid_type_in_col, valid_att_in_field, valid_att_in_label]: if not pred(arch): _logger.error('Invalid XML: %s', pred.__doc__) return False From 65e95dae494fae3932bb9d37c0374cd8a9f31fd1 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 25 Jun 2013 08:00:41 +0200 Subject: [PATCH 036/421] [FIX] view inheritance tests: generate valid architectures bzr revid: xmo@openerp.com-20130625060041-pfgdrnawfwuts2rj --- openerp/addons/base/tests/test_views.py | 70 +++++++++++++++++-------- 1 file changed, 47 insertions(+), 23 deletions(-) diff --git a/openerp/addons/base/tests/test_views.py b/openerp/addons/base/tests/test_views.py index 794f6b2ab7e..4be7761deff 100644 --- a/openerp/addons/base/tests/test_views.py +++ b/openerp/addons/base/tests/test_views.py @@ -1,6 +1,6 @@ # -*- encoding: utf-8 -*- from lxml import etree as ET -from lxml.builder import E, ElementMaker +from lxml.builder import E from openerp.tests import common import unittest2 @@ -98,17 +98,48 @@ class TestNodeLocator(common.BaseCase): self.assertIsNone(node) class TestViewInheritance(common.TransactionCase): - def arch_for(self, name, view_type='form'): - return ET.tostring(ET.Element(view_type, string=name)) + def arch_for(self, name, view_type='form', parent=None): + """ Generates a trivial view of the specified ``view_type``. + + The generated view is empty but ``name`` is set as its root's ``@string``. + + If ``parent`` is not falsy, generates an extension view (instead of + a root view) replacing the parent's ``@string`` by ``name`` + + :param str name: ``@string`` value for the view root + :param str view_type: + :param bool parent: + :return: generated arch + :rtype: str + """ + if not parent: + element = E(view_type, string=name) + else: + element = E(view_type, + E.attribute(name, name='string'), + position='attributes' + ) + return ET.tostring(element) def makeView(self, name, parent=None, arch=None): - v = { + """ Generates a basic ir.ui.view with the provided name, parent and arch. + + If no parent is provided, the view is top-level. + + If no arch is provided, generates one by calling :meth:`~.arch_for`. + + :param str name: + :param int parent: id of the parent view, if any + :param str arch: + :returns: the created view's id. + :rtype: int + """ + view_id = self.View.create(self.cr, self.uid, { 'model': self.model, 'name': name, - 'arch': arch or self.arch_for(name), + 'arch': arch or self.arch_for(name, parent=parent), 'inherit_id': parent, - } - view_id = self.View.create(self.cr, self.uid, v) + }) self.ids[name] = view_id return view_id @@ -132,7 +163,7 @@ class TestViewInheritance(common.TransactionCase): self.makeView("A221", a22) b = self.makeView('B', arch=self.arch_for("B", 'tree')) - self.makeView('B1', b, arch=self.arch_for("B1", 'tree')) + self.makeView('B1', b, arch=self.arch_for("B1", 'tree', parent=b)) c = self.makeView('C', arch=self.arch_for("C", 'tree')) self.View.write(self.cr, self.uid, c, {'priority': 1}) @@ -140,12 +171,11 @@ class TestViewInheritance(common.TransactionCase): self.View.pool._init = self._init super(TestViewInheritance, self).tearDown() - @unittest2.skip("Not tested") def test_get_inheriting_views_arch(self): self.assertEqual(self.View.get_inheriting_views_arch( self.cr, self.uid, self.ids['A'], self.model), [ - (self.arch_for('A1'), self.ids['A1']), - (self.arch_for('A2'), self.ids['A2']), + (self.arch_for('A1', parent=True), self.ids['A1']), + (self.arch_for('A2', parent=True), self.ids['A2']), ]) self.assertEqual(self.View.get_inheriting_views_arch( @@ -154,23 +184,21 @@ class TestViewInheritance(common.TransactionCase): self.assertEqual(self.View.get_inheriting_views_arch( self.cr, self.uid, self.ids['A11'], self.model), - [(self.arch_for('A111'), self.ids['A111'])]) + [(self.arch_for('A111', parent=True), self.ids['A111'])]) - @unittest2.skip("Not tested") def test_iter(self): descendents = list(self.View.iter(self.cr, self.uid, self.ids['A1'], self.model)) self.assertEqual(descendents, [ - (self.ids[name], self.arch_for(name)) + (self.ids[name], self.arch_for(name, parent=True)) for name in ['A1', 'A11', 'A111', 'A12'] ]) descendents = list(self.View.iter( self.cr, self.uid, self.ids['A2'], self.model, exclude_base=True)) self.assertEqual(descendents, [ - (self.ids[name], self.arch_for(name)) + (self.ids[name], self.arch_for(name, parent=True)) for name in ['A21', 'A22', 'A221'] ]) - @unittest2.skip("Not tested") def test_root_ancestor(self): A_id = self.ids['A'] root_id = self.View.root_ancestor(self.cr, self.uid, view_id=A_id) @@ -189,12 +217,10 @@ class TestViewInheritance(common.TransactionCase): self.cr, self.uid, view_id=self.ids['B1']) self.assertEqual(root_id, self.ids['B']) - @unittest2.skip("Not tested") def test_no_root_ancestor(self): with self.assertRaises(self.View.NoViewError): self.View.root_ancestor(self.cr, self.uid, view_id=12345678) - @unittest2.skip("Not tested") def test_default_view(self): default = self.View.default_view( self.cr, self.uid, model=self.model, view_type='form') @@ -204,7 +230,6 @@ class TestViewInheritance(common.TransactionCase): self.cr, self.uid, model=self.model, view_type='tree') self.assertEqual(default_tree, self.ids['C']) - @unittest2.skip("Not tested") def test_no_default_view(self): with self.assertRaises(self.View.NoDefaultError): self.View.default_view( @@ -230,12 +255,11 @@ class TestViewCombined(common.TransactionCase): """ class TestNoModel(common.TransactionCase): - @unittest2.skip("Not tested") def test_create_view_nomodel(self): View = self.registry('ir.ui.view') view_id = View.create(self.cr, self.uid, { 'name': 'dummy', - 'arch': '', + 'arch': '', 'inherit_id': False }) fields = ['name', 'arch', 'type', 'priority', 'inherit_id', 'model'] @@ -243,7 +267,7 @@ class TestNoModel(common.TransactionCase): self.assertEqual(view, { 'id': view_id, 'name': 'dummy', - 'arch': '', + 'arch': '', 'type': 'form', 'priority': 16, 'inherit_id': False, @@ -287,7 +311,7 @@ class TestNoModel(common.TransactionCase): 'value': u"Copyrighter, tous droits réservés", }) sarch, fields = View._view__view_look_dom_arch( - self.cr, self.uid, None,self.arch, None, {'lang': 'fr_FR'}) + self.cr, self.uid, None, self.arch, None, {'lang': 'fr_FR'}) self.assertEqual( sarch, ET.tostring(self.arch, encoding='utf-8') From b3ade05f11fa49905e9225db9a493025fbb42f5b Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 25 Jun 2013 10:48:57 +0200 Subject: [PATCH 037/421] [ADD] basic tests on application of inheritance specs bzr revid: xmo@openerp.com-20130625084857-k72kpvdrvejfragb --- openerp/addons/base/tests/test_views.py | 129 ++++++++++++++++++++++-- 1 file changed, 123 insertions(+), 6 deletions(-) diff --git a/openerp/addons/base/tests/test_views.py b/openerp/addons/base/tests/test_views.py index 4be7761deff..87a018888d5 100644 --- a/openerp/addons/base/tests/test_views.py +++ b/openerp/addons/base/tests/test_views.py @@ -239,13 +239,130 @@ class TestViewInheritance(common.TransactionCase): self.View.default_view( self.cr, self.uid, model=self.model, view_type='graph') - @unittest2.skip("Not tested") - def test_apply_inherited_archs(self): - self.fail() +class TestApplyInheritanceSpecs(common.TransactionCase): + """ Applies a sequence of inheritance specification nodes to a base + architecture. IO state parameters (cr, uid, model, context) are used for + error reporting - @unittest2.skip("Not tested") - def test_apply_inheritance_specs(self): - self.fail() + The base architecture is altered in-place. + """ + def setUp(self): + super(TestApplyInheritanceSpecs, self).setUp() + self.View = self.registry('ir.ui.view') + self.base_arch = E.form( + Field(name="target"), + string="Title") + + def test_replace(self): + spec = ET.tostring( + Field( + Field(name="replacement"), + name="target", position="replace")) + + self.View.apply_inheritance_specs(self.cr, self.uid, 'test', + None, self.base_arch, + None, spec) + + self.assertEqual( + ET.tostring(self.base_arch), + ET.tostring(E.form(Field(name="replacement"), string="Title"))) + + def test_delete(self): + spec = ET.tostring(Field(name="target", position="replace")) + + self.View.apply_inheritance_specs(self.cr, self.uid, 'test', + None, self.base_arch, + None, spec) + + self.assertEqual( + ET.tostring(self.base_arch), + ET.tostring(E.form(string="Title"))) + + def test_insert_after(self): + spec = ET.tostring( + Field( + Field(name="inserted"), + name="target", position="after")) + + self.View.apply_inheritance_specs(self.cr, self.uid, 'test', + None, self.base_arch, + None, spec) + + self.assertEqual( + ET.tostring(self.base_arch), + ET.tostring(E.form( + Field(name="target"), + Field(name="inserted"), + string="Title" + ))) + + def test_insert_before(self): + spec = ET.tostring( + Field( + Field(name="inserted"), + name="target", position="before")) + + self.View.apply_inheritance_specs(self.cr, self.uid, 'test', + None, self.base_arch, + None, spec) + + self.assertEqual( + ET.tostring(self.base_arch), + ET.tostring(E.form( + Field(name="inserted"), + Field(name="target"), + string="Title"))) + + def test_insert_inside(self): + default = ET.tostring( + Field(Field(name="inserted"), name="target")) + spec = ET.tostring( + Field(Field(name="inserted 2"), name="target", position='inside')) + + self.View.apply_inheritance_specs(self.cr, self.uid, 'test', + None, self.base_arch, + None, default) + self.View.apply_inheritance_specs(self.cr, self.uid, 'test', + None, self.base_arch, + None, spec) + + self.assertEqual( + ET.tostring(self.base_arch), + ET.tostring(E.form( + Field( + Field(name="inserted"), + Field(name="inserted 2"), + name="target"), + string="Title"))) + + def test_unpack_data(self): + spec = ET.tostring( + E.data( + Field(Field(name="inserted 0"), name="target"), + Field(Field(name="inserted 1"), name="target"), + Field(Field(name="inserted 2"), name="target"), + Field(Field(name="inserted 3"), name="target"), + )) + + self.View.apply_inheritance_specs(self.cr, self.uid, 'test', + None, self.base_arch, + None, spec) + + self.assertEqual( + ET.tostring(self.base_arch), + ET.tostring(E.form( + Field( + Field(name="inserted 0"), + Field(name="inserted 1"), + Field(name="inserted 2"), + Field(name="inserted 3"), + name="target"), + string="Title"))) + + +class TestApplyInheritedArchs(common.TransactionCase): + """ Applies a sequence of modificator archs to a base view + """ class TestViewCombined(common.TransactionCase): """ From a65419b0421538cfec7ce645e76a2caeec2ab763 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Tue, 25 Jun 2013 12:28:00 +0200 Subject: [PATCH 038/421] [IMP] add tests for apply_inheritance_specs error reporting (to improve later, eg by removing cr, uid, context, ...) bzr revid: xmo@openerp.com-20130625102800-qpw6m9m5cib1qvu8 --- openerp/addons/base/tests/test_views.py | 30 +++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/openerp/addons/base/tests/test_views.py b/openerp/addons/base/tests/test_views.py index 87a018888d5..8a9a0dcb467 100644 --- a/openerp/addons/base/tests/test_views.py +++ b/openerp/addons/base/tests/test_views.py @@ -359,6 +359,36 @@ class TestApplyInheritanceSpecs(common.TransactionCase): name="target"), string="Title"))) + def test_invalid_position(self): + spec = ET.tostring( + Field( + Field(name="whoops"), + name="target", position="serious_series")) + + with self.assertRaises(AttributeError): + self.View.apply_inheritance_specs(self.cr, self.uid, 'test', + None, self.base_arch, + None, spec) + + def test_incorrect_version(self): + # Version ignored on //field elements, so use something else + arch = E.form(E.element(foo="42")) + spec = ET.tostring(E.element( + Field(name="placeholder"), + foo="42", version="7.0")) + + with self.assertRaises(AttributeError): + self.View.apply_inheritance_specs(self.cr, self.uid, 'test', + None, arch, + None, spec) + + def test_target_not_found(self): + spec = ET.tostring(Field(name="targut")) + + with self.assertRaises(AttributeError): + self.View.apply_inheritance_specs(self.cr, self.uid, 'test', + None, self.base_arch, + None, spec) class TestApplyInheritedArchs(common.TransactionCase): """ Applies a sequence of modificator archs to a base view From f1691693775f229d5147f9479f0971d88251b1b6 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Tue, 25 Jun 2013 19:01:18 +0200 Subject: [PATCH 039/421] [FIX] view validation tests bzr revid: chs@openerp.com-20130625170118-dab54jupbexmqoq5 --- openerp/tests/test_view_validation.py | 15 +++++++++------ openerp/tools/view_validation.py | 7 ++++++- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/openerp/tests/test_view_validation.py b/openerp/tests/test_view_validation.py index 362bdc5d851..122f2f3ef4e 100644 --- a/openerp/tests/test_view_validation.py +++ b/openerp/tests/test_view_validation.py @@ -4,7 +4,10 @@ from lxml import etree from StringIO import StringIO import unittest2 -from openerp.tools.view_validation import * +from openerp.tools.view_validation import (valid_page_in_book, valid_att_in_form, valid_type_in_colspan, + valid_type_in_col, valid_att_in_field, valid_att_in_label, + valid_field_in_graph, valid_field_in_tree + ) invalid_form = etree.parse(StringIO('''\ @@ -79,7 +82,7 @@ invalid_tree = etree.parse(StringIO('''\ ''')).getroot() -valid_tree= etree.parse(StringIO('''\ +valid_tree = etree.parse(StringIO('''\ @@ -97,12 +100,12 @@ class test_view_validation(unittest2.TestCase): assert valid_page_in_book(valid_form) def test_all_field_validation(self): - assert not valid_att_in_field(invalid_form) - assert valid_att_in_field(valid_form) + assert not valid_att_in_field(invalid_form) + assert valid_att_in_field(valid_form) def test_all_label_validation(self): - assert not valid_att_in_label(invalid_form) - assert valid_att_in_label(valid_form) + assert not valid_att_in_label(invalid_form) + assert valid_att_in_label(valid_form) def test_form_string_validation(self): assert not valid_att_in_form(invalid_form) diff --git a/openerp/tools/view_validation.py b/openerp/tools/view_validation.py index 27967c42c12..9101d10064e 100644 --- a/openerp/tools/view_validation.py +++ b/openerp/tools/view_validation.py @@ -40,6 +40,10 @@ def valid_att_in_label(arch): return not arch.xpath('//label[not ((@for) or (@string))]') +def valid_att_in_form(arch): + return True + + def valid_type_in_colspan(arch): """A `colspan` attribute must be an `integer` type.""" for attrib in arch.xpath('//*/@colspan'): @@ -62,7 +66,8 @@ def valid_type_in_col(arch): def valid_view(arch): if arch.tag == 'form': - for pred in [valid_page_in_book, valid_type_in_colspan, valid_type_in_col, valid_att_in_field, valid_att_in_label]: + for pred in [valid_page_in_book, valid_att_in_form, valid_type_in_colspan, + valid_type_in_col, valid_att_in_field, valid_att_in_label]: if not pred(arch): _logger.error('Invalid XML: %s', pred.__doc__) return False From c5bd4b6d8d5d21251d68d9a9fdb109addbb5e629 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Tue, 25 Jun 2013 21:12:59 +0200 Subject: [PATCH 040/421] [FIX] view validation tests bzr revid: chs@openerp.com-20130625191259-wplffcvrgmlk6hmz --- openerp/tests/test_view_validation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openerp/tests/test_view_validation.py b/openerp/tests/test_view_validation.py index 122f2f3ef4e..4cd8b95bbaa 100644 --- a/openerp/tests/test_view_validation.py +++ b/openerp/tests/test_view_validation.py @@ -108,7 +108,6 @@ class test_view_validation(unittest2.TestCase): assert valid_att_in_label(valid_form) def test_form_string_validation(self): - assert not valid_att_in_form(invalid_form) assert valid_att_in_form(valid_form) def test_graph_validation(self): From a93bc8bdb01554caa7892d121f1e9ee21dc2d67b Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Thu, 27 Jun 2013 11:13:29 +0200 Subject: [PATCH 041/421] qweb templates bzr revid: al@openerp.com-20130627091329-t3hjytf3gt0dmkvo --- openerp/addons/base/ir/ir_ui_view.py | 14 +- openerp/tools/qweb.py | 241 +++++++++++++++++++++++++++ 2 files changed, 252 insertions(+), 3 deletions(-) create mode 100644 openerp/tools/qweb.py diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index d70bf4d02c8..0a8042bd941 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -19,13 +19,13 @@ # ############################################################################## import copy - -import logging import itertools -from lxml import etree +import logging import os import time +from lxml import etree + from openerp import tools from openerp.modules import module from openerp.osv import fields, osv, orm @@ -829,6 +829,13 @@ class view(osv.osv): raise orm.except_orm('View error', msg) return arch, fields + def render(self, cr, uid, id_or_xml_id, values, context=None): + def loader(name): + xml = self.read_combined(self, cr, uid, id_or_xml_id, context=context) + return xml['arch'] + engine = openerp.tools.qweb.QWebXml(loader) + return engine.render(id_or_xml_id, values) + class view_sc(osv.osv): _name = 'ir.ui.view_sc' _columns = { @@ -864,5 +871,6 @@ class view_sc(osv.osv): ('shortcut_unique', 'unique(res_id, resource, user_id)', 'Shortcut for this menu already exists!'), ] + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/tools/qweb.py b/openerp/tools/qweb.py new file mode 100644 index 00000000000..6d0eee8feba --- /dev/null +++ b/openerp/tools/qweb.py @@ -0,0 +1,241 @@ +import xml +import re + +class QWebEval(object): + def __init__(self,data): + self.data=data + def __getitem__(self,expr): + if self.data.has_key(expr): + return self.data[expr] + r=None + try: + r=eval(expr,self.data) + except NameError,e: + pass + except AttributeError,e: + pass + except Exception,e: + print "qweb: expression error '%s' "%expr,e + if self.data.has_key("__builtins__"): + del self.data["__builtins__"] + return r + def eval_object(self,expr): + return self[expr] + def eval_str(self,expr): + if expr=="0": + return self.data[0] + if isinstance(self[expr],unicode): + return self[expr].encode("utf8") + return str(self[expr]) + def eval_format(self,expr): + try: + return str(expr%self) + except: + return "qweb: format error '%s' "%expr +# if isinstance(r,unicode): +# return r.encode("utf8") + def eval_bool(self,expr): + if self.eval_object(expr): + return 1 + else: + return 0 + +class QWebXml(object): + """QWeb Xml templating engine + + The templating engine use a very simple syntax, "magic" xml attributes, to + produce any kind of texutal output (even non-xml). + + QWebXml: + the template engine core implements the basic magic attributes: + + t-att t-raw t-esc t-if t-foreach t-set t-call t-trim + + + - loader: function that return a template + + + """ + def __init__(self, loader): + self.loader = loader + self.node=xml.dom.Node + self._t={} + self._render_tag={} + + prefix='render_tag_' + for i in [j for j in dir(self) if j.startswith(prefix)]: + name=i[len(prefix):].replace('_','-') + self._render_tag[name]=getattr(self.__class__,i) + + self._render_att={} + prefix='render_att_' + for i in [j for j in dir(self) if j.startswith(prefix)]: + name=i[len(prefix):].replace('_','-') + self._render_att[name]=getattr(self.__class__,i) + + def register_tag(self,tag,func): + self._render_tag[tag]=func + def add_template(self,x): + if hasattr(x,'documentElement'): + dom=x + elif x.startswith("%s%s"%(name,g_att,pre,inner,name) + else: + return "<%s%s/>"%(name,g_att) + + # Attributes + def render_att_att(self,e,an,av,v): + if an.startswith("t-attf-"): + att,val=an[7:],self.eval_format(av,v) + elif an.startswith("t-att-"): + att,val=(an[6:],self.eval_str(av,v)) + else: + att,val=self.eval_object(av,v) + return ' %s="%s"'%(att,cgi.escape(val,1)) + + # Tags + def render_tag_raw(self,e,t_att,g_att,v): + return self.eval_str(t_att["raw"],v) + def render_tag_rawf(self,e,t_att,g_att,v): + return self.eval_format(t_att["rawf"],v) + def render_tag_esc(self,e,t_att,g_att,v): + return cgi.escape(self.eval_str(t_att["esc"],v)) + def render_tag_escf(self,e,t_att,g_att,v): + return cgi.escape(self.eval_format(t_att["escf"],v)) + def render_tag_foreach(self,e,t_att,g_att,v): + expr=t_att["foreach"] + enum=self.eval_object(expr,v) + if enum!=None: + var=t_att.get('as',expr).replace('.','_') + d=v.copy() + size=-1 + if isinstance(enum,types.ListType): + size=len(enum) + elif isinstance(enum,types.TupleType): + size=len(enum) + elif hasattr(enum,'count'): + size=enum.count() + d["%s_size"%var]=size + d["%s_all"%var]=enum + index=0 + ru=[] + for i in enum: + d["%s_value"%var]=i + d["%s_index"%var]=index + d["%s_first"%var]=index==0 + d["%s_even"%var]=index%2 + d["%s_odd"%var]=(index+1)%2 + d["%s_last"%var]=index+1==size + if index%2: + d["%s_parity"%var]='odd' + else: + d["%s_parity"%var]='even' + if isinstance(i,types.DictType): + d.update(i) + else: + d[var]=i + ru.append(self.render_element(e,g_att,d)) + index+=1 + return "".join(ru) + else: + return "qweb: t-foreach %s not found."%expr + def render_tag_if(self,e,t_att,g_att,v): + if self.eval_bool(t_att["if"],v): + return self.render_element(e,g_att,v) + else: + return "" + def render_tag_call(self,e,t_att,g_att,v): + # TODO t-prefix + if t_att.has_key("import"): + d=v + else: + d=v.copy() + d[0]=self.render_element(e,g_att,d) + return self.render(t_att["call"],d) + def render_tag_set(self,e,t_att,g_att,v): + if t_att.has_key("eval"): + v[t_att["set"]]=self.eval_object(t_att["eval"],v) + else: + v[t_att["set"]]=self.render_element(e,g_att,v) + return "" + +# From 01c08e2ebca814edb111fe88fd4eee4b992f45b9 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Thu, 27 Jun 2013 15:16:29 +0200 Subject: [PATCH 042/421] type is regular field bzr revid: al@openerp.com-20130627131629-1au7xtdv2fdxtko0 --- openerp/addons/base/ir/ir_ui_view.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 0a8042bd941..cd3e18df108 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -98,7 +98,7 @@ class view(osv.osv): 'name': fields.char('View Name', required=True), 'model': fields.char('Object', size=64, select=True), 'priority': fields.integer('Sequence', required=True), - 'type': fields.function(_type_field, type='selection', selection=[ + 'type': fields.selection([ ('tree','Tree'), ('form','Form'), ('mdx','mdx'), @@ -107,7 +107,8 @@ class view(osv.osv): ('diagram','Diagram'), ('gantt', 'Gantt'), ('kanban', 'Kanban'), - ('search','Search')], string='View Type', select=True, store=True), + ('search','Search'), + ('qweb', 'QWeb')], string='View Type'), 'arch_file': fields.char("View path"), 'arch_db': fields.text("Arch content", oldname='arch'), 'arch': fields.function(_arch_get, fnct_inv=_arch_set, store=False, string="View Architecture", type='text', nodrop=True), @@ -128,14 +129,13 @@ class view(osv.osv): _relaxng_validator = None def create(self, cr, uid, values, context=None): - if 'type' in values: - _logger.warning("Setting the `type` field is deprecated in the `ir.ui.view` model.") - if not values.get('name'): + if not 'type' in values: if values.get('inherit_id'): - inferred_type = self.browse(cr, uid, values['inherit_id'], context).type + values['type'] = self.browse(cr, uid, values['inherit_id'], context).type else: - inferred_type = etree.fromstring(values['arch'].encode('utf8')).tag - values['name'] = "%s %s" % (values['model'], inferred_type) + values['type'] = etree.fromstring(values['arch'].encode('utf8')).tag + if not values.get('name'): + values['name'] = "%s %s" % (values['model'], values['']) return super(view, self).create(cr, uid, values, context) def _relaxng(self): @@ -832,7 +832,10 @@ class view(osv.osv): def render(self, cr, uid, id_or_xml_id, values, context=None): def loader(name): xml = self.read_combined(self, cr, uid, id_or_xml_id, context=context) + # parse arch + # on the root tag of arch add the attribute t-name="" return xml['arch'] + engine = openerp.tools.qweb.QWebXml(loader) return engine.render(id_or_xml_id, values) From 09828511536cb4e3cbd14b4672ea27bbaa869ca7 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Thu, 27 Jun 2013 16:20:22 +0200 Subject: [PATCH 043/421] [FIX] qweb: make code works bzr revid: chs@openerp.com-20130627142022-fg0brlf5mjrpaulb --- openerp/tools/qweb.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/openerp/tools/qweb.py b/openerp/tools/qweb.py index 6d0eee8feba..5d146cf0020 100644 --- a/openerp/tools/qweb.py +++ b/openerp/tools/qweb.py @@ -1,5 +1,7 @@ -import xml -import re +import cgi +import types + +import xml # FIXME use lxml class QWebEval(object): def __init__(self,data): @@ -87,16 +89,16 @@ class QWebXml(object): for n in dom.documentElement.childNodes: if n.nodeName=="t": self._t[str(n.getAttribute("t-name"))]=n - def get_template(self,name): + + def get_template(self, name): if name in self._t: return self._t[name] else: - xml = loader(name) - add_template(xml) + xml = self.loader(name) + self.add_template(xml) if name in self._t: return self._t[name] - return 'qweb: template "%s" not found' % name - + raise KeyError('qweb: template "%s" not found' % name) def eval_object(self,expr,v): return QWebEval(v).eval_object(expr) From 4efebc6fd0c70923341948b91d522cfd1fbabd7c Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Thu, 27 Jun 2013 16:25:06 +0200 Subject: [PATCH 044/421] [FIX] ir.ui.view:render() can render view by xmlid/path bzr revid: chs@openerp.com-20130627142506-gdmre0jj36v5inc0 --- openerp/addons/base/ir/ir_ui_view.py | 44 ++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index cd3e18df108..010cc34216b 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -19,9 +19,11 @@ # ############################################################################## import copy +from functools import partial import itertools import logging import os +import sys import time from lxml import etree @@ -33,7 +35,7 @@ from openerp.tools import graph, SKIPPED_ELEMENT_TYPES from openerp.tools.safe_eval import safe_eval as eval from openerp.tools.translate import _ from openerp.tools.view_validation import valid_view -from openerp.tools import misc +from openerp.tools import misc, qweb _logger = logging.getLogger(__name__) @@ -829,14 +831,46 @@ class view(osv.osv): raise orm.except_orm('View error', msg) return arch, fields + def _get_arch(self, cr, uid, id_, context=None): + from pprint import pprint as pp + pp(id_) + try: + id_ = int(id_) + except ValueError: + if '/' not in id_ and '.' not in id_: + raise ValueError('Invalid id: %r' % (id_,)) + s = id_.find('/') + s = s if s >= 0 else sys.maxint + d = id_.find('.') + d = d if d >= 0 else sys.maxint + + if d < s: + # xml id + IMD = self.pool['ir.model.data'] + m, _, n = id_.partition('.') + _, id_ = IMD.get_object_reference(cr, uid, m, n) + else: + # path id => read directly on disk + # TODO apply inheritence + try: + with misc.file_open(id_) as f: + return f.read().decode('utf-8') + except Exception: + raise ValueError('Invalid id: %r' % (id_,)) + + pp(id_) + r = self.read_combined(cr, uid, id_, fields=['arch'], view_type=None, model=None, context=context) + pp(r) + return r['arch'] + def render(self, cr, uid, id_or_xml_id, values, context=None): def loader(name): - xml = self.read_combined(self, cr, uid, id_or_xml_id, context=context) + arch = self._get_arch(cr, uid, name, context=context) # parse arch # on the root tag of arch add the attribute t-name="" - return xml['arch'] - - engine = openerp.tools.qweb.QWebXml(loader) + arch = u'{1}'.format(name, arch) + return arch + engine = qweb.QWebXml(loader) return engine.render(id_or_xml_id, values) class view_sc(osv.osv): From 0ed83c3cec5719d20e93354cf62b7b63ac12fbe5 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Thu, 27 Jun 2013 16:36:39 +0200 Subject: [PATCH 045/421] [FIX] view creation bzr revid: chs@openerp.com-20130627143639-nxurinzeqqjqdiam --- 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 010cc34216b..29c3e8ae6c4 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -137,7 +137,7 @@ class view(osv.osv): else: values['type'] = etree.fromstring(values['arch'].encode('utf8')).tag if not values.get('name'): - values['name'] = "%s %s" % (values['model'], values['']) + values['name'] = "%s %s" % (values['model'], values['type']) return super(view, self).create(cr, uid, values, context) def _relaxng(self): From a0d05319e457d967edfedda84decab11f8625ad1 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Thu, 27 Jun 2013 17:08:07 +0200 Subject: [PATCH 046/421] [FIX] qweb: flake8 bzr revid: chs@openerp.com-20130627150807-1xy7kr9g2xlsnqf2 --- openerp/tools/qweb.py | 320 ++++++++++++++++++++++-------------------- 1 file changed, 170 insertions(+), 150 deletions(-) diff --git a/openerp/tools/qweb.py b/openerp/tools/qweb.py index 5d146cf0020..a1ee2feae2e 100644 --- a/openerp/tools/qweb.py +++ b/openerp/tools/qweb.py @@ -1,42 +1,53 @@ import cgi +import logging import types +from openerp.tools.safe_eval import safe_eval as eval + import xml # FIXME use lxml +import xml.dom.minidom + +_logger = logging.getLogger(__name__) class QWebEval(object): - def __init__(self,data): - self.data=data - def __getitem__(self,expr): - if self.data.has_key(expr): + def __init__(self, data): + self.data = data + + def __getitem__(self, expr): + if expr in self.data: return self.data[expr] - r=None + r = None try: - r=eval(expr,self.data) - except NameError,e: + r = eval(expr, self.data) + except NameError: pass - except AttributeError,e: + except AttributeError: pass - except Exception,e: - print "qweb: expression error '%s' "%expr,e - if self.data.has_key("__builtins__"): - del self.data["__builtins__"] + except Exception: + _logger.exception('invalid expression: %r', expr) + + self.data.pop('__builtins__', None) return r - def eval_object(self,expr): + + def eval_object(self, expr): return self[expr] - def eval_str(self,expr): - if expr=="0": + + def eval_str(self, expr): + if expr == "0": return self.data[0] - if isinstance(self[expr],unicode): + if isinstance(self[expr], unicode): return self[expr].encode("utf8") return str(self[expr]) - def eval_format(self,expr): + + def eval_format(self, expr): try: - return str(expr%self) + return str(expr % self) except: - return "qweb: format error '%s' "%expr + return "qweb: format error '%s' " % expr # if isinstance(r,unicode): # return r.encode("utf8") - def eval_bool(self,expr): + + def eval_bool(self, expr): if self.eval_object(expr): return 1 else: @@ -44,15 +55,15 @@ class QWebEval(object): class QWebXml(object): """QWeb Xml templating engine - + The templating engine use a very simple syntax, "magic" xml attributes, to produce any kind of texutal output (even non-xml). - + QWebXml: the template engine core implements the basic magic attributes: - + t-att t-raw t-esc t-if t-foreach t-set t-call t-trim - + - loader: function that return a template @@ -60,35 +71,34 @@ class QWebXml(object): """ def __init__(self, loader): self.loader = loader - self.node=xml.dom.Node - self._t={} - self._render_tag={} + self.node = xml.dom.Node + self._t = {} + self._render_tag = {} - prefix='render_tag_' + prefix = 'render_tag_' for i in [j for j in dir(self) if j.startswith(prefix)]: - name=i[len(prefix):].replace('_','-') - self._render_tag[name]=getattr(self.__class__,i) + name = i[len(prefix):].replace('_', '-') + self._render_tag[name] = getattr(self.__class__, i) - self._render_att={} - prefix='render_att_' + self._render_att = {} + prefix = 'render_att_' for i in [j for j in dir(self) if j.startswith(prefix)]: - name=i[len(prefix):].replace('_','-') - self._render_att[name]=getattr(self.__class__,i) + name = i[len(prefix):].replace('_', '-') + self._render_att[name] = getattr(self.__class__, i) - def register_tag(self,tag,func): - self._render_tag[tag]=func - def add_template(self,x): - if hasattr(x,'documentElement'): - dom=x + def register_tag(self, tag, func): + self._render_tag[tag] = func + + def add_template(self, x): + if hasattr(x, 'documentElement'): + dom = x elif x.startswith("%s%s"%(name,g_att,pre,inner,name) + return "<%s%s>%s%s" % (name, g_att, pre, inner, name) else: - return "<%s%s/>"%(name,g_att) + return "<%s%s/>" % (name, g_att) # Attributes - def render_att_att(self,e,an,av,v): + def render_att_att(self, e, an, av, v): if an.startswith("t-attf-"): - att,val=an[7:],self.eval_format(av,v) + att, val = an[7:], self.eval_format(av, v) elif an.startswith("t-att-"): - att,val=(an[6:],self.eval_str(av,v)) + att, val = an[6:], self.eval_str(av, v) else: - att,val=self.eval_object(av,v) - return ' %s="%s"'%(att,cgi.escape(val,1)) + att, val = self.eval_object(av, v) + return ' %s="%s"' % (att, cgi.escape(val, 1)) # Tags - def render_tag_raw(self,e,t_att,g_att,v): - return self.eval_str(t_att["raw"],v) - def render_tag_rawf(self,e,t_att,g_att,v): - return self.eval_format(t_att["rawf"],v) - def render_tag_esc(self,e,t_att,g_att,v): - return cgi.escape(self.eval_str(t_att["esc"],v)) - def render_tag_escf(self,e,t_att,g_att,v): - return cgi.escape(self.eval_format(t_att["escf"],v)) - def render_tag_foreach(self,e,t_att,g_att,v): - expr=t_att["foreach"] - enum=self.eval_object(expr,v) - if enum!=None: - var=t_att.get('as',expr).replace('.','_') - d=v.copy() - size=-1 - if isinstance(enum,types.ListType): - size=len(enum) - elif isinstance(enum,types.TupleType): - size=len(enum) - elif hasattr(enum,'count'): - size=enum.count() - d["%s_size"%var]=size - d["%s_all"%var]=enum - index=0 - ru=[] + def render_tag_raw(self, e, t_att, g_att, v): + return self.eval_str(t_att["raw"], v) + + def render_tag_rawf(self, e, t_att, g_att, v): + return self.eval_format(t_att["rawf"], v) + + def render_tag_esc(self, e, t_att, g_att, v): + return cgi.escape(self.eval_str(t_att["esc"], v)) + + def render_tag_escf(self, e, t_att, g_att, v): + return cgi.escape(self.eval_format(t_att["escf"], v)) + + def render_tag_foreach(self, e, t_att, g_att, v): + expr = t_att["foreach"] + enum = self.eval_object(expr, v) + if enum is not None: + var = t_att.get('as', expr).replace('.', '_') + d = v.copy() + size = -1 + if isinstance(enum, types.ListType): + size = len(enum) + elif isinstance(enum, types.TupleType): + size = len(enum) + elif hasattr(enum, 'count'): + size = enum.count() + d["%s_size" % var] = size + d["%s_all" % var] = enum + index = 0 + ru = [] for i in enum: - d["%s_value"%var]=i - d["%s_index"%var]=index - d["%s_first"%var]=index==0 - d["%s_even"%var]=index%2 - d["%s_odd"%var]=(index+1)%2 - d["%s_last"%var]=index+1==size - if index%2: - d["%s_parity"%var]='odd' + d["%s_value" % var] = i + d["%s_index" % var] = index + d["%s_first" % var] = index == 0 + d["%s_even" % var] = index % 2 + d["%s_odd" % var] = (index + 1) % 2 + d["%s_last" % var] = index + 1 == size + if index % 2: + d["%s_parity" % var] = 'odd' else: - d["%s_parity"%var]='even' - if isinstance(i,types.DictType): + d["%s_parity" % var] = 'even' + if isinstance(i, types.DictType): d.update(i) else: - d[var]=i - ru.append(self.render_element(e,g_att,d)) - index+=1 + d[var] = i + ru.append(self.render_element(e, g_att, d)) + index += 1 return "".join(ru) else: - return "qweb: t-foreach %s not found."%expr - def render_tag_if(self,e,t_att,g_att,v): - if self.eval_bool(t_att["if"],v): - return self.render_element(e,g_att,v) + return "qweb: t-foreach %s not found." % expr + + def render_tag_if(self, e, t_att, g_att, v): + if self.eval_bool(t_att["if"], v): + return self.render_element(e, g_att, v) else: return "" - def render_tag_call(self,e,t_att,g_att,v): - # TODO t-prefix - if t_att.has_key("import"): - d=v - else: - d=v.copy() - d[0]=self.render_element(e,g_att,d) - return self.render(t_att["call"],d) - def render_tag_set(self,e,t_att,g_att,v): - if t_att.has_key("eval"): - v[t_att["set"]]=self.eval_object(t_att["eval"],v) - else: - v[t_att["set"]]=self.render_element(e,g_att,v) - return "" -# + def render_tag_call(self, e, t_att, g_att, v): + # TODO t-prefix + if "import" in t_att: + d = v + else: + d = v.copy() + d[0] = self.render_element(e, g_att, d) + return self.render(t_att["call"], d) + + def render_tag_set(self, e, t_att, g_att, v): + if "eval" in t_att: + v[t_att["set"]] = self.eval_object(t_att["eval"], v) + else: + v[t_att["set"]] = self.render_element(e, g_att, v) + return "" From d84c90b41d6aea58b2030a8199877624db557979 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Thu, 27 Jun 2013 17:37:58 +0200 Subject: [PATCH 047/421] [FIX] views: correct type detection bzr revid: chs@openerp.com-20130627153758-1yhk7d79is5c707r --- openerp/addons/base/ir/ir_ui_view.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 29c3e8ae6c4..271c462d0df 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -131,11 +131,12 @@ class view(osv.osv): _relaxng_validator = None def create(self, cr, uid, values, context=None): - if not 'type' in values: + if 'type' not in values: if values.get('inherit_id'): values['type'] = self.browse(cr, uid, values['inherit_id'], context).type else: - values['type'] = etree.fromstring(values['arch'].encode('utf8')).tag + values['type'] = etree.fromstring(values['arch']).tag + if not values.get('name'): values['name'] = "%s %s" % (values['model'], values['type']) return super(view, self).create(cr, uid, values, context) From ad9f8a9fa1a00606a33018c90af082e7000f86f7 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Thu, 27 Jun 2013 19:10:27 +0200 Subject: [PATCH 048/421] [ADD] ir_ui_view: add tags to tree bzr revid: mat@openerp.com-20130627171027-j3t7l8dadm8cohvx --- openerp/addons/base/ir/ir_ui_view.py | 65 +++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 5 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 271c462d0df..f6c34f176fb 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -221,6 +221,47 @@ class view(osv.osv): :type fallback: mapping """ if context is None: context = {} + + def clean_anotations(arch, parent_info=None): + for child in arch: + if child.tag == 't' or child.tag == 'field': + # can not anote t and field while cleaning + continue + + child_text = "".join([etree.tostring(x) for x in child]) + if child.attrib.get('data-edit-model'): + if child_text.find('data-edit-model') != -1 or child_text.find(' Date: Sat, 29 Jun 2013 23:35:16 +0200 Subject: [PATCH 049/421] untangle inheritance and postprocessing bzr revid: al@openerp.com-20130629213516-pqcl6oz1g3o5tpgn --- openerp/addons/base/ir/ir_ui_view.py | 604 +++++++++++---------------- openerp/osv/orm.py | 167 ++++---- 2 files changed, 324 insertions(+), 447 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index f6c34f176fb..b3a70b684e5 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -57,19 +57,6 @@ class view_custom(osv.osv): class view(osv.osv): _name = 'ir.ui.view' - class NoViewError(Exception): pass - class NoDefaultError(NoViewError): pass - - def _type_field(self, cr, uid, ids, name, args, context=None): - result = {} - for record in self.browse(cr, uid, ids, context): - # Get the type from the inherited view if any. - if record.inherit_id: - result[record.id] = record.inherit_id.type - else: - result[record.id] = etree.fromstring(record.arch.encode('utf8')).tag - return result - def _arch_get(self, cr, uid, ids, name, arg, context=None): """ For each id being read, return arch_db or the content of arch_file @@ -130,17 +117,6 @@ class view(osv.osv): # Holds the RNG schema _relaxng_validator = None - def create(self, cr, uid, values, context=None): - if 'type' not in values: - if values.get('inherit_id'): - values['type'] = self.browse(cr, uid, values['inherit_id'], context).type - else: - values['type'] = etree.fromstring(values['arch']).tag - - if not values.get('name'): - values['name'] = "%s %s" % (values['model'], values['type']) - return super(view, self).create(cr, uid, values, context) - def _relaxng(self): if not self._relaxng_validator: frng = tools.file_open(os.path.join('base','rng','view.rng')) @@ -157,7 +133,7 @@ class view(osv.osv): for view in self.browse(cr, uid, ids, context): # Sanity check: the view should not break anything upon rendering! try: - fvg = self.read_combined(cr, uid, view.id, view.type, view.model, context=context) + fvg = self.read_combined(cr, uid, view.id, None, context=context) view_arch_utf8 = fvg['arch'] except Exception, e: _logger.exception(e) @@ -179,15 +155,8 @@ class view(osv.osv): return False return True - def _check_model(self, cr, uid, ids, context=None): - for view in self.browse(cr, uid, ids, context): - if view.model and view.model not in self.pool: - return False - return True - _constraints = [ (_check_xml, 'Invalid XML for View Architecture!', ['arch']) - #(_check_model, 'The model name does not exist.', ['model']), ] def _auto_init(self, cr, context=None): @@ -196,107 +165,85 @@ class view(osv.osv): if not cr.fetchone(): cr.execute('CREATE INDEX ir_ui_view_model_type_inherit_id ON ir_ui_view (model, inherit_id)') - def read_combined(self, cr, uid, view_id, view_type, model, - fields=None, fallback=None, context=None): + def create(self, cr, uid, values, context=None): + if 'type' not in values: + if values.get('inherit_id'): + values['type'] = self.browse(cr, uid, values['inherit_id'], context).type + else: + values['type'] = etree.fromstring(values['arch']).tag + + if not values.get('name'): + values['name'] = "%s %s" % (values['model'], values['type']) + return super(view, self).create(cr, uid, values, context) + + def write(self, cr, uid, ids, vals, context=None): + if not isinstance(ids, (list, tuple)): + ids = [ids] + + # drop the corresponding view customizations (used for dashboards for example), otherwise + # not all users would see the updated views + custom_view_ids = self.pool.get('ir.ui.view.custom').search(cr, uid, [('ref_id','in',ids)]) + if custom_view_ids: + self.pool.get('ir.ui.view.custom').unlink(cr, uid, custom_view_ids) + + return super(view, self).write(cr, uid, ids, vals, 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: + view with no parent (inherit_id=Fase) with the lowest priority. + + :param str model: + :param int view_type: + :return: id of the default view of False if none found + :rtype: int """ - Utility function stringing together all method calls necessary to get - a full, final view: + ids = self.search(cr, uid, [ + ['model', '=', model], + ['type', '=', view_type], + ['inherit_id', '=', False], + ], limit=1, order='priority', context=context) + if not ids: + return False + return ids[0] - * Gets the default view if no view_id is provided - * Gets the top of the view tree if a sub-view is requested - * Applies all inherited archs on the root view - * Applies post-processing - * Returns the view with all requested fields + # inheritance - .. note:: ``arch`` is always added to the fields list even if not - requested (similar to ``id``) + def get_inheriting_views_arch(self, cr, uid, view_id, context=None): + """Retrieves the architecture of views that inherit from the given view, from the sets of + views that should currently be used in the system. During the module upgrade phase it + may happen that a view is present in the database but the fields it relies on are not + fully loaded yet. This method only considers views that belong to modules whose code + is already loaded. Custom views defined directly in the database are loaded only + after the module initialization phase is completely finished. - If no view is available (no view_id or invalid view_id provided, or - no view stored for (model, view_type)) a view record will be fetched - from the ``defaults`` mapping? - - :param fallback: a mapping of {view_type: view_dict}, if no view can - be found (read) will be used to provide a default - before post-processing - :type fallback: mapping + :param int view_id: id of the view whose inheriting views should be retrieved + :param str model: model identifier of the view's related model (for double-checking) + :rtype: list of tuples + :return: [(view_arch,view_id), ...] """ - if context is None: context = {} + user_groups = frozenset(self.pool.get('res.users').browse(cr, 1, uid, context).groups_id) - def clean_anotations(arch, parent_info=None): - for child in arch: - if child.tag == 't' or child.tag == 'field': - # can not anote t and field while cleaning - continue + conditions = [['inherit_id', '=', view_id]] + if self.pool._init: + # Module init currently in progress, only consider views from + # modules whose code is already loaded + conditions.extend([ + ['model_ids.model', '=', 'ir.ui.view'], + ['model_ids.module', 'in', tuple(self.pool._init_modules)], + ]) + view_ids = self.search(cr, uid, conditions, context=context) - child_text = "".join([etree.tostring(x) for x in child]) - if child.attrib.get('data-edit-model'): - if child_text.find('data-edit-model') != -1 or child_text.find('{1}'.format(name, arch) @@ -929,6 +756,75 @@ class view(osv.osv): engine = qweb.QWebXml(loader) return engine.render(id_or_xml_id, values) + # maybe used to print the workflow ? + + def graph_get(self, cr, uid, id, model, node_obj, conn_obj, src_node, des_node, label, scale, context=None): + nodes=[] + nodes_name=[] + transitions=[] + start=[] + tres={} + labels={} + no_ancester=[] + blank_nodes = [] + + _Model_Obj = self.pool[model] + _Node_Obj = self.pool[node_obj] + _Arrow_Obj = self.pool[conn_obj] + + for model_key,model_value in _Model_Obj._columns.items(): + if model_value._type=='one2many': + if model_value._obj==node_obj: + _Node_Field=model_key + _Model_Field=model_value._fields_id + flag=False + for node_key,node_value in _Node_Obj._columns.items(): + if node_value._type=='one2many': + if node_value._obj==conn_obj: + if src_node in _Arrow_Obj._columns and flag: + _Source_Field=node_key + if des_node in _Arrow_Obj._columns and not flag: + _Destination_Field=node_key + flag = True + + datas = _Model_Obj.read(cr, uid, id, [],context) + for a in _Node_Obj.read(cr,uid,datas[_Node_Field],[]): + if a[_Source_Field] or a[_Destination_Field]: + nodes_name.append((a['id'],a['name'])) + nodes.append(a['id']) + else: + blank_nodes.append({'id': a['id'],'name':a['name']}) + + if a.has_key('flow_start') and a['flow_start']: + start.append(a['id']) + else: + if not a[_Source_Field]: + no_ancester.append(a['id']) + for t in _Arrow_Obj.read(cr,uid, a[_Destination_Field],[]): + transitions.append((a['id'], t[des_node][0])) + tres[str(t['id'])] = (a['id'],t[des_node][0]) + label_string = "" + if label: + for lbl in eval(label): + if t.has_key(tools.ustr(lbl)) and tools.ustr(t[lbl])=='False': + label_string += ' ' + else: + label_string = label_string + " " + tools.ustr(t[lbl]) + labels[str(t['id'])] = (a['id'],label_string) + g = graph(nodes, transitions, no_ancester) + g.process(start) + g.scale(*scale) + result = g.result_get() + results = {} + for node in nodes_name: + results[str(node[0])] = result[node[0]] + results[str(node[0])]['name'] = node[1] + return {'nodes': results, + 'transitions': tres, + 'label' : labels, + 'blank_nodes': blank_nodes, + 'node_parent_field': _Model_Field,} + class view_sc(osv.osv): _name = 'ir.ui.view_sc' _columns = { diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 04571cf1717..51d49950aa5 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -634,27 +634,6 @@ class MetaModel(type): if not self._custom: self.module_to_models.setdefault(self._module, []).append(self) -class FallbackViewMapping(collections.defaultdict): - def __init__(self, cr, uid, model, context=None): - super(FallbackViewMapping, self).__init__() - self.cr = cr - self.uid = uid - self.model = model - self.context = context - - def __missing__(self, view_type): - try: - arch = getattr(self.model, '_get_default_%s_view' % view_type)( - self.cr, self.uid, self.context) - except AttributeError: - raise except_orm(_('Invalid Architecture!'), _("There is no view of type '%s' defined for the structure!") % view_type) - return { - 'id': 0, - 'type': view_type, - 'name': 'default', - 'field_parent': False, - 'arch': arch, - } # Definition of log access columns, automatically added to models if # self._log_access is True @@ -1796,15 +1775,12 @@ class BaseModel(object): return view - def fields_view_get(self, cr, user, view_id=None, view_type='form', context=None, toolbar=False, submenu=False): + def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False): """ Get the detailed composition of the requested view like fields, model, view architecture - :param cr: database cursor - :param user: current user id :param view_id: id of the view or None :param view_type: type of the view to return if view_id is None ('form', tree', ...) - :param context: context arguments, like lang, time zone :param toolbar: true to include contextual actions :param submenu: deprecated :return: dictionary describing the composition of the requested view (including inherited views and extensions) @@ -1812,69 +1788,72 @@ class BaseModel(object): * if the inherited view has unknown position to work with other than 'before', 'after', 'inside', 'replace' * if some tag other than 'position' is found in parent view :raise Invalid ArchitectureError: if there is view type other than form, tree, calendar, search etc defined on the structure - """ if context is None: context = {} View = self.pool['ir.ui.view'] - view_ref = context.get(view_type + '_view_ref') - - if view_ref and not view_id and '.' in view_ref: - module, view_ref = view_ref.split('.', 1) - cr.execute("SELECT res_id FROM ir_model_data WHERE model='ir.ui.view' AND module=%s AND name=%s", (module, view_ref)) - view_ref_res = cr.fetchone() - if view_ref_res: - view_id = view_ref_res[0] - - root_view = View.read_combined( - cr, user, view_id, view_type, self._name, fields=[ - 'id', 'name', 'field_parent', 'type', 'model', 'arch' - ], fallback=FallbackViewMapping(cr, user, self, context=context), - context=context) - result = { 'model': self._name, - 'arch': root_view['arch'], - 'type': root_view['type'], - 'view_id': root_view['id'], - 'name': root_view['name'], - 'field_parent': root_view['field_parent'] or False + 'field_parent': False, } + # try to find a view_id if none provided + if not view_id: + # _view_ref in context can be used to overrride the default view + view_ref = context.get(view_type + '_view_ref') + if view_ref and '.' in view_ref: + module, view_ref = view_ref.split('.', 1) + cr.execute("SELECT res_id FROM ir_model_data WHERE model='ir.ui.view' AND module=%s AND name=%s", (module, view_ref)) + view_ref_res = cr.fetchone() + if view_ref_res: + view_id = view_ref_res[0] + else: + # otherwise try to find the lowest priority matching ir.ui.view + view_id = View.default_view(cr, uid, self._name, view_type, context=context) + + if view_id: + # read the view with inherited views applied + root_view = View.read_combined(cr, uid, view_id, fields=['id', 'name', 'field_parent', 'type', 'model', 'arch'], context=context) + result['arch'] = root_view['arch'] + result['name'] = root_view['name'] + result['type'] = root_view['type'] + result['view_id'] = root_view['id'] + result['field_parent'] = root_view['field_parent'] + else: + # fallback on default views methods if no ir.ui.view could be found + try: + get_func = getattr(self, '_get_default_%s_view' % view_type) + arch_etree = get_func(self.cr, self.uid, self.context) + result['arch'] = etree.tostring(arch_etree, encoding='utf-8') + result['type'] = view_type + result['name'] = 'default' + except AttributeError: + raise except_orm(_('Invalid Architecture!'), _("No default view of type '%s' could be found !") % view_type) + + # Apply post processing, groups and modifiers etc... ctx = context if root_view.get('model') != self._name: ctx = dict(context, base_model_name=root_view.get('model')) - xarch, xfields = View._view__view_look_dom_arch( - cr, user, self._name, etree.fromstring(result['arch']), - result['view_id'], context=ctx) + xarch, xfields = View.postprocess_and_fields( cr, uid, self._name, etree.fromstring(result['arch']), result['view_id'], context=ctx) result['arch'] = xarch result['fields'] = xfields + # Add related action information if aksed if toolbar: + toclean = ('report_sxw_content', 'report_rml_content', 'report_sxw', 'report_rml', 'report_sxw_content_data', 'report_rml_content_data') def clean(x): x = x[2] - for key in ('report_sxw_content', 'report_rml_content', - 'report_sxw', 'report_rml', - 'report_sxw_content_data', 'report_rml_content_data'): + for key in toclean: if key in x: del x[key] return x ir_values_obj = self.pool.get('ir.values') - resprint = ir_values_obj.get(cr, user, 'action', - 'client_print_multi', [(self._name, False)], False, - context) - resaction = ir_values_obj.get(cr, user, 'action', - 'client_action_multi', [(self._name, False)], False, - context) - - resrelate = ir_values_obj.get(cr, user, 'action', - 'client_action_relate', [(self._name, False)], False, - context) - resaction = [clean(action) for action in resaction - if view_type == 'tree' or not action[2].get('multi')] - resprint = [clean(print_) for print_ in resprint - if view_type == 'tree' or not print_[2].get('multi')] + resprint = ir_values_obj.get(cr, uid, 'action', 'client_print_multi', [(self._name, False)], False, context) + resaction = ir_values_obj.get(cr, uid, 'action', 'client_action_multi', [(self._name, False)], False, context) + resrelate = ir_values_obj.get(cr, uid, 'action', 'client_action_relate', [(self._name, False)], False, context) + resaction = [clean(action) for action in resaction if view_type == 'tree' or not action[2].get('multi')] + resprint = [clean(print_) for print_ in resprint if view_type == 'tree' or not print_[2].get('multi')] #When multi="True" set it will display only in More of the list view resrelate = [clean(action) for action in resrelate if (action[2].get('multi') and view_type == 'tree') or (not action[2].get('multi') and view_type == 'form')] @@ -1890,7 +1869,7 @@ class BaseModel(object): return result def _view_look_dom_arch(self, cr, uid, node, view_id, context=None): - return self['ir.ui.view']._view__view_look_dom_arch( + return self['ir.ui.view'].postprocess_and_fields( cr, uid, self._name, node, view_id, context=context) def search_count(self, cr, user, args, context=None): @@ -3045,31 +3024,6 @@ class BaseModel(object): self._columns[field_name].required = True self._columns[field_name].ondelete = "cascade" - #def __getattr__(self, name): - # """ - # Proxies attribute accesses to the `inherits` parent so we can call methods defined on the inherited parent - # (though inherits doesn't use Python inheritance). - # Handles translating between local ids and remote ids. - # Known issue: doesn't work correctly when using python's own super(), don't involve inherit-based inheritance - # when you have inherits. - # """ - # for model, field in self._inherits.iteritems(): - # proxy = self.pool.get(model) - # if hasattr(proxy, name): - # attribute = getattr(proxy, name) - # if not hasattr(attribute, '__call__'): - # return attribute - # break - # else: - # return super(orm, self).__getattr__(name) - - # def _proxy(cr, uid, ids, *args, **kwargs): - # objects = self.browse(cr, uid, ids, kwargs.get('context', None)) - # lst = [obj[field].id for obj in objects if obj[field]] - # return getattr(proxy, name)(cr, uid, lst, *args, **kwargs) - - # return _proxy - def fields_get(self, cr, user, allfields=None, context=None, write_access=True): """ Return the definition of each field. @@ -4911,6 +4865,32 @@ class BaseModel(object): """ stuff to do right after the registry is built """ pass + + #def __getattr__(self, name): + # """ + # Proxies attribute accesses to the `inherits` parent so we can call methods defined on the inherited parent + # (though inherits doesn't use Python inheritance). + # Handles translating between local ids and remote ids. + # Known issue: doesn't work correctly when using python's own super(), don't involve inherit-based inheritance + # when you have inherits. + # """ + # for model, field in self._inherits.iteritems(): + # proxy = self.pool.get(model) + # if hasattr(proxy, name): + # attribute = getattr(proxy, name) + # if not hasattr(attribute, '__call__'): + # return attribute + # break + # else: + # return super(orm, self).__getattr__(name) + + # def _proxy(cr, uid, ids, *args, **kwargs): + # objects = self.browse(cr, uid, ids, kwargs.get('context', None)) + # lst = [obj[field].id for obj in objects if obj[field]] + # return getattr(proxy, name)(cr, uid, lst, *args, **kwargs) + + # return _proxy + def __getattr__(self, name): if name.startswith('signal_'): signal_name = name[len('signal_'):] @@ -4976,12 +4956,12 @@ def itemgetter_tuple(items): if len(items) == 1: return lambda gettable: (gettable[items[0]],) return operator.itemgetter(*items) + class ImportWarning(Warning): """ Used to send warnings upwards the stack during the import process """ pass - def convert_pgerror_23502(model, fields, info, e): m = re.match(r'^null value in column "(?P\w+)" violates ' r'not-null constraint\n', @@ -4997,6 +4977,7 @@ def convert_pgerror_23502(model, fields, info, e): 'message': message, 'field': field_name, } + def convert_pgerror_23505(model, fields, info, e): m = re.match(r'^duplicate key (?P\w+) violates unique constraint', str(e)) From 4aa13cf591e5bb9a66aea19d3e648a82984b197d Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Sun, 30 Jun 2013 01:41:58 +0200 Subject: [PATCH 050/421] correct inheritance branding bzr revid: al@openerp.com-20130629234158-ttav8rypjrkxvctf --- openerp/addons/base/ir/ir_ui_view.py | 70 ++++++++++++++++------------ 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index b3a70b684e5..da07db8fee0 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -241,7 +241,7 @@ class view(osv.osv): if not (view.groups_id and user_groups.isdisjoint(view.groups_id))] def raise_view_error(self, cr, uid, view_id, message, context=None): - view = self.browse(cr, uid, [view_id], context) + view = self.browse(cr, uid, [view_id], context)[0] message = "Inherit error: %s view_id: %s, xml_id: %s, model: %s, parent_view: %s" % (message, view_id, view.xml_id, view.model, view.inherit_id) raise AttributeError(message) @@ -257,7 +257,6 @@ class view(osv.osv): :param arch: a parent architecture to modify :param spec: a modifying node in an inheriting view :return: a node in the source matching the spec - """ if spec.tag == 'xpath': nodes = arch.xpath(spec.get('expr')) @@ -282,7 +281,20 @@ class view(osv.osv): return node return None - def apply_inheritance_specs(self, cr, uid, source, specs_arch, inherit_id, context=None): + def inherit_branding(self, specs_tree, view_id, xpath="/"): + for node in specs_tree: + if node.tag == 'data' or node.tag == 'xpath': + node = self.inherit_branding(node, view_id, xpath + node.tag + '/') + else: + node.attrib.update({ + 'data-oe-model': 'ir.ui.view', + 'data-oe-field': 'arch', + 'data-oe-view-id': str(view_id), + 'data-oe-xpath': xpath + }) + return specs_tree + + def apply_inheritance_specs(self, cr, uid, source, specs_tree, inherit_id, context=None): """ Apply an inheriting view (a descendant of the base view) Apply to a source architecture all the spec nodes (i.e. nodes @@ -290,14 +302,11 @@ class view(osv.osv): architecture) given by an inheriting view. :param Element source: a parent architecture to modify - :param descendant_id: the database id of the descendant - :param specs_arch: a modifying architecture in an inheriting view + :param Elepect specs_tree: a modifying architecture in an inheriting view + :param inherit_id: the database id of specs_arch :return: a modified source where the specs are applied :rtype: Element """ - if isinstance(specs_arch, unicode): - specs_arch = specs_arch.encode('utf-8') - specs_tree = etree.fromstring(specs_arch) # Queue of specification nodes (i.e. nodes describing where and # changes to apply to some parent architecture). specs = [specs_tree] @@ -352,28 +361,20 @@ class view(osv.osv): return source - def add_root_tags(self, arch_tree, model, view_id): - for child in arch_tree: - if child.tag == 'data' or child.tag == 'xpath': - child = self.add_root_tags(child, model, view_id) - else: - child.attrib.update({ - 'data-edit-model': model or 'undefined', - 'data-edit-view-id': str(view_id), - 'data-edit-xpath': '/' - }) - return arch_tree - - def apply_view_inheritance(self, cr, uid, source, inherit_id, context=None): + def apply_view_inheritance(self, cr, uid, source, source_id, context=None): """ Apply all the (directly and indirectly) inheriting views. :param source: a parent architecture to modify (with parent modifications already applied) - :param inherit_id: the database view_id of the parent view + :param source_id: the database view_id of the parent view :return: a modified source where all the modifying architecture are applied """ - sql_inherit = self.pool.get('ir.ui.view').get_inheriting_views_arch(cr, uid, inherit_id) - for (view_arch, view_id) in sql_inherit: - source = self.apply_inheritance_specs(cr, uid, source, view_arch, view_id, context=context) + if context is None: context = {} + sql_inherit = self.pool.get('ir.ui.view').get_inheriting_views_arch(cr, uid, source_id) + for (specs, view_id) in sql_inherit: + specs_tree = etree.fromstring(specs.encode('utf-8')) + if context.get('inherit_branding'): + self.inherit_branding(specs_tree, view_id) + source = self.apply_inheritance_specs(cr, uid, source, specs_tree, view_id, context=context) source = self.apply_view_inheritance(cr, uid, source, view_id, context=context) return source @@ -401,7 +402,14 @@ class view(osv.osv): # read the view arch [view] = self.read(cr, uid, [root_id], fields=fields, context=context) - arch_tree = etree.fromstring( view['arch'].encode('utf-8') if isinstance(view['arch'], unicode) else view['arch']) + arch_tree = etree.fromstring(view['arch'].encode('utf-8')) + + if context.get('inherit_branding'): + arch_tree.attrib.update({ + 'data-oe-model': 'ir.ui.view', + 'data-oe-field': 'arch', + 'data-oe-view-id': str(root_id), + }) # and apply inheritance arch = self.apply_view_inheritance(cr, uid, arch_tree, root_id, context=context) @@ -715,8 +723,9 @@ class view(osv.osv): return arch def read_template(self, cr, uid, id_, context=None): - from pprint import pprint as pp - pp(id_) + import pprint + pprint.pprint(id_) + pprint.pprint(context) try: id_ = int(id_) except ValueError: @@ -741,9 +750,8 @@ class view(osv.osv): except Exception: raise ValueError('Invalid id: %r' % (id_,)) - pp(id_) - r = self.read_combined(cr, uid, id_, fields=['arch'], view_type=None, model=None, context=context) - pp(r) + r = self.read_combined(cr, uid, id_, fields=['arch'], context=context) + pprint.pprint(r) return r['arch'] def render(self, cr, uid, id_or_xml_id, values, context=None): From 096344369143b3fe0d9f042fe791437cf49d9b9f Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Sun, 30 Jun 2013 03:09:41 +0200 Subject: [PATCH 051/421] correct distribution of branded tags bzr revid: al@openerp.com-20130630010941-t922sz1smwi6jnni --- openerp/addons/base/ir/ir_ui_view.py | 83 +++++++++++----------------- 1 file changed, 33 insertions(+), 50 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index da07db8fee0..5fc69e15f95 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -19,13 +19,14 @@ # ############################################################################## import copy -from functools import partial import itertools import logging import os import sys +import re import time +from functools import partial from lxml import etree from openerp import tools @@ -288,9 +289,9 @@ class view(osv.osv): else: node.attrib.update({ 'data-oe-model': 'ir.ui.view', + 'data-oe-id': str(view_id), 'data-oe-field': 'arch', - 'data-oe-view-id': str(view_id), - 'data-oe-xpath': xpath + 'data-oe-xpath': xpath + node.tag + '/' }) return specs_tree @@ -407,8 +408,8 @@ class view(osv.osv): if context.get('inherit_branding'): arch_tree.attrib.update({ 'data-oe-model': 'ir.ui.view', + 'data-oe-id': str(root_id), 'data-oe-field': 'arch', - 'data-oe-view-id': str(root_id), }) # and apply inheritance @@ -416,7 +417,7 @@ class view(osv.osv): return dict(view, arch=etree.tostring(arch, encoding='utf-8')) - # post processing + # postprocessing: groups, modifiers, ... def postprocess(self, cr, user, model, node, view_id, in_tree_view, model_fields, context=None): """Return the description of the fields in the node. @@ -682,50 +683,8 @@ class view(osv.osv): # view used as templates - def clean_anotations(arch, parent_info=None): - for child in arch: - if child.tag == 't' or child.tag == 'field': - # can not anote t and field while cleaning - continue - - child_text = "".join([etree.tostring(x) for x in child]) - if child.attrib.get('data-edit-model'): - if child_text.find('data-edit-model') != -1 or child_text.find('{1}'.format(name, arch) + return arch engine = qweb.QWebXml(loader) return engine.render(id_or_xml_id, values) From 54a41790a0cf8b200b1eb9954620bcdd249ca176 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Sun, 30 Jun 2013 03:58:00 +0200 Subject: [PATCH 052/421] distribute branding xpath count bzr revid: al@openerp.com-20130630015800-gln7j46ume1h536k --- openerp/addons/base/ir/ir_ui_view.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 5fc69e15f95..bf80a489f23 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -712,13 +712,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): + def distribute_branding(self, e, branding=None, xpath=None, count=None): branding_copy = ['data-oe-model','data-oe-id','data-oe-field','data-oe-xpath'] branding_dist = {} if e.tag == 't' or e.tag == 'field': # can not anotate t and field return True - xpath = (xpath or '') + '/' + e.tag + xpath = "%s/%s[%s]" % (xpath or '', e.tag, (count and count.get(e.tag)) or 0) if branding and not e.attrib.get('data-oe-model'): e.attrib.update(branding) e.attrib['data-oe-xpath'] = xpath @@ -730,8 +730,10 @@ class view(osv.osv): if e.attrib.get(i): branding_dist[i] = e.attrib.get(i) e.attrib.pop(i) + count = {} for child in e: - self.distribute_branding(child, branding_dist, xpath) + count[child.tag] = count.get(child.tag,-1) + 1 + self.distribute_branding(child, branding_dist, xpath, count) def render(self, cr, uid, id_or_xml_id, values, context=None): def loader(name): From 05c9b607f809e76bfdf54417ca1ea6810b81c87e Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Sun, 30 Jun 2013 23:35:15 +0200 Subject: [PATCH 053/421] fix rng validation, fields_view_get, qweb t-esc t-raw default bzr revid: al@openerp.com-20130630213515-go5qr2dmo0xlazvr --- openerp/addons/base/ir/ir_ui_view.py | 41 ++++++++++----------- openerp/osv/orm.py | 16 ++++----- openerp/tools/qweb.py | 53 ++++++++++++++++++---------- 3 files changed, 64 insertions(+), 46 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index bf80a489f23..0fa42f0c2b0 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -139,21 +139,21 @@ class view(osv.osv): except Exception, e: _logger.exception(e) return False - - # RNG-based validation is not possible anymore with 7.0 forms - # TODO 7.0: provide alternative assertion-based validation of view_arch_utf8 - view_docs = [etree.fromstring(view_arch_utf8)] - if view_docs[0].tag == 'data': - # A element is a wrapper for multiple root nodes - view_docs = view_docs[0] - validator = self._relaxng() - for view_arch in view_docs: - if (view_arch.get('version') < '7.0') and validator and not validator.validate(view_arch): - for error in validator.error_log: - _logger.error(tools.ustr(error)) - return False - if not valid_view(view_arch): - return False + if view.type != 'qweb': + # RNG-based validation is not possible anymore with 7.0 forms + # TODO 7.0: provide alternative assertion-based validation of view_arch_utf8 + view_docs = [etree.fromstring(view_arch_utf8)] + if view_docs[0].tag == 'data': + # A element is a wrapper for multiple root nodes + view_docs = view_docs[0] + validator = self._relaxng() + for view_arch in view_docs: + if (view_arch.get('version') < '7.0') and validator and not validator.validate(view_arch): + for error in validator.error_log: + _logger.error(tools.ustr(error)) + return False + if not valid_view(view_arch): + return False return True _constraints = [ @@ -200,14 +200,15 @@ class view(osv.osv): :return: id of the default view of False if none found :rtype: int """ - ids = self.search(cr, uid, [ + domain = [ ['model', '=', model], ['type', '=', view_type], ['inherit_id', '=', False], - ], limit=1, order='priority', context=context) + ] + ids = self.search(cr, uid, domain, limit=1, order='priority', context=context) if not ids: return False - return ids[0] + return ids[0] # inheritance @@ -739,11 +740,11 @@ class view(osv.osv): def loader(name): arch = self.read_template(cr, uid, name, context=context) - arch_tree = etree.fromstring(arch.encode('utf-8')) + arch_tree = etree.fromstring(arch) self.distribute_branding(arch_tree) arch = etree.tostring(arch_tree, encoding='utf-8') - arch = u'{1}'.format(name, arch) + arch = '%s' % (name.encode('utf-8'), arch) return arch engine = qweb.QWebXml(loader) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 51d49950aa5..6073104ed4e 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -1820,24 +1820,24 @@ class BaseModel(object): result['type'] = root_view['type'] result['view_id'] = root_view['id'] result['field_parent'] = root_view['field_parent'] + # Apply post processing, groups and modifiers etc... + ctx = context + if root_view.get('model') != self._name: + ctx = dict(context, base_model_name=root_view.get('model')) + xarch, xfields = View.postprocess_and_fields( cr, uid, self._name, etree.fromstring(result['arch']), result['view_id'], context=ctx) + result['arch'] = xarch + result['fields'] = xfields else: # fallback on default views methods if no ir.ui.view could be found try: get_func = getattr(self, '_get_default_%s_view' % view_type) - arch_etree = get_func(self.cr, self.uid, self.context) + arch_etree = get_func(cr, uid, context) result['arch'] = etree.tostring(arch_etree, encoding='utf-8') result['type'] = view_type result['name'] = 'default' except AttributeError: raise except_orm(_('Invalid Architecture!'), _("No default view of type '%s' could be found !") % view_type) - # Apply post processing, groups and modifiers etc... - ctx = context - if root_view.get('model') != self._name: - ctx = dict(context, base_model_name=root_view.get('model')) - xarch, xfields = View.postprocess_and_fields( cr, uid, self._name, etree.fromstring(result['arch']), result['view_id'], context=ctx) - result['arch'] = xarch - result['fields'] = xfields # Add related action information if aksed if toolbar: diff --git a/openerp/tools/qweb.py b/openerp/tools/qweb.py index a1ee2feae2e..fa35476a5fe 100644 --- a/openerp/tools/qweb.py +++ b/openerp/tools/qweb.py @@ -34,7 +34,7 @@ class QWebEval(object): def eval_str(self, expr): if expr == "0": - return self.data[0] + return self.data.get(0, '') if isinstance(self[expr], unicode): return self[expr].encode("utf8") return str(self[expr]) @@ -122,7 +122,9 @@ class QWebXml(object): def eval_bool(self, expr, v): return QWebEval(v).eval_bool(expr) - def render(self, tname, v={}, out=None): + def render(self, tname, v=None, out=None): + if v is None: + v = {} return self.render_node(self.get_template(tname), v) def render_node(self, e, v): @@ -130,7 +132,6 @@ class QWebXml(object): if e.nodeType == self.node.TEXT_NODE or e.nodeType == self.node.CDATA_SECTION_NODE: r = e.data.encode("utf8") elif e.nodeType == self.node.ELEMENT_NODE: - pre = "" g_att = "" t_render = None t_att = {} @@ -155,15 +156,24 @@ class QWebXml(object): if t_render in self._render_tag: r = self._render_tag[t_render](self, e, t_att, g_att, v) else: - r = self.render_element(e, g_att, v, pre, t_att.get("trim", 0)) + r = self.render_element(e, t_att, g_att, v) return r - def render_element(self, e, g_att, v, pre="", trim=0): - g_inner = [] - for n in e.childNodes: - g_inner.append(self.render_node(n, v)) + def render_element(self, e, t_att, g_att, v, inner=None): + # e: element + # t_att: t-* attributes + # g_att: generated attributes + # v: values + # inner: optional innerXml + if inner: + g_inner = inner + else: + g_inner = [] + for n in e.childNodes: + g_inner.append(self.render_node(n, v)) name = str(e.nodeName) inner = "".join(g_inner) + trim = t_att.get("trim", 0) if trim == 0: pass elif trim == 'left': @@ -174,8 +184,9 @@ class QWebXml(object): inner = inner.strip() if name == "t": return inner - elif len(inner): - return "<%s%s>%s%s" % (name, g_att, pre, inner, name) + elif len(inner) or name in ['script','i']: + # script should be rendered as + return "<%s%s>%s" % (name, g_att, inner, name) else: return "<%s%s/>" % (name, g_att) @@ -191,16 +202,20 @@ class QWebXml(object): # Tags def render_tag_raw(self, e, t_att, g_att, v): - return self.eval_str(t_att["raw"], v) + inner = self.eval_str(t_att["raw"], v) + return self.render_element(e, t_att, g_att, v, inner) def render_tag_rawf(self, e, t_att, g_att, v): - return self.eval_format(t_att["rawf"], v) + inner = self.eval_format(t_att["rawf"], v) + return self.render_element(e, t_att, g_att, v, inner) def render_tag_esc(self, e, t_att, g_att, v): - return cgi.escape(self.eval_str(t_att["esc"], v)) + inner = cgi.escape(self.eval_str(t_att["esc"], v)) + return self.render_element(e, t_att, g_att, v, inner) def render_tag_escf(self, e, t_att, g_att, v): - return cgi.escape(self.eval_format(t_att["escf"], v)) + inner = cgi.escape(self.eval_format(t_att["escf"], v)) + return self.render_element(e, t_att, g_att, v, inner) def render_tag_foreach(self, e, t_att, g_att, v): expr = t_att["foreach"] @@ -234,7 +249,7 @@ class QWebXml(object): d.update(i) else: d[var] = i - ru.append(self.render_element(e, g_att, d)) + ru.append(self.render_element(e, t_att, g_att, d)) index += 1 return "".join(ru) else: @@ -242,7 +257,7 @@ class QWebXml(object): def render_tag_if(self, e, t_att, g_att, v): if self.eval_bool(t_att["if"], v): - return self.render_element(e, g_att, v) + return self.render_element(e, t_att, g_att, v) else: return "" @@ -252,12 +267,14 @@ class QWebXml(object): d = v else: d = v.copy() - d[0] = self.render_element(e, g_att, d) + d[0] = self.render_element(e, t_att, g_att, d) return self.render(t_att["call"], d) def render_tag_set(self, e, t_att, g_att, v): if "eval" in t_att: v[t_att["set"]] = self.eval_object(t_att["eval"], v) else: - v[t_att["set"]] = self.render_element(e, g_att, v) + v[t_att["set"]] = self.render_element(e, t_att, g_att, v) return "" + +# leave this, al. From df01c096ecc960846b84a7d51dc0c838612643e0 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Mon, 1 Jul 2013 02:24:24 +0200 Subject: [PATCH 054/421] branding temp fix for comment in xml main company demo data bzr revid: al@openerp.com-20130701002424-ojr6yzlr9m0xv6vi --- openerp/addons/base/base_demo.xml | 13 +++++++++++++ openerp/addons/base/ir/ir_ui_view.py | 26 +++++++++++++++----------- openerp/tools/qweb.py | 5 ++--- 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/openerp/addons/base/base_demo.xml b/openerp/addons/base/base_demo.xml index 757534cae21..5fb6e94eecc 100644 --- a/openerp/addons/base/base_demo.xml +++ b/openerp/addons/base/base_demo.xml @@ -1,6 +1,7 @@ + Demo User @@ -9,6 +10,13 @@ + Dunder Mufflin + 1725 Slough Ave. + Scranton + 18540 + +1 555 123 8069 + info@example.com + www.example.com iVBORw0KGgoAAAANSUhEUgAAALQAAAAuCAYAAACBMDMXAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A /wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sDCAo7GWN31l0AAA1fSURBVHja 7Zx5dFXFHcc/eQk7KBiUTVGRRezA8ahYamgRFbWAcmyPe+uGSrW1FrFqF9u61bZWm1Kx1lgVpHVp @@ -75,6 +83,11 @@ BZrmED0AAAAASUVORK5CYII= + + Dunder Mufflin + + + demo diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 0fa42f0c2b0..cba61952b86 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -157,7 +157,7 @@ class view(osv.osv): return True _constraints = [ - (_check_xml, 'Invalid XML for View Architecture!', ['arch']) + #(_check_xml, 'Invalid XML for View Architecture!', ['arch']) ] def _auto_init(self, cr, context=None): @@ -242,7 +242,7 @@ class view(osv.osv): for view in self.browse(cr, 1, view_ids, context) if not (view.groups_id and user_groups.isdisjoint(view.groups_id))] - def raise_view_error(self, cr, uid, view_id, message, context=None): + def raise_view_error(self, cr, uid, message, view_id, context=None): view = self.browse(cr, uid, [view_id], context)[0] message = "Inherit error: %s view_id: %s, xml_id: %s, model: %s, parent_view: %s" % (message, view_id, view.xml_id, view.model, view.inherit_id) raise AttributeError(message) @@ -285,15 +285,19 @@ class view(osv.osv): def inherit_branding(self, specs_tree, view_id, xpath="/"): for node in specs_tree: - if node.tag == 'data' or node.tag == 'xpath': - node = self.inherit_branding(node, view_id, xpath + node.tag + '/') - else: - node.attrib.update({ - 'data-oe-model': 'ir.ui.view', - 'data-oe-id': str(view_id), - 'data-oe-field': 'arch', - 'data-oe-xpath': xpath + node.tag + '/' - }) + try: + if node.tag == 'data' or node.tag == 'xpath': + node = self.inherit_branding(node, view_id, xpath + node.tag + '/') + else: + node.attrib.update({ + 'data-oe-model': 'ir.ui.view', + 'data-oe-id': str(view_id), + 'data-oe-field': 'arch', + 'data-oe-xpath': xpath + node.tag + '/' + }) + except Exception,e: + print "inherit branding error",e,xpath,node.tag + return specs_tree def apply_inheritance_specs(self, cr, uid, source, specs_tree, inherit_id, context=None): diff --git a/openerp/tools/qweb.py b/openerp/tools/qweb.py index fa35476a5fe..8bd1371e8c5 100644 --- a/openerp/tools/qweb.py +++ b/openerp/tools/qweb.py @@ -2,7 +2,7 @@ import cgi import logging import types -from openerp.tools.safe_eval import safe_eval as eval +#from openerp.tools.safe_eval import safe_eval as eval import xml # FIXME use lxml import xml.dom.minidom @@ -16,7 +16,7 @@ class QWebEval(object): def __getitem__(self, expr): if expr in self.data: return self.data[expr] - r = None + r = '' try: r = eval(expr, self.data) except NameError: @@ -262,7 +262,6 @@ class QWebXml(object): return "" def render_tag_call(self, e, t_att, g_att, v): - # TODO t-prefix if "import" in t_att: d = v else: From 9ad26be6806938cf6cc4f4c0f5e027ad14fc2a24 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Mon, 1 Jul 2013 12:51:07 +0200 Subject: [PATCH 055/421] [FIX] ir_ui_view: accomodate previously changed apply_inheritance_specs() signature. bzr revid: vmt@openerp.com-20130701105107-thsi2o2pmeifo338 --- openerp/addons/base/tests/test_views.py | 99 ++++++++++++------------- 1 file changed, 46 insertions(+), 53 deletions(-) diff --git a/openerp/addons/base/tests/test_views.py b/openerp/addons/base/tests/test_views.py index 8a9a0dcb467..038469403f4 100644 --- a/openerp/addons/base/tests/test_views.py +++ b/openerp/addons/base/tests/test_views.py @@ -254,39 +254,37 @@ class TestApplyInheritanceSpecs(common.TransactionCase): string="Title") def test_replace(self): - spec = ET.tostring( - Field( + spec = Field( Field(name="replacement"), - name="target", position="replace")) + name="target", position="replace") - self.View.apply_inheritance_specs(self.cr, self.uid, 'test', - None, self.base_arch, - None, spec) + self.View.apply_inheritance_specs(self.cr, self.uid, + self.base_arch, + spec, None) self.assertEqual( ET.tostring(self.base_arch), ET.tostring(E.form(Field(name="replacement"), string="Title"))) def test_delete(self): - spec = ET.tostring(Field(name="target", position="replace")) + spec = Field(name="target", position="replace") - self.View.apply_inheritance_specs(self.cr, self.uid, 'test', - None, self.base_arch, - None, spec) + self.View.apply_inheritance_specs(self.cr, self.uid, + self.base_arch, + spec, None) self.assertEqual( ET.tostring(self.base_arch), ET.tostring(E.form(string="Title"))) def test_insert_after(self): - spec = ET.tostring( - Field( + spec = Field( Field(name="inserted"), - name="target", position="after")) + name="target", position="after") - self.View.apply_inheritance_specs(self.cr, self.uid, 'test', - None, self.base_arch, - None, spec) + self.View.apply_inheritance_specs(self.cr, self.uid, + self.base_arch, + spec, None) self.assertEqual( ET.tostring(self.base_arch), @@ -297,14 +295,13 @@ class TestApplyInheritanceSpecs(common.TransactionCase): ))) def test_insert_before(self): - spec = ET.tostring( - Field( + spec = Field( Field(name="inserted"), - name="target", position="before")) + name="target", position="before") - self.View.apply_inheritance_specs(self.cr, self.uid, 'test', - None, self.base_arch, - None, spec) + self.View.apply_inheritance_specs(self.cr, self.uid, + self.base_arch, + spec, None) self.assertEqual( ET.tostring(self.base_arch), @@ -314,17 +311,15 @@ class TestApplyInheritanceSpecs(common.TransactionCase): string="Title"))) def test_insert_inside(self): - default = ET.tostring( - Field(Field(name="inserted"), name="target")) - spec = ET.tostring( - Field(Field(name="inserted 2"), name="target", position='inside')) + default = Field(Field(name="inserted"), name="target") + spec = Field(Field(name="inserted 2"), name="target", position='inside') - self.View.apply_inheritance_specs(self.cr, self.uid, 'test', - None, self.base_arch, - None, default) - self.View.apply_inheritance_specs(self.cr, self.uid, 'test', - None, self.base_arch, - None, spec) + self.View.apply_inheritance_specs(self.cr, self.uid, + self.base_arch, + default, None) + self.View.apply_inheritance_specs(self.cr, self.uid, + self.base_arch, + spec, None) self.assertEqual( ET.tostring(self.base_arch), @@ -336,17 +331,16 @@ class TestApplyInheritanceSpecs(common.TransactionCase): string="Title"))) def test_unpack_data(self): - spec = ET.tostring( - E.data( + spec = E.data( Field(Field(name="inserted 0"), name="target"), Field(Field(name="inserted 1"), name="target"), Field(Field(name="inserted 2"), name="target"), Field(Field(name="inserted 3"), name="target"), - )) + ) - self.View.apply_inheritance_specs(self.cr, self.uid, 'test', - None, self.base_arch, - None, spec) + self.View.apply_inheritance_specs(self.cr, self.uid, + self.base_arch, + spec, None) self.assertEqual( ET.tostring(self.base_arch), @@ -360,35 +354,34 @@ class TestApplyInheritanceSpecs(common.TransactionCase): string="Title"))) def test_invalid_position(self): - spec = ET.tostring( - Field( + spec = Field( Field(name="whoops"), - name="target", position="serious_series")) + name="target", position="serious_series") with self.assertRaises(AttributeError): - self.View.apply_inheritance_specs(self.cr, self.uid, 'test', - None, self.base_arch, - None, spec) + self.View.apply_inheritance_specs(self.cr, self.uid, + self.base_arch, + spec, None) def test_incorrect_version(self): # Version ignored on //field elements, so use something else arch = E.form(E.element(foo="42")) - spec = ET.tostring(E.element( + spec = E.element( Field(name="placeholder"), - foo="42", version="7.0")) + foo="42", version="7.0") with self.assertRaises(AttributeError): - self.View.apply_inheritance_specs(self.cr, self.uid, 'test', - None, arch, - None, spec) + self.View.apply_inheritance_specs(self.cr, self.uid, + arch, + spec, None) def test_target_not_found(self): - spec = ET.tostring(Field(name="targut")) + spec = Field(name="targut") with self.assertRaises(AttributeError): - self.View.apply_inheritance_specs(self.cr, self.uid, 'test', - None, self.base_arch, - None, spec) + self.View.apply_inheritance_specs(self.cr, self.uid, + self.base_arch, + spec, None) class TestApplyInheritedArchs(common.TransactionCase): """ Applies a sequence of modificator archs to a base view From bd4f503b9b676e4b2fe01ce9843716804afa77b7 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Mon, 1 Jul 2013 13:05:21 +0200 Subject: [PATCH 056/421] [FIX] ir_ui_view: __view_look_dom_arch() was renamed into postprocess_and_fields(). bzr revid: vmt@openerp.com-20130701110521-muec7xyt7op7bhql --- openerp/addons/base/tests/test_views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openerp/addons/base/tests/test_views.py b/openerp/addons/base/tests/test_views.py index 038469403f4..19086e5328a 100644 --- a/openerp/addons/base/tests/test_views.py +++ b/openerp/addons/base/tests/test_views.py @@ -431,7 +431,7 @@ class TestNoModel(common.TransactionCase): """ View = self.registry('ir.ui.view') - sarch, fields = View._view__view_look_dom_arch( + sarch, fields = View.postprocess_and_fields( self.cr, self.uid, None, self.arch, None) self.assertEqual(sarch, ET.tostring(self.arch, encoding='utf-8')) @@ -450,7 +450,7 @@ class TestNoModel(common.TransactionCase): 'src': 'Copyright copyrighter', 'value': u"Copyrighter, tous droits réservés", }) - sarch, fields = View._view__view_look_dom_arch( + sarch, fields = View.postprocess_and_fields( self.cr, self.uid, None, self.arch, None, {'lang': 'fr_FR'}) self.assertEqual( sarch, From b4e6c6e22df3e3163dde7d9f2b6016e1f0c0cb9a Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Mon, 1 Jul 2013 13:23:09 +0200 Subject: [PATCH 057/421] add count for inherit_branding bzr revid: mat@openerp.com-20130701112309-hf3rarslfc2hhvr4 --- openerp/addons/base/ir/ir_ui_view.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index cba61952b86..ddbde5be262 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -283,17 +283,21 @@ class view(osv.osv): return node return None - def inherit_branding(self, specs_tree, view_id, xpath="/"): + def inherit_branding(self, specs_tree, view_id, base_xpath=None, count=None): + if not count: + count = {} for node in specs_tree: try: + count[node.tag] = count.get(node.tag, -1) + 1 + xpath = "%s/%s[%s]" % (base_xpath or '', node.tag, (count and count.get(node.tag)) or 0) if node.tag == 'data' or node.tag == 'xpath': - node = self.inherit_branding(node, view_id, xpath + node.tag + '/') + 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 + node.tag + '/' + 'data-oe-xpath': xpath }) except Exception,e: print "inherit branding error",e,xpath,node.tag @@ -406,7 +410,7 @@ class view(osv.osv): if fields: fields = list(set(fields) | set(['arch', 'model'])) - # read the view arch + # read the view arch [view] = self.read(cr, uid, [root_id], fields=fields, context=context) arch_tree = etree.fromstring(view['arch'].encode('utf-8')) From aba5593f0158e91fd608b18de249dd0c4efabf8c Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Mon, 1 Jul 2013 13:27:26 +0200 Subject: [PATCH 058/421] [FIX] ir_ui_view: get_inheriting_views_arch() does no longer take a model. bzr revid: vmt@openerp.com-20130701112726-600dsa9udm0rt4uj --- openerp/addons/base/tests/test_views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openerp/addons/base/tests/test_views.py b/openerp/addons/base/tests/test_views.py index 19086e5328a..4fc64decda5 100644 --- a/openerp/addons/base/tests/test_views.py +++ b/openerp/addons/base/tests/test_views.py @@ -173,17 +173,17 @@ class TestViewInheritance(common.TransactionCase): def test_get_inheriting_views_arch(self): self.assertEqual(self.View.get_inheriting_views_arch( - self.cr, self.uid, self.ids['A'], self.model), [ + self.cr, self.uid, self.ids['A']), [ (self.arch_for('A1', parent=True), self.ids['A1']), (self.arch_for('A2', parent=True), self.ids['A2']), ]) self.assertEqual(self.View.get_inheriting_views_arch( - self.cr, self.uid, self.ids['A21'], self.model), + self.cr, self.uid, self.ids['A21']), []) self.assertEqual(self.View.get_inheriting_views_arch( - self.cr, self.uid, self.ids['A11'], self.model), + self.cr, self.uid, self.ids['A11']), [(self.arch_for('A111', parent=True), self.ids['A111'])]) def test_iter(self): From 3b4ecbf0332f50cd75690ff4d691e0f2ccd2e971 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Mon, 1 Jul 2013 13:29:30 +0200 Subject: [PATCH 059/421] [FIX] ir_ui_view: it seems the iter() method is no longer there, removed the test.. bzr revid: vmt@openerp.com-20130701112930-k8rhlvgbaj1y1zl5 --- openerp/addons/base/tests/test_views.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/openerp/addons/base/tests/test_views.py b/openerp/addons/base/tests/test_views.py index 4fc64decda5..c3e09c830a0 100644 --- a/openerp/addons/base/tests/test_views.py +++ b/openerp/addons/base/tests/test_views.py @@ -186,19 +186,6 @@ class TestViewInheritance(common.TransactionCase): self.cr, self.uid, self.ids['A11']), [(self.arch_for('A111', parent=True), self.ids['A111'])]) - def test_iter(self): - descendents = list(self.View.iter(self.cr, self.uid, self.ids['A1'], self.model)) - self.assertEqual(descendents, [ - (self.ids[name], self.arch_for(name, parent=True)) - for name in ['A1', 'A11', 'A111', 'A12'] - ]) - descendents = list(self.View.iter( - self.cr, self.uid, self.ids['A2'], self.model, exclude_base=True)) - self.assertEqual(descendents, [ - (self.ids[name], self.arch_for(name, parent=True)) - for name in ['A21', 'A22', 'A221'] - ]) - def test_root_ancestor(self): A_id = self.ids['A'] root_id = self.View.root_ancestor(self.cr, self.uid, view_id=A_id) From f42bcfa0f8e77e6289706306daf36aeb323d5eff Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Mon, 1 Jul 2013 13:41:29 +0200 Subject: [PATCH 060/421] [FIX] ir_ui_view: does no longer provide root_ancestor(), so removed corresponding test. bzr revid: vmt@openerp.com-20130701114129-03jtrx0g6abk6xev --- openerp/addons/base/tests/test_views.py | 30 ++++--------------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/openerp/addons/base/tests/test_views.py b/openerp/addons/base/tests/test_views.py index c3e09c830a0..65a5079121b 100644 --- a/openerp/addons/base/tests/test_views.py +++ b/openerp/addons/base/tests/test_views.py @@ -186,28 +186,6 @@ class TestViewInheritance(common.TransactionCase): self.cr, self.uid, self.ids['A11']), [(self.arch_for('A111', parent=True), self.ids['A111'])]) - def test_root_ancestor(self): - A_id = self.ids['A'] - root_id = self.View.root_ancestor(self.cr, self.uid, view_id=A_id) - self.assertEqual(root_id, A_id, - "when given a root view, operation should be id") - - root_id = self.View.root_ancestor( - self.cr, self.uid, view_id=self.ids['A11']) - self.assertEqual(root_id, A_id) - - root_id = self.View.root_ancestor( - self.cr, self.uid, view_id=self.ids['A221']) - self.assertEqual(root_id, A_id) - - root_id = self.View.root_ancestor( - self.cr, self.uid, view_id=self.ids['B1']) - self.assertEqual(root_id, self.ids['B']) - - def test_no_root_ancestor(self): - with self.assertRaises(self.View.NoViewError): - self.View.root_ancestor(self.cr, self.uid, view_id=12345678) - def test_default_view(self): default = self.View.default_view( self.cr, self.uid, model=self.model, view_type='form') @@ -218,13 +196,13 @@ class TestViewInheritance(common.TransactionCase): self.assertEqual(default_tree, self.ids['C']) def test_no_default_view(self): - with self.assertRaises(self.View.NoDefaultError): + self.assertFalse( self.View.default_view( - self.cr, self.uid, model='does.not.exist', view_type='form') + self.cr, self.uid, model='does.not.exist', view_type='form')) - with self.assertRaises(self.View.NoDefaultError): + self.assertFalse( self.View.default_view( - self.cr, self.uid, model=self.model, view_type='graph') + self.cr, self.uid, model=self.model, view_type='graph')) class TestApplyInheritanceSpecs(common.TransactionCase): """ Applies a sequence of inheritance specification nodes to a base From 3b0935325eba8dce51a343bde83cfd2953d10b00 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Mon, 1 Jul 2013 14:26:26 +0200 Subject: [PATCH 061/421] fields_view_get apply post processing even for default views bzr revid: al@openerp.com-20130701122626-9g2uo6ox3p74e55q --- openerp/osv/orm.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 6073104ed4e..d851a072a44 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -1812,6 +1812,8 @@ class BaseModel(object): # otherwise try to find the lowest priority matching ir.ui.view view_id = View.default_view(cr, uid, self._name, view_type, context=context) + # context for postproscessing might be overriden + ctx = context if view_id: # read the view with inherited views applied root_view = View.read_combined(cr, uid, view_id, fields=['id', 'name', 'field_parent', 'type', 'model', 'arch'], context=context) @@ -1820,13 +1822,9 @@ class BaseModel(object): result['type'] = root_view['type'] result['view_id'] = root_view['id'] result['field_parent'] = root_view['field_parent'] - # Apply post processing, groups and modifiers etc... - ctx = context + # override context fro postprocessing if root_view.get('model') != self._name: ctx = dict(context, base_model_name=root_view.get('model')) - xarch, xfields = View.postprocess_and_fields( cr, uid, self._name, etree.fromstring(result['arch']), result['view_id'], context=ctx) - result['arch'] = xarch - result['fields'] = xfields else: # fallback on default views methods if no ir.ui.view could be found try: @@ -1838,6 +1836,10 @@ class BaseModel(object): except AttributeError: raise except_orm(_('Invalid Architecture!'), _("No default view of type '%s' could be found !") % view_type) + # Apply post processing, groups and modifiers etc... + xarch, xfields = View.postprocess_and_fields( cr, uid, self._name, etree.fromstring(result['arch']), result['view_id'], context=ctx) + result['arch'] = xarch + result['fields'] = xfields # Add related action information if aksed if toolbar: From b0a348d24e0ffcaf49ca55cfd7847c53ee228738 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Mon, 1 Jul 2013 15:02:58 +0200 Subject: [PATCH 062/421] fields_view_get view_id bzr revid: al@openerp.com-20130701130258-52nvw8aj6s8bcjaf --- openerp/osv/orm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index d851a072a44..1fcb8ec85d1 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -1837,7 +1837,7 @@ class BaseModel(object): raise except_orm(_('Invalid Architecture!'), _("No default view of type '%s' could be found !") % view_type) # Apply post processing, groups and modifiers etc... - xarch, xfields = View.postprocess_and_fields( cr, uid, self._name, etree.fromstring(result['arch']), result['view_id'], context=ctx) + xarch, xfields = View.postprocess_and_fields( cr, uid, self._name, etree.fromstring(result['arch']), view_id, context=ctx) result['arch'] = xarch result['fields'] = xfields From a7b0a0379c02f2f6023ebd74a5c0e4e72826a65f Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Mon, 1 Jul 2013 15:53:13 +0200 Subject: [PATCH 063/421] add save method for javascript call bzr revid: mat@openerp.com-20130701135313-absej6vq16aztrc8 --- openerp/addons/base/ir/ir_ui_view.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index ddbde5be262..d3ccdb1ec5c 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -189,6 +189,23 @@ 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 + + :param str model: + :param int res_id: + :param str xpath: valid xpath to the tag to replace + """ + model_obj = self.pool.get(model) + origin = model_obj.read(cr, uid, [res_id], [field], context=context) + if xpath: + origin_tree = etree.fromstring(origin.encode('utf-8')) + zone = origin_tree.xpath(xpath)[0] + zone.getparent().replace(zone, etree.fromstring(value)) + value = etree.tostring(origin_tree, encoding='utf-8') + + model_obj.write(cr, uid, res_id, {field: value}, context=context) + # default view selection def default_view(self, cr, uid, model, view_type, context=None): @@ -288,8 +305,8 @@ class view(osv.osv): count = {} for node in specs_tree: try: - count[node.tag] = count.get(node.tag, -1) + 1 - xpath = "%s/%s[%s]" % (base_xpath or '', node.tag, (count and count.get(node.tag)) or 0) + 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: @@ -727,7 +744,7 @@ class view(osv.osv): if e.tag == 't' or e.tag == 'field': # can not anotate t and field return True - xpath = "%s/%s[%s]" % (xpath or '', e.tag, (count and count.get(e.tag)) or 0) + xpath = "%s/%s[%s]" % (xpath or '', e.tag, (count and count.get(e.tag)) or 1) if branding and not e.attrib.get('data-oe-model'): e.attrib.update(branding) e.attrib['data-oe-xpath'] = xpath @@ -741,7 +758,7 @@ class view(osv.osv): e.attrib.pop(i) count = {} for child in e: - count[child.tag] = count.get(child.tag,-1) + 1 + count[child.tag] = count.get(child.tag, 0) + 1 self.distribute_branding(child, branding_dist, xpath, count) def render(self, cr, uid, id_or_xml_id, values, context=None): From 7786d96095037461ce53d9bcaea6deadb8202b12 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Mon, 1 Jul 2013 16:53:34 +0200 Subject: [PATCH 064/421] [FIX] ir_ui_view: restore the inheritance mechanism for model-specific inheritance: A view for a given model can inherit from a root view specified for another model. When applying the modifying views to the root view, only the views with the same model as the child view must be considered. This patch restore that behavior. bzr revid: vmt@openerp.com-20130701145334-ojp1plqjveym7cj7 --- openerp/addons/base/ir/ir_ui_view.py | 17 ++++++++++------- openerp/addons/base/tests/test_views.py | 6 +++--- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index d3ccdb1ec5c..fca4e089ae3 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -229,7 +229,7 @@ class view(osv.osv): # inheritance - def get_inheriting_views_arch(self, cr, uid, view_id, context=None): + def get_inheriting_views_arch(self, cr, uid, view_id, model, context=None): """Retrieves the architecture of views that inherit from the given view, from the sets of views that should currently be used in the system. During the module upgrade phase it may happen that a view is present in the database but the fields it relies on are not @@ -238,13 +238,13 @@ class view(osv.osv): after the module initialization phase is completely finished. :param int view_id: id of the view whose inheriting views should be retrieved - :param str model: model identifier of the view's related model (for double-checking) + :param str model: model identifier of the inheriting views. :rtype: list of tuples :return: [(view_arch,view_id), ...] """ user_groups = frozenset(self.pool.get('res.users').browse(cr, 1, uid, context).groups_id) - conditions = [['inherit_id', '=', view_id]] + conditions = [['inherit_id', '=', view_id], ['model', '=', model]] if self.pool._init: # Module init currently in progress, only consider views from # modules whose code is already loaded @@ -388,21 +388,24 @@ class view(osv.osv): return source - def apply_view_inheritance(self, cr, uid, source, source_id, context=None): + def apply_view_inheritance(self, cr, uid, source, source_id, model, context=None): """ Apply all the (directly and indirectly) inheriting views. :param source: a parent architecture to modify (with parent modifications already applied) :param source_id: the database view_id of the parent view + :param model: the original model for which we create a view (not + necessarily the same as the source's model); only the inheriting + views with that specific model will be applied. :return: a modified source where all the modifying architecture are applied """ if context is None: context = {} - sql_inherit = self.pool.get('ir.ui.view').get_inheriting_views_arch(cr, uid, source_id) + sql_inherit = self.pool.get('ir.ui.view').get_inheriting_views_arch(cr, uid, source_id, model) for (specs, view_id) in sql_inherit: specs_tree = etree.fromstring(specs.encode('utf-8')) if context.get('inherit_branding'): self.inherit_branding(specs_tree, view_id) source = self.apply_inheritance_specs(cr, uid, source, specs_tree, view_id, context=context) - source = self.apply_view_inheritance(cr, uid, source, view_id, context=context) + source = self.apply_view_inheritance(cr, uid, source, view_id, model, context=context) return source def read_combined(self, cr, uid, view_id, fields=None, context=None): @@ -439,7 +442,7 @@ class view(osv.osv): }) # and apply inheritance - arch = self.apply_view_inheritance(cr, uid, arch_tree, root_id, context=context) + arch = self.apply_view_inheritance(cr, uid, arch_tree, root_id, v.model, context=context) return dict(view, arch=etree.tostring(arch, encoding='utf-8')) diff --git a/openerp/addons/base/tests/test_views.py b/openerp/addons/base/tests/test_views.py index 65a5079121b..6feba4df672 100644 --- a/openerp/addons/base/tests/test_views.py +++ b/openerp/addons/base/tests/test_views.py @@ -173,17 +173,17 @@ class TestViewInheritance(common.TransactionCase): def test_get_inheriting_views_arch(self): self.assertEqual(self.View.get_inheriting_views_arch( - self.cr, self.uid, self.ids['A']), [ + self.cr, self.uid, self.ids['A'], self.model), [ (self.arch_for('A1', parent=True), self.ids['A1']), (self.arch_for('A2', parent=True), self.ids['A2']), ]) self.assertEqual(self.View.get_inheriting_views_arch( - self.cr, self.uid, self.ids['A21']), + self.cr, self.uid, self.ids['A21'], self.model), []) self.assertEqual(self.View.get_inheriting_views_arch( - self.cr, self.uid, self.ids['A11']), + self.cr, self.uid, self.ids['A11'], self.model), [(self.arch_for('A111', parent=True), self.ids['A111'])]) def test_default_view(self): From e372cbbce496b20cb510d43a4a2dded685a6e1ff Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Mon, 1 Jul 2013 17:55:41 +0200 Subject: [PATCH 065/421] add t-field and t-record in rendering bzr revid: mat@openerp.com-20130701155541-20plkar9a89qosij --- openerp/addons/base/ir/ir_ui_view.py | 2 +- openerp/tools/qweb.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index fca4e089ae3..45a2e40d04f 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -754,7 +754,7 @@ class view(osv.osv): 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=)',child_text): + if re.search('(data-oe-model=|t-esc=|t-raw=|t-field=)',child_text): for i in branding_copy: if e.attrib.get(i): branding_dist[i] = e.attrib.get(i) diff --git a/openerp/tools/qweb.py b/openerp/tools/qweb.py index 8bd1371e8c5..221bf138e28 100644 --- a/openerp/tools/qweb.py +++ b/openerp/tools/qweb.py @@ -276,4 +276,20 @@ class QWebXml(object): v[t_att["set"]] = self.render_element(e, t_att, g_att, v) return "" + def render_tag_field(self, e, t_att, g_att, v): + """ eg: +1 555 555 8069""" + record = v[t_att["record"]] + + if record._model._columns.get(t_att["field"])._type == 'many2one': + inner = cgi.escape(str(getattr(record, t_att["field"]).name_get()[0][1])) + else: + inner = cgi.escape(str(getattr(record, t_att["field"]))) + + if e.tagName != 't': + # are escaped + g_att += ' %s="%s"' % ('data-oe-model', record._model._name) + g_att += ' %s="%s"' % ('data-oe-id', str(record.id)) + g_att += ' %s="%s"' % ('data-oe-field', t_att["field"]) + return self.render_element(e, t_att, g_att, v, inner) + # leave this, al. From 1e303767eedb3441c515ca71dcdaa44d19111176 Mon Sep 17 00:00:00 2001 From: Fabien Meghazi Date: Tue, 2 Jul 2013 14:43:13 +0200 Subject: [PATCH 066/421] m bzr revid: fme@openerp.com-20130702124313-cuehsln93w7mzhqx --- 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 45a2e40d04f..dd62b69f7c7 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -197,8 +197,8 @@ class view(osv.osv): :param str xpath: valid xpath to the tag to replace """ model_obj = self.pool.get(model) - origin = model_obj.read(cr, uid, [res_id], [field], context=context) 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, etree.fromstring(value)) From 641c4fe78457f5bc4175150141e2fe06789c11a8 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Tue, 2 Jul 2013 14:59:16 +0200 Subject: [PATCH 067/421] ir_ui_view: parse using lxml.html instead of etree bzr revid: mat@openerp.com-20130702125916-ahk6jkim08j2voam --- openerp/addons/base/ir/ir_ui_view.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index dd62b69f7c7..2b3841ad672 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -28,6 +28,7 @@ import time from functools import partial from lxml import etree +from lxml.html import tostring, fromstring from openerp import tools from openerp.modules import module @@ -199,10 +200,10 @@ class view(osv.osv): 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')) + origin_tree = fromstring(origin.encode('utf-8')) zone = origin_tree.xpath(xpath)[0] - zone.getparent().replace(zone, etree.fromstring(value)) - value = etree.tostring(origin_tree, encoding='utf-8') + zone.getparent().replace(zone, fromstring(value)) + value = tostring(origin_tree, encoding='utf-8') model_obj.write(cr, uid, res_id, {field: value}, context=context) From f34b894b2272db49586c8c5ebec728c221ba5808 Mon Sep 17 00:00:00 2001 From: Christophe Matthieu Date: Tue, 2 Jul 2013 14:00:18 +0200 Subject: [PATCH 068/421] [FIX] qweb.py: t-set t-value bzr revid: chm@openerp.com-20130702120018-w9llyk8indqyywmo --- 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 221bf138e28..edae665ddfc 100644 --- a/openerp/tools/qweb.py +++ b/openerp/tools/qweb.py @@ -270,8 +270,8 @@ class QWebXml(object): return self.render(t_att["call"], d) def render_tag_set(self, e, t_att, g_att, v): - if "eval" in t_att: - v[t_att["set"]] = self.eval_object(t_att["eval"], v) + if "value" in t_att: + v[t_att["set"]] = self.eval_object(t_att["value"], v) else: v[t_att["set"]] = self.render_element(e, t_att, g_att, v) return "" From 8f25c9bdae500e8823aa868a32965e4cf24ad687 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Tue, 2 Jul 2013 16:56:05 +0200 Subject: [PATCH 069/421] be xml in the db bzr revid: al@openerp.com-20130702145605-nkzjall1msp6ggsa --- openerp/addons/base/ir/ir_ui_view.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 2b3841ad672..c1e02c4aef0 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -26,9 +26,9 @@ import sys import re import time -from functools import partial +import lxml.html from lxml import etree -from lxml.html import tostring, fromstring +from functools import partial from openerp import tools from openerp.modules import module @@ -200,10 +200,11 @@ class view(osv.osv): model_obj = self.pool.get(model) if xpath: origin = model_obj.read(cr, uid, [res_id], [field], context=context)[0][field] - origin_tree = fromstring(origin.encode('utf-8')) + origin_tree = etree.fromstring(origin.encode('utf-8')) zone = origin_tree.xpath(xpath)[0] - zone.getparent().replace(zone, fromstring(value)) - value = tostring(origin_tree, encoding='utf-8') + print "zone", zone, "value", value + zone.getparent().replace(zone, lxml.html.fromstring(value)) + value = etree.tostring(origin_tree, encoding='utf-8') model_obj.write(cr, uid, res_id, {field: value}, context=context) From d346320699aba9ac8c16c7ca516d37f0f5e3300f Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Tue, 2 Jul 2013 17:44:46 +0200 Subject: [PATCH 070/421] distribute branding for fields bzr revid: al@openerp.com-20130702154446-7fpel212amdwmmjz --- openerp/addons/base/ir/ir_ui_view.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index c1e02c4aef0..49fa4defc08 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -746,11 +746,8 @@ class view(osv.osv): def distribute_branding(self, e, branding=None, xpath=None, count=None): branding_copy = ['data-oe-model','data-oe-id','data-oe-field','data-oe-xpath'] branding_dist = {} - if e.tag == 't' or e.tag == 'field': - # can not anotate t and field - return True xpath = "%s/%s[%s]" % (xpath or '', e.tag, (count and count.get(e.tag)) or 1) - if branding and not e.attrib.get('data-oe-model'): + 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'): From 2bac4d24c4c45982a064663effa51781eba38e0e Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Tue, 2 Jul 2013 18:07:50 +0200 Subject: [PATCH 071/421] distribute branding take care of call and t bzr revid: al@openerp.com-20130702160750-0yv4g6bcnh4v80rp --- 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 49fa4defc08..2cfcd33dfff 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -753,7 +753,7 @@ class view(osv.osv): 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=)',child_text): + if re.search('(data-oe-model=|t-esc=|t-raw=|t-field=|t-call=)',child_text) or e.tag == "t": for i in branding_copy: if e.attrib.get(i): branding_dist[i] = e.attrib.get(i) From 25388630ccd803d34c74679ca80d50833d2ef056 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Tue, 2 Jul 2013 18:41:57 +0200 Subject: [PATCH 072/421] [FIX] views: distribute branding only if not t-* attributes bzr revid: chs@openerp.com-20130702164157-nsfq9xpswj0r56ry --- 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 2cfcd33dfff..995f730712a 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -758,10 +758,11 @@ class view(osv.osv): if e.attrib.get(i): branding_dist[i] = e.attrib.get(i) e.attrib.pop(i) - count = {} - for child in e: - count[child.tag] = count.get(child.tag, 0) + 1 - self.distribute_branding(child, branding_dist, xpath, count) + if not re.search('(t-esc=|t-raw=|t-field=|t-call=)',child_text) or e.tag == "t": + count = {} + for child in e: + count[child.tag] = count.get(child.tag, 0) + 1 + self.distribute_branding(child, branding_dist, xpath, count) def render(self, cr, uid, id_or_xml_id, values, context=None): def loader(name): @@ -770,7 +771,6 @@ class view(osv.osv): arch_tree = etree.fromstring(arch) self.distribute_branding(arch_tree) arch = etree.tostring(arch_tree, encoding='utf-8') - arch = '%s' % (name.encode('utf-8'), arch) return arch From dbe58b7dde89c1c5ef92bd6dd7a7d23bdbb54574 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Tue, 2 Jul 2013 18:48:05 +0200 Subject: [PATCH 073/421] qweb t-field bzr revid: al@openerp.com-20130702164805-df3a03szrrx114r1 --- openerp/tools/qweb.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/openerp/tools/qweb.py b/openerp/tools/qweb.py index edae665ddfc..7b19666f5e6 100644 --- a/openerp/tools/qweb.py +++ b/openerp/tools/qweb.py @@ -280,16 +280,22 @@ class QWebXml(object): """ eg: +1 555 555 8069""" record = v[t_att["record"]] - if record._model._columns.get(t_att["field"])._type == 'many2one': - inner = cgi.escape(str(getattr(record, t_att["field"]).name_get()[0][1])) - else: - inner = cgi.escape(str(getattr(record, t_att["field"]))) + inner = "" + try: + if record._model._columns.get(t_att["field"])._type == 'many2one': + field = getattr(record, t_att["field"]) + if field: + inner = cgi.escape(str(field.name_get()[0][1])) + else: + inner = cgi.escape(str(getattr(record, t_att["field"]))) + if e.tagName != 't': + # are escaped + g_att += ' %s="%s"' % ('data-oe-model', record._model._name) + g_att += ' %s="%s"' % ('data-oe-id', str(record.id)) + g_att += ' %s="%s"' % ('data-oe-field', t_att["field"]) + except AttributeError: + _logger.warning("t-field no field %s for model %s", t_att["field"], record._model._name) - if e.tagName != 't': - # are escaped - g_att += ' %s="%s"' % ('data-oe-model', record._model._name) - g_att += ' %s="%s"' % ('data-oe-id', str(record.id)) - g_att += ' %s="%s"' % ('data-oe-field', t_att["field"]) return self.render_element(e, t_att, g_att, v, inner) # leave this, al. From 906da9b7ef88745b7ab461441ad2649e71ada5fb Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Tue, 2 Jul 2013 19:36:48 +0200 Subject: [PATCH 074/421] [FIX] views: correct distribute branding bzr revid: chs@openerp.com-20130702173648-96mxveli0eh1hzcj --- openerp/addons/base/ir/ir_ui_view.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 995f730712a..9926a121cdf 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -744,6 +744,9 @@ class view(osv.osv): return r['arch'] def distribute_branding(self, e, branding=None, xpath=None, count=None): + def has_qweb_attr(node): + return node.tag == 't' or any(a.startswith('t-') for a in node.attrib) + 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) @@ -758,7 +761,7 @@ class view(osv.osv): if e.attrib.get(i): branding_dist[i] = e.attrib.get(i) e.attrib.pop(i) - if not re.search('(t-esc=|t-raw=|t-field=|t-call=)',child_text) or e.tag == "t": + if not has_qweb_attr(e): count = {} for child in e: count[child.tag] = count.get(child.tag, 0) + 1 From 0a5b3c9fd7aee11a00b60baf49972c0ca74cc1c5 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Tue, 2 Jul 2013 20:14:39 +0200 Subject: [PATCH 075/421] [FIX] views: correct distribute branding bzr revid: chs@openerp.com-20130702181439-4ghn4ciz5efmzumk --- openerp/addons/base/ir/ir_ui_view.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 9926a121cdf..bb2034de15c 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -744,9 +744,6 @@ class view(osv.osv): return r['arch'] def distribute_branding(self, e, branding=None, xpath=None, count=None): - def has_qweb_attr(node): - return node.tag == 't' or any(a.startswith('t-') for a in node.attrib) - 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) @@ -756,12 +753,12 @@ class view(osv.osv): 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=)',child_text) or e.tag == "t": + if re.search('(data-oe-model=|t-esc=|t-raw=|t-field=|t-call=)',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 not has_qweb_attr(e): + if 't-raw' not in e.attrib: count = {} for child in e: count[child.tag] = count.get(child.tag, 0) + 1 From 726b333148be61d998e36c7bd084c8ef05fdbdfe Mon Sep 17 00:00:00 2001 From: Fabien Pinckaers Date: Tue, 2 Jul 2013 21:03:01 +0200 Subject: [PATCH 076/421] [FIX] copy() on ir.ui.view bzr revid: fp@tinyerp.com-20130702190301-oh7kj8iatbij3bm1 --- oe | 2 +- openerp/addons/base/ir/ir_ui_view.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/oe b/oe index 59827731409..5987cc71a8a 100755 --- a/oe +++ b/oe @@ -1,4 +1,4 @@ -#! /usr/bin/env python2 +#! /usr/bin/env python if __name__ == '__main__': import openerpcommand.main diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index bb2034de15c..ac4117f2973 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -846,6 +846,15 @@ class view(osv.osv): 'blank_nodes': blank_nodes, 'node_parent_field': _Model_Field,} + def copy(self, cr, uid, id, default=None, context=None): + if not default: + default = {} + default.update({ + 'model_ids': [], + }) + return super(view, self).copy(cr, uid, id, default, context=context) + + class view_sc(osv.osv): _name = 'ir.ui.view_sc' _columns = { From 58a4677f46678c7819a6a786c96b4f09f5e960c0 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Tue, 2 Jul 2013 22:49:49 +0200 Subject: [PATCH 077/421] [FIX] qweb: t-field must search all_columns bzr revid: chs@openerp.com-20130702204949-nqzhz226d24fvthx --- 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 7b19666f5e6..6656ef87ceb 100644 --- a/openerp/tools/qweb.py +++ b/openerp/tools/qweb.py @@ -282,7 +282,7 @@ class QWebXml(object): inner = "" try: - if record._model._columns.get(t_att["field"])._type == 'many2one': + if record._model._all_columns.get(t_att["field"]).column._type == 'many2one': field = getattr(record, t_att["field"]) if field: inner = cgi.escape(str(field.name_get()[0][1])) From 81ec0dbc1e29a855eaaeecb062abc1ff9b43ea86 Mon Sep 17 00:00:00 2001 From: Fabien Meghazi Date: Thu, 18 Jul 2013 19:03:14 +0200 Subject: [PATCH 078/421] [REM] Removed arch_file from ir.ui.view bzr revid: fme@openerp.com-20130718170314-f6g6topynizgnwl6 --- openerp/addons/base/ir/ir_ui_view.py | 36 ++-------------------- openerp/addons/base/ir/ir_ui_view_view.xml | 1 - 2 files changed, 3 insertions(+), 34 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index ac4117f2973..0b117a4f662 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -31,7 +31,6 @@ from lxml import etree from functools import partial from openerp import tools -from openerp.modules import module from openerp.osv import fields, osv, orm from openerp.tools import graph, SKIPPED_ELEMENT_TYPES from openerp.tools.safe_eval import safe_eval as eval @@ -59,32 +58,6 @@ class view_custom(osv.osv): class view(osv.osv): _name = 'ir.ui.view' - def _arch_get(self, cr, uid, ids, name, arg, context=None): - """ - For each id being read, return arch_db or the content of arch_file - """ - result = {} - for record in self.read(cr, uid, ids, ['arch_file', 'arch_db'], context=context): - if record['arch_db']: - result[record['id']] = record['arch_db'] - continue - - view_module, path = record['arch_file'].split('/', 1) - arch_path = module.get_module_resource(view_module, path) - if not arch_path: - raise IOError("No file '%s' in module '%s'" % (path, view_module)) - - with misc.file_open(arch_path) as f: - result[record['id']] = f.read().decode('utf-8') - - return result - - def _arch_set(self, cr, uid, id, name, value, arg, context=None): - """ - Forward writing to arch_db - """ - self.write(cr, uid, id, {'arch_db': value}, context=context) - _columns = { 'name': fields.char('View Name', required=True), 'model': fields.char('Object', size=64, select=True), @@ -100,9 +73,7 @@ class view(osv.osv): ('kanban', 'Kanban'), ('search','Search'), ('qweb', 'QWeb')], string='View Type'), - 'arch_file': fields.char("View path"), - 'arch_db': fields.text("Arch content", oldname='arch'), - 'arch': fields.function(_arch_get, fnct_inv=_arch_set, store=False, string="View Architecture", type='text', nodrop=True), + 'arch': fields.text('View Architecture', required=True), 'inherit_id': fields.many2one('ir.ui.view', 'Inherited View', ondelete='cascade', select=True), 'field_parent': fields.char('Child Field',size=64), 'xml_id': fields.function(osv.osv.get_xml_id, type='char', size=128, string="External ID", @@ -767,13 +738,12 @@ class view(osv.osv): 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' % (name.encode('utf-8'), arch) - + arch = '%s' % (arch) return arch + engine = qweb.QWebXml(loader) return engine.render(id_or_xml_id, values) diff --git a/openerp/addons/base/ir/ir_ui_view_view.xml b/openerp/addons/base/ir/ir_ui_view_view.xml index 6ae2cdc4ff3..44594c1e616 100644 --- a/openerp/addons/base/ir/ir_ui_view_view.xml +++ b/openerp/addons/base/ir/ir_ui_view_view.xml @@ -15,7 +15,6 @@ - From cb569244372c9272372ebec6d1565ce54c537b8c Mon Sep 17 00:00:00 2001 From: Fabien Meghazi Date: Mon, 22 Jul 2013 12:12:10 +0200 Subject: [PATCH 079/421] [ADD] xml data