Merge lp:~openerp-dev/openobject-server/trunk-replace-inherit_option_id-xmo into lp:openobject-server

Proposed by Xavier (Open ERP)
Status: Needs review
Proposed branch: lp:~openerp-dev/openobject-server/trunk-replace-inherit_option_id-xmo
Merge into: lp:openobject-server
Diff against target: 753 lines (+491/-42)
5 files modified
openerp/addons/base/ir/ir_ui_view.py (+95/-13)
openerp/addons/base/tests/test_views.py (+370/-10)
openerp/import_xml.rng (+16/-2)
openerp/osv/orm.py (+3/-13)
openerp/tools/convert.py (+7/-4)
To merge this branch: bzr merge lp:~openerp-dev/openobject-server/trunk-replace-inherit_option_id-xmo
Reviewer Review Type Date Requested Status
Olivier Dony (Odoo) Pending
Review via email: mp+218404@code.launchpad.net

Description of the change

Builds on https://code.launchpad.net/~openerp-dev/openobject-server/trunk-extended-view-inheritance-xmo/+merge/218256

Replaces inherit_option_id by a 3-state selection:
* always applied
* optional, currently enabled
* optional, currently disabled

Addons part (most of the work) at https://code.launchpad.net/~openerp-dev/openobject-addons/trunk-replace-inherit_option_id-xmo/+merge/218403

To post a comment you must log in.
5209. By Xavier (Open ERP)

[MERGE] from extended view inheritance, add help for application field

Unmerged revisions

5209. By Xavier (Open ERP)

[MERGE] from extended view inheritance, add help for application field

5208. By Xavier (Open ERP)

[IMP] inherit_option_id -> application

5207. By Xavier (Open ERP)

[IMP] prevent changing a view from application: always to application: disabled

not sure that's actually useful, and can still go always -> enabled -> disabled...

5206. By Xavier (Open ERP)

[ADD] application field & check during inheriting views read

5205. By Xavier (Open ERP)

[MERGE] from trunk

5204. By Xavier (Open ERP)

[ADD] support for primary mode in <template>

should probably validate that there's an inherit_id (?)

5203. By Xavier (Open ERP)

[ADD] use of explicit primary mode in read_combined

5202. By Xavier (Open ERP)

[FIX] default_view should be based on mode=primary, not on inherit_id=False

5201. By Xavier (Open ERP)

[ADD] mode attribute to views

Not used yet, only defined its relation to inherit_id:

not inherit_id + primary -> ok
not inherit_id + extension -> error
inherit_id + primary -> ok
inherit_id + extension -> ok

5200. By Xavier (Open ERP)

[IMP] simplify handling of callable _constraint message

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'openerp/addons/base/ir/ir_ui_view.py'
--- openerp/addons/base/ir/ir_ui_view.py 2014-05-01 15:26:04 +0000
+++ openerp/addons/base/ir/ir_ui_view.py 2014-05-06 14:57:36 +0000
@@ -117,8 +117,36 @@
117 'groups_id': fields.many2many('res.groups', 'ir_ui_view_group_rel', 'view_id', 'group_id',117 'groups_id': fields.many2many('res.groups', 'ir_ui_view_group_rel', 'view_id', 'group_id',
118 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."),118 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."),
119 'model_ids': fields.one2many('ir.model.data', 'res_id', domain=[('model','=','ir.ui.view')], auto_join=True),119 'model_ids': fields.one2many('ir.model.data', 'res_id', domain=[('model','=','ir.ui.view')], auto_join=True),
120
121 'mode': fields.selection(
122 [('primary', "Base view"), ('extension', "Extension View")],
123 string="View inheritance mode", required=True,
124 help="""Only applies if this view inherits from an other one (inherit_id is not False/Null).
125
126* if extension (default), if this view is requested the closest primary view
127 is looked up (via inherit_id), then all views inheriting from it with this
128 view's model are applied
129* if primary, the closest primary view is fully resolved (even if it uses a
130 different model than this one), then this view's inheritance specs
131 (<xpath/>) are applied, and the result is used as if it were this view's
132 actual arch.
133"""),
134
135 'application': fields.selection([
136 ('always', "Always applied"),
137 ('enabled', "Optional, enabled"),
138 ('disabled', "Optional, disabled"),
139 ],
140 required=True, string="Application status",
141 help="""If this view is inherited,
142* if always, the view always extends its parent
143* if enabled, the view currently extends its parent but can be disabled
144* if disabled, the view currently does not extend its parent but can be enabled
145 """),
120 }146 }
121 _defaults = {147 _defaults = {
148 'mode': 'primary',
149 'application': 'always',
122 'priority': 16,150 'priority': 16,
123 }151 }
124 _order = "priority,name"152 _order = "priority,name"
@@ -167,8 +195,14 @@
167 return False195 return False
168 return True196 return True
169197
198 _sql_constraints = [
199 ('inheritance_mode',
200 "CHECK (mode != 'extension' OR inherit_id IS NOT NULL)",
201 "Invalid inheritance mode: if the mode is 'extension', the view must"
202 " extend an other view"),
203 ]
170 _constraints = [204 _constraints = [
171 (_check_xml, 'Invalid view definition', ['arch'])205 (_check_xml, 'Invalid view definition', ['arch']),
172 ]206 ]
173207
174 def _auto_init(self, cr, context=None):208 def _auto_init(self, cr, context=None):
@@ -177,6 +211,12 @@
177 if not cr.fetchone():211 if not cr.fetchone():
178 cr.execute('CREATE INDEX ir_ui_view_model_type_inherit_id ON ir_ui_view (model, inherit_id)')212 cr.execute('CREATE INDEX ir_ui_view_model_type_inherit_id ON ir_ui_view (model, inherit_id)')
179213
214 def _compute_defaults(self, cr, uid, values, context=None):
215 if 'inherit_id' in values:
216 values.setdefault(
217 'mode', 'extension' if values['inherit_id'] else 'primary')
218 return values
219
180 def create(self, cr, uid, values, context=None):220 def create(self, cr, uid, values, context=None):
181 if 'type' not in values:221 if 'type' not in values:
182 if values.get('inherit_id'):222 if values.get('inherit_id'):
@@ -185,10 +225,13 @@
185 values['type'] = etree.fromstring(values['arch']).tag225 values['type'] = etree.fromstring(values['arch']).tag
186226
187 if not values.get('name'):227 if not values.get('name'):
188 values['name'] = "%s %s" % (values['model'], values['type'])228 values['name'] = "%s %s" % (values.get('model'), values['type'])
189229
190 self.read_template.clear_cache(self)230 self.read_template.clear_cache(self)
191 return super(view, self).create(cr, uid, values, context)231 return super(view, self).create(
232 cr, uid,
233 self._compute_defaults(cr, uid, values, context=context),
234 context=context)
192235
193 def write(self, cr, uid, ids, vals, context=None):236 def write(self, cr, uid, ids, vals, context=None):
194 if not isinstance(ids, (list, tuple)):237 if not isinstance(ids, (list, tuple)):
@@ -202,17 +245,43 @@
202 if custom_view_ids:245 if custom_view_ids:
203 self.pool.get('ir.ui.view.custom').unlink(cr, uid, custom_view_ids)246 self.pool.get('ir.ui.view.custom').unlink(cr, uid, custom_view_ids)
204247
248 if vals.get('application') == 'disabled':
249 from_always = self.search(
250 cr, uid, [('id', 'in', ids), ('application', '=', 'always')], context=context)
251 if from_always:
252 raise ValueError(
253 "Can't disable views %s marked as always applied" % (
254 ', '.join(map(str, from_always))))
255
205 self.read_template.clear_cache(self)256 self.read_template.clear_cache(self)
206 ret = super(view, self).write(cr, uid, ids, vals, context)257 ret = super(view, self).write(
258 cr, uid, ids,
259 self._compute_defaults(cr, uid, vals, context=context),
260 context)
207261
208 # if arch is modified views become noupdatable262 # if arch is modified views become noupdatable
209 if 'arch' in vals and not context.get('install_mode', False):263 if 'arch' in vals and not context.get('install_mode'):
210 # TODO: should be doable in a read and a write264 # TODO: should be doable in a read and a write
211 for view_ in self.browse(cr, uid, ids, context=context):265 for view_ in self.browse(cr, uid, ids, context=context):
212 if view_.model_data_id:266 if view_.model_data_id:
213 self.pool.get('ir.model.data').write(cr, openerp.SUPERUSER_ID, view_.model_data_id.id, {'noupdate': True})267 self.pool.get('ir.model.data').write(cr, openerp.SUPERUSER_ID, view_.model_data_id.id, {'noupdate': True})
214 return ret268 return ret
215269
270 def toggle(self, cr, uid, ids, context=None):
271 """ Switches between enabled and disabled application statuses
272 """
273 for view in self.browse(cr, uid, ids, context=context):
274 if view.application == 'enabled':
275 view.write({'application': 'disabled'})
276 elif view.application == 'disabled':
277 view.write({'application': 'enabled'})
278 else:
279 raise ValueError(_("Can't toggle view %d with application %r") % (
280 view.id,
281 view.application,
282 ))
283
284
216 def copy(self, cr, uid, id, default=None, context=None):285 def copy(self, cr, uid, id, default=None, context=None):
217 if not default:286 if not default:
218 default = {}287 default = {}
@@ -224,7 +293,7 @@
224 # default view selection293 # default view selection
225 def default_view(self, cr, uid, model, view_type, context=None):294 def default_view(self, cr, uid, model, view_type, context=None):
226 """ Fetches the default view for the provided (model, view_type) pair:295 """ Fetches the default view for the provided (model, view_type) pair:
227 view with no parent (inherit_id=Fase) with the lowest priority.296 primary view with the lowest priority.
228297
229 :param str model:298 :param str model:
230 :param int view_type:299 :param int view_type:
@@ -234,7 +303,7 @@
234 domain = [303 domain = [
235 ['model', '=', model],304 ['model', '=', model],
236 ['type', '=', view_type],305 ['type', '=', view_type],
237 ['inherit_id', '=', False],306 ['mode', '=', 'primary'],
238 ]307 ]
239 ids = self.search(cr, uid, domain, limit=1, order='priority', context=context)308 ids = self.search(cr, uid, domain, limit=1, order='priority', context=context)
240 if not ids:309 if not ids:
@@ -260,15 +329,19 @@
260329
261 user_groups = frozenset(self.pool.get('res.users').browse(cr, 1, uid, context).groups_id)330 user_groups = frozenset(self.pool.get('res.users').browse(cr, 1, uid, context).groups_id)
262331
263 check_view_ids = context and context.get('check_view_ids') or (0,)332 conditions = [
264 conditions = [['inherit_id', '=', view_id], ['model', '=', model]]333 ['inherit_id', '=', view_id],
334 ['model', '=', model],
335 ['mode', '=', 'extension'],
336 ['application', 'in', ['always', 'enabled']],
337 ]
265 if self.pool._init:338 if self.pool._init:
266 # Module init currently in progress, only consider views from339 # Module init currently in progress, only consider views from
267 # modules whose code is already loaded340 # modules whose code is already loaded
268 conditions.extend([341 conditions.extend([
269 '|',342 '|',
270 ['model_ids.module', 'in', tuple(self.pool._init_modules)],343 ['model_ids.module', 'in', tuple(self.pool._init_modules)],
271 ['id', 'in', check_view_ids],344 ['id', 'in', context and context.get('check_view_ids') or (0,)],
272 ])345 ])
273 view_ids = self.search(cr, uid, conditions, context=context)346 view_ids = self.search(cr, uid, conditions, context=context)
274347
@@ -424,7 +497,7 @@
424 if context is None: context = {}497 if context is None: context = {}
425 if root_id is None:498 if root_id is None:
426 root_id = source_id499 root_id = source_id
427 sql_inherit = self.pool.get('ir.ui.view').get_inheriting_views_arch(cr, uid, source_id, model, context=context)500 sql_inherit = self.pool['ir.ui.view'].get_inheriting_views_arch(cr, uid, source_id, model, context=context)
428 for (specs, view_id) in sql_inherit:501 for (specs, view_id) in sql_inherit:
429 specs_tree = etree.fromstring(specs.encode('utf-8'))502 specs_tree = etree.fromstring(specs.encode('utf-8'))
430 if context.get('inherit_branding'):503 if context.get('inherit_branding'):
@@ -447,7 +520,7 @@
447520
448 # if view_id is not a root view, climb back to the top.521 # if view_id is not a root view, climb back to the top.
449 base = v = self.browse(cr, uid, view_id, context=context)522 base = v = self.browse(cr, uid, view_id, context=context)
450 while v.inherit_id:523 while v.mode != 'primary':
451 v = v.inherit_id524 v = v.inherit_id
452 root_id = v.id525 root_id = v.id
453526
@@ -457,7 +530,16 @@
457530
458 # read the view arch531 # read the view arch
459 [view] = self.read(cr, uid, [root_id], fields=fields, context=context)532 [view] = self.read(cr, uid, [root_id], fields=fields, context=context)
460 arch_tree = etree.fromstring(view['arch'].encode('utf-8'))533 view_arch = etree.fromstring(view['arch'].encode('utf-8'))
534 if not v.inherit_id:
535 arch_tree = view_arch
536 else:
537 parent_view = self.read_combined(
538 cr, uid, v.inherit_id.id, fields=fields, context=context)
539 arch_tree = etree.fromstring(parent_view['arch'])
540 self.apply_inheritance_specs(
541 cr, uid, arch_tree, view_arch, parent_view['id'], context=context)
542
461543
462 if context.get('inherit_branding'):544 if context.get('inherit_branding'):
463 arch_tree.attrib.update({545 arch_tree.attrib.update({
464546
=== modified file 'openerp/addons/base/tests/test_views.py'
--- openerp/addons/base/tests/test_views.py 2014-04-08 11:49:36 +0000
+++ openerp/addons/base/tests/test_views.py 2014-05-06 14:57:36 +0000
@@ -1,12 +1,16 @@
1# -*- encoding: utf-8 -*-1# -*- encoding: utf-8 -*-
2from functools import partial2from functools import partial
3import itertools
34
4import unittest25import unittest2
56
6from lxml import etree as ET7from lxml import etree as ET
7from lxml.builder import E8from lxml.builder import E
89
10from psycopg2 import IntegrityError
11
9from openerp.tests import common12from openerp.tests import common
13import openerp.tools
1014
11Field = E.field15Field = E.field
1216
@@ -14,9 +18,22 @@
14 def setUp(self):18 def setUp(self):
15 super(ViewCase, self).setUp()19 super(ViewCase, self).setUp()
16 self.addTypeEqualityFunc(ET._Element, self.assertTreesEqual)20 self.addTypeEqualityFunc(ET._Element, self.assertTreesEqual)
21 self.Views = self.registry('ir.ui.view')
22
23 def browse(self, id, context=None):
24 return self.Views.browse(self.cr, self.uid, id, context=context)
25 def create(self, value, context=None):
26 return self.Views.create(self.cr, self.uid, value, context=context)
27
28 def read_combined(self, id):
29 return self.Views.read_combined(
30 self.cr, self.uid,
31 id, ['arch'],
32 context={'check_view_ids': self.Views.search(self.cr, self.uid, [])}
33 )
1734
18 def assertTreesEqual(self, n1, n2, msg=None):35 def assertTreesEqual(self, n1, n2, msg=None):
19 self.assertEqual(n1.tag, n2.tag)36 self.assertEqual(n1.tag, n2.tag, msg)
20 self.assertEqual((n1.text or '').strip(), (n2.text or '').strip(), msg)37 self.assertEqual((n1.text or '').strip(), (n2.text or '').strip(), msg)
21 self.assertEqual((n1.tail or '').strip(), (n2.tail or '').strip(), msg)38 self.assertEqual((n1.tail or '').strip(), (n2.tail or '').strip(), msg)
2239
@@ -24,8 +41,8 @@
24 # equality (!?!?!?!)41 # equality (!?!?!?!)
25 self.assertEqual(dict(n1.attrib), dict(n2.attrib), msg)42 self.assertEqual(dict(n1.attrib), dict(n2.attrib), msg)
2643
27 for c1, c2 in zip(n1, n2):44 for c1, c2 in itertools.izip_longest(n1, n2):
28 self.assertTreesEqual(c1, c2, msg)45 self.assertEqual(c1, c2, msg)
2946
3047
31class TestNodeLocator(common.TransactionCase):48class TestNodeLocator(common.TransactionCase):
@@ -374,13 +391,6 @@
374 """ Applies a sequence of modificator archs to a base view391 """ Applies a sequence of modificator archs to a base view
375 """392 """
376393
377class TestViewCombined(ViewCase):
378 """
379 Test fallback operations of View.read_combined:
380 * defaults mapping
381 * ?
382 """
383
384class TestNoModel(ViewCase):394class TestNoModel(ViewCase):
385 def test_create_view_nomodel(self):395 def test_create_view_nomodel(self):
386 View = self.registry('ir.ui.view')396 View = self.registry('ir.ui.view')
@@ -628,6 +638,8 @@
628 def _insert_view(self, **kw):638 def _insert_view(self, **kw):
629 """Insert view into database via a query to passtrough validation"""639 """Insert view into database via a query to passtrough validation"""
630 kw.pop('id', None)640 kw.pop('id', None)
641 kw.setdefault('mode', 'extension' if kw.get('inherit_id') else 'primary')
642 kw.setdefault('application', 'always')
631643
632 keys = sorted(kw.keys())644 keys = sorted(kw.keys())
633 fields = ','.join('"%s"' % (k.replace('"', r'\"'),) for k in keys)645 fields = ','.join('"%s"' % (k.replace('"', r'\"'),) for k in keys)
@@ -804,3 +816,351 @@
804 E.button(name="action_next", type="object", string="New button")),816 E.button(name="action_next", type="object", string="New button")),
805 string="Replacement title", version="7.0"817 string="Replacement title", version="7.0"
806 ))818 ))
819
820class ViewModeField(ViewCase):
821 """
822 This should probably, eventually, be folded back into other test case
823 classes, integrating the test (or not) of the mode field to regular cases
824 """
825
826 def testModeImplicitValue(self):
827 """ mode is auto-generated from inherit_id:
828 * inherit_id -> mode=extension
829 * not inherit_id -> mode=primary
830 """
831 view = self.browse(self.create({
832 'inherit_id': None,
833 'arch': '<qweb/>'
834 }))
835 self.assertEqual(view.mode, 'primary')
836
837 view2 = self.browse(self.create({
838 'inherit_id': view.id,
839 'arch': '<qweb/>'
840 }))
841 self.assertEqual(view2.mode, 'extension')
842
843 @openerp.tools.mute_logger('openerp.sql_db')
844 def testModeExplicit(self):
845 view = self.browse(self.create({
846 'inherit_id': None,
847 'arch': '<qweb/>'
848 }))
849 view2 = self.browse(self.create({
850 'inherit_id': view.id,
851 'mode': 'primary',
852 'arch': '<qweb/>'
853 }))
854 self.assertEqual(view.mode, 'primary')
855
856 with self.assertRaises(IntegrityError):
857 self.create({
858 'inherit_id': None,
859 'mode': 'extension',
860 'arch': '<qweb/>'
861 })
862
863 @openerp.tools.mute_logger('openerp.sql_db')
864 def testPurePrimaryToExtension(self):
865 """
866 A primary view with inherit_id=None can't be converted to extension
867 """
868 view_pure_primary = self.browse(self.create({
869 'inherit_id': None,
870 'arch': '<qweb/>'
871 }))
872 with self.assertRaises(IntegrityError):
873 view_pure_primary.write({'mode': 'extension'})
874
875 def testInheritPrimaryToExtension(self):
876 """
877 A primary view with an inherit_id can be converted to extension
878 """
879 base = self.create({'inherit_id': None, 'arch': '<qweb/>'})
880 view = self.browse(self.create({
881 'inherit_id': base,
882 'mode': 'primary',
883 'arch': '<qweb/>'
884 }))
885
886 view.write({'mode': 'extension'})
887
888 def testDefaultExtensionToPrimary(self):
889 """
890 An extension view can be converted to primary
891 """
892 base = self.create({'inherit_id': None, 'arch': '<qweb/>'})
893 view = self.browse(self.create({
894 'inherit_id': base,
895 'arch': '<qweb/>'
896 }))
897
898 view.write({'mode': 'primary'})
899
900class TestDefaultView(ViewCase):
901 def testDefaultViewBase(self):
902 self.create({
903 'inherit_id': False,
904 'priority': 10,
905 'mode': 'primary',
906 'arch': '<qweb/>',
907 })
908 v2 = self.create({
909 'inherit_id': False,
910 'priority': 1,
911 'mode': 'primary',
912 'arch': '<qweb/>',
913 })
914
915 default = self.Views.default_view(self.cr, self.uid, False, 'qweb')
916 self.assertEqual(
917 default, v2,
918 "default_view should get the view with the lowest priority for "
919 "a (model, view_type) pair"
920 )
921
922 def testDefaultViewPrimary(self):
923 v1 = self.create({
924 'inherit_id': False,
925 'priority': 10,
926 'mode': 'primary',
927 'arch': '<qweb/>',
928 })
929 self.create({
930 'inherit_id': False,
931 'priority': 5,
932 'mode': 'primary',
933 'arch': '<qweb/>',
934 })
935 v3 = self.create({
936 'inherit_id': v1,
937 'priority': 1,
938 'mode': 'primary',
939 'arch': '<qweb/>',
940 })
941
942 default = self.Views.default_view(self.cr, self.uid, False, 'qweb')
943 self.assertEqual(
944 default, v3,
945 "default_view should get the view with the lowest priority for "
946 "a (model, view_type) pair in all the primary tables"
947 )
948
949class TestViewCombined(ViewCase):
950 """
951 * When asked for a view, instead of looking for the closest parent with
952 inherit_id=False look for mode=primary
953 * If root.inherit_id, resolve the arch for root.inherit_id (?using which
954 model?), then apply root's inheritance specs to it
955 * Apply inheriting views on top
956 """
957
958 def setUp(self):
959 super(TestViewCombined, self).setUp()
960
961 self.a1 = self.create({
962 'model': 'a',
963 'arch': '<qweb><a1/></qweb>'
964 })
965 self.a2 = self.create({
966 'model': 'a',
967 'inherit_id': self.a1,
968 'priority': 5,
969 'arch': '<xpath expr="//a1" position="after"><a2/></xpath>'
970 })
971 self.a3 = self.create({
972 'model': 'a',
973 'inherit_id': self.a1,
974 'arch': '<xpath expr="//a1" position="after"><a3/></xpath>'
975 })
976 # mode=primary should be an inheritance boundary in both direction,
977 # even within a model it should not extend the parent
978 self.a4 = self.create({
979 'model': 'a',
980 'inherit_id': self.a1,
981 'mode': 'primary',
982 'arch': '<xpath expr="//a1" position="after"><a4/></xpath>',
983 })
984
985 self.b1 = self.create({
986 'model': 'b',
987 'inherit_id': self.a3,
988 'mode': 'primary',
989 'arch': '<xpath expr="//a1" position="after"><b1/></xpath>'
990 })
991 self.b2 = self.create({
992 'model': 'b',
993 'inherit_id': self.b1,
994 'arch': '<xpath expr="//a1" position="after"><b2/></xpath>'
995 })
996
997 self.c1 = self.create({
998 'model': 'c',
999 'inherit_id': self.a1,
1000 'mode': 'primary',
1001 'arch': '<xpath expr="//a1" position="after"><c1/></xpath>'
1002 })
1003 self.c2 = self.create({
1004 'model': 'c',
1005 'inherit_id': self.c1,
1006 'priority': 5,
1007 'arch': '<xpath expr="//a1" position="after"><c2/></xpath>'
1008 })
1009 self.c3 = self.create({
1010 'model': 'c',
1011 'inherit_id': self.c2,
1012 'priority': 10,
1013 'arch': '<xpath expr="//a1" position="after"><c3/></xpath>'
1014 })
1015
1016 self.d1 = self.create({
1017 'model': 'd',
1018 'inherit_id': self.b1,
1019 'mode': 'primary',
1020 'arch': '<xpath expr="//a1" position="after"><d1/></xpath>'
1021 })
1022
1023 def test_basic_read(self):
1024 arch = self.read_combined(self.a1)['arch']
1025 self.assertEqual(
1026 ET.fromstring(arch),
1027 E.qweb(
1028 E.a1(),
1029 E.a3(),
1030 E.a2(),
1031 ), arch)
1032
1033 def test_read_from_child(self):
1034 arch = self.read_combined(self.a3)['arch']
1035 self.assertEqual(
1036 ET.fromstring(arch),
1037 E.qweb(
1038 E.a1(),
1039 E.a3(),
1040 E.a2(),
1041 ), arch)
1042
1043 def test_read_from_child_primary(self):
1044 arch = self.read_combined(self.a4)['arch']
1045 self.assertEqual(
1046 ET.fromstring(arch),
1047 E.qweb(
1048 E.a1(),
1049 E.a4(),
1050 E.a3(),
1051 E.a2(),
1052 ), arch)
1053
1054 def test_cross_model_simple(self):
1055 arch = self.read_combined(self.c2)['arch']
1056 self.assertEqual(
1057 ET.fromstring(arch),
1058 E.qweb(
1059 E.a1(),
1060 E.c3(),
1061 E.c2(),
1062 E.c1(),
1063 E.a3(),
1064 E.a2(),
1065 ), arch)
1066
1067 def test_cross_model_double(self):
1068 arch = self.read_combined(self.d1)['arch']
1069 self.assertEqual(
1070 ET.fromstring(arch),
1071 E.qweb(
1072 E.a1(),
1073 E.d1(),
1074 E.b2(),
1075 E.b1(),
1076 E.a3(),
1077 E.a2(),
1078 ), arch)
1079
1080class TestOptionalViews(ViewCase):
1081 """
1082 Tests ability to enable/disable inherited views, formerly known as
1083 inherit_option_id
1084 """
1085
1086 def setUp(self):
1087 super(TestOptionalViews, self).setUp()
1088 self.v0 = self.create({
1089 'model': 'a',
1090 'arch': '<qweb><base/></qweb>',
1091 })
1092 self.v1 = self.create({
1093 'model': 'a',
1094 'inherit_id': self.v0,
1095 'application': 'always',
1096 'priority': 10,
1097 'arch': '<xpath expr="//base" position="after"><v1/></xpath>',
1098 })
1099 self.v2 = self.create({
1100 'model': 'a',
1101 'inherit_id': self.v0,
1102 'application': 'enabled',
1103 'priority': 9,
1104 'arch': '<xpath expr="//base" position="after"><v2/></xpath>',
1105 })
1106 self.v3 = self.create({
1107 'model': 'a',
1108 'inherit_id': self.v0,
1109 'application': 'disabled',
1110 'priority': 8,
1111 'arch': '<xpath expr="//base" position="after"><v3/></xpath>'
1112 })
1113
1114 def test_applied(self):
1115 """ mandatory and enabled views should be applied
1116 """
1117 arch = self.read_combined(self.v0)['arch']
1118 self.assertEqual(
1119 ET.fromstring(arch),
1120 E.qweb(
1121 E.base(),
1122 E.v1(),
1123 E.v2(),
1124 )
1125 )
1126
1127 def test_applied_state_toggle(self):
1128 """ Change application states of v2 and v3, check that the results
1129 are as expected
1130 """
1131 self.browse(self.v2).write({'application': 'disabled'})
1132 arch = self.read_combined(self.v0)['arch']
1133 self.assertEqual(
1134 ET.fromstring(arch),
1135 E.qweb(
1136 E.base(),
1137 E.v1(),
1138 )
1139 )
1140
1141 self.browse(self.v3).write({'application': 'enabled'})
1142 arch = self.read_combined(self.v0)['arch']
1143 self.assertEqual(
1144 ET.fromstring(arch),
1145 E.qweb(
1146 E.base(),
1147 E.v1(),
1148 E.v3(),
1149 )
1150 )
1151
1152 self.browse(self.v2).write({'application': 'enabled'})
1153 arch = self.read_combined(self.v0)['arch']
1154 self.assertEqual(
1155 ET.fromstring(arch),
1156 E.qweb(
1157 E.base(),
1158 E.v1(),
1159 E.v2(),
1160 E.v3(),
1161 )
1162 )
1163
1164 def test_mandatory_no_disabled(self):
1165 with self.assertRaises(Exception):
1166 self.browse(self.v1).write({'application': 'disabled'})
8071167
=== modified file 'openerp/import_xml.rng'
--- openerp/import_xml.rng 2014-01-16 09:17:16 +0000
+++ openerp/import_xml.rng 2014-05-06 14:57:36 +0000
@@ -217,9 +217,23 @@
217 <rng:optional><rng:attribute name="priority"/></rng:optional>217 <rng:optional><rng:attribute name="priority"/></rng:optional>
218 <rng:choice>218 <rng:choice>
219 <rng:group>219 <rng:group>
220 <rng:optional><rng:attribute name="inherit_id"/></rng:optional>220 <rng:optional>
221 <rng:optional><rng:attribute name="inherit_option_id"/></rng:optional>221 <rng:attribute name="inherit_id"/>
222 <rng:optional>
223 <rng:attribute name="primary">
224 <rng:value>True</rng:value>
225 </rng:attribute>
226 </rng:optional>
227 </rng:optional>
222 <rng:optional><rng:attribute name="groups"/></rng:optional>228 <rng:optional><rng:attribute name="groups"/></rng:optional>
229 <rng:optional>
230 <rng:attribute name="optional">
231 <rng:choice>
232 <rng:value>enabled</rng:value>
233 <rng:value>disabled</rng:value>
234 </rng:choice>
235 </rng:attribute>
236 </rng:optional>
223 </rng:group>237 </rng:group>
224 <rng:optional>238 <rng:optional>
225 <rng:attribute name="page"><rng:value>True</rng:value></rng:attribute>239 <rng:attribute name="page"><rng:value>True</rng:value></rng:attribute>
226240
=== modified file 'openerp/osv/orm.py'
--- openerp/osv/orm.py 2014-04-16 14:34:31 +0000
+++ openerp/osv/orm.py 2014-05-06 14:57:36 +0000
@@ -729,7 +729,6 @@
729 _all_columns = {}729 _all_columns = {}
730730
731 _table = None731 _table = None
732 _invalids = set()
733 _log_create = False732 _log_create = False
734 _sql_constraints = []733 _sql_constraints = []
735 _protected = ['read', 'write', 'create', 'default_get', 'perm_read', 'unlink', 'fields_get', 'fields_view_get', 'search', 'name_get', 'distinct_field_get', 'name_search', 'copy', 'import_data', 'search_count', 'exists']734 _protected = ['read', 'write', 'create', 'default_get', 'perm_read', 'unlink', 'fields_get', 'fields_view_get', 'search', 'name_get', 'distinct_field_get', 'name_search', 'copy', 'import_data', 'search_count', 'exists']
@@ -1543,9 +1542,6 @@
15431542
1544 yield dbid, xid, converted, dict(extras, record=stream.index)1543 yield dbid, xid, converted, dict(extras, record=stream.index)
15451544
1546 def get_invalid_fields(self, cr, uid):
1547 return list(self._invalids)
1548
1549 def _validate(self, cr, uid, ids, context=None):1545 def _validate(self, cr, uid, ids, context=None):
1550 context = context or {}1546 context = context or {}
1551 lng = context.get('lang')1547 lng = context.get('lang')
@@ -1566,12 +1562,9 @@
1566 # Check presence of __call__ directly instead of using1562 # Check presence of __call__ directly instead of using
1567 # callable() because it will be deprecated as of Python 3.01563 # callable() because it will be deprecated as of Python 3.0
1568 if hasattr(msg, '__call__'):1564 if hasattr(msg, '__call__'):
1569 tmp_msg = msg(self, cr, uid, ids, context=context)1565 translated_msg = msg(self, cr, uid, ids, context=context)
1570 if isinstance(tmp_msg, tuple):1566 if isinstance(translated_msg, tuple):
1571 tmp_msg, params = tmp_msg1567 translated_msg = translated_msg[0] % translated_msg[1]
1572 translated_msg = tmp_msg % params
1573 else:
1574 translated_msg = tmp_msg
1575 else:1568 else:
1576 translated_msg = trans._get_source(cr, uid, self._name, 'constraint', lng, msg)1569 translated_msg = trans._get_source(cr, uid, self._name, 'constraint', lng, msg)
1577 if extra_error:1570 if extra_error:
@@ -1579,11 +1572,8 @@
1579 error_msgs.append(1572 error_msgs.append(
1580 _("The field(s) `%s` failed against a constraint: %s") % (', '.join(fields), translated_msg)1573 _("The field(s) `%s` failed against a constraint: %s") % (', '.join(fields), translated_msg)
1581 )1574 )
1582 self._invalids.update(fields)
1583 if error_msgs:1575 if error_msgs:
1584 raise except_orm('ValidateError', '\n'.join(error_msgs))1576 raise except_orm('ValidateError', '\n'.join(error_msgs))
1585 else:
1586 self._invalids.clear()
15871577
1588 def default_get(self, cr, uid, fields_list, context=None):1578 def default_get(self, cr, uid, fields_list, context=None):
1589 """1579 """
15901580
=== modified file 'openerp/tools/convert.py'
--- openerp/tools/convert.py 2014-04-24 13:14:05 +0000
+++ openerp/tools/convert.py 2014-05-06 14:57:36 +0000
@@ -861,7 +861,7 @@
861 if '.' not in full_tpl_id:861 if '.' not in full_tpl_id:
862 full_tpl_id = '%s.%s' % (self.module, tpl_id)862 full_tpl_id = '%s.%s' % (self.module, tpl_id)
863 # set the full template name for qweb <module>.<id>863 # set the full template name for qweb <module>.<id>
864 if not (el.get('inherit_id') or el.get('inherit_option_id')):864 if not el.get('inherit_id'):
865 el.set('t-name', full_tpl_id)865 el.set('t-name', full_tpl_id)
866 el.tag = 't'866 el.tag = 't'
867 else:867 else:
@@ -884,15 +884,18 @@
884 record.append(Field("qweb", name='type'))884 record.append(Field("qweb", name='type'))
885 record.append(Field(el.get('priority', "16"), name='priority'))885 record.append(Field(el.get('priority', "16"), name='priority'))
886 record.append(Field(el, name="arch", type="xml"))886 record.append(Field(el, name="arch", type="xml"))
887 for field_name in ('inherit_id','inherit_option_id'):887 if 'inherit_id' in el.attrib:
888 value = el.attrib.pop(field_name, None)888 record.append(Field(name='inherit_id', ref=el.get('inherit_id')))
889 if value: record.append(Field(name=field_name, ref=value))
890 groups = el.attrib.pop('groups', None)889 groups = el.attrib.pop('groups', None)
891 if groups:890 if groups:
892 grp_lst = map(lambda x: "ref('%s')" % x, groups.split(','))891 grp_lst = map(lambda x: "ref('%s')" % x, groups.split(','))
893 record.append(Field(name="groups_id", eval="[(6, 0, ["+', '.join(grp_lst)+"])]"))892 record.append(Field(name="groups_id", eval="[(6, 0, ["+', '.join(grp_lst)+"])]"))
894 if el.attrib.pop('page', None) == 'True':893 if el.attrib.pop('page', None) == 'True':
895 record.append(Field(name="page", eval="True"))894 record.append(Field(name="page", eval="True"))
895 if el.get('primary') == 'True':
896 record.append(Field('primary', name='mode'))
897 if el.get('optional'):
898 record.append(Field(el.get('optional'), name='application'))
896899
897 return self._tag_record(cr, record, data_node)900 return self._tag_record(cr, record, data_node)
898901