Merge lp:~openerp-dev/openobject-server/trunk-replace-inherit_option_id-xmo into lp:openobject-server
- trunk-replace-inherit_option_id-xmo
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Olivier Dony (Odoo) | Pending | ||
Review via email: mp+218404@code.launchpad.net |
Commit message
Description of the change
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:/
- 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
1 | === modified file 'openerp/addons/base/ir/ir_ui_view.py' |
2 | --- openerp/addons/base/ir/ir_ui_view.py 2014-05-01 15:26:04 +0000 |
3 | +++ openerp/addons/base/ir/ir_ui_view.py 2014-05-06 14:57:36 +0000 |
4 | @@ -117,8 +117,36 @@ |
5 | 'groups_id': fields.many2many('res.groups', 'ir_ui_view_group_rel', 'view_id', 'group_id', |
6 | 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."), |
7 | 'model_ids': fields.one2many('ir.model.data', 'res_id', domain=[('model','=','ir.ui.view')], auto_join=True), |
8 | + |
9 | + 'mode': fields.selection( |
10 | + [('primary', "Base view"), ('extension', "Extension View")], |
11 | + string="View inheritance mode", required=True, |
12 | + help="""Only applies if this view inherits from an other one (inherit_id is not False/Null). |
13 | + |
14 | +* if extension (default), if this view is requested the closest primary view |
15 | + is looked up (via inherit_id), then all views inheriting from it with this |
16 | + view's model are applied |
17 | +* if primary, the closest primary view is fully resolved (even if it uses a |
18 | + different model than this one), then this view's inheritance specs |
19 | + (<xpath/>) are applied, and the result is used as if it were this view's |
20 | + actual arch. |
21 | +"""), |
22 | + |
23 | + 'application': fields.selection([ |
24 | + ('always', "Always applied"), |
25 | + ('enabled', "Optional, enabled"), |
26 | + ('disabled', "Optional, disabled"), |
27 | + ], |
28 | + required=True, string="Application status", |
29 | + help="""If this view is inherited, |
30 | +* if always, the view always extends its parent |
31 | +* if enabled, the view currently extends its parent but can be disabled |
32 | +* if disabled, the view currently does not extend its parent but can be enabled |
33 | + """), |
34 | } |
35 | _defaults = { |
36 | + 'mode': 'primary', |
37 | + 'application': 'always', |
38 | 'priority': 16, |
39 | } |
40 | _order = "priority,name" |
41 | @@ -167,8 +195,14 @@ |
42 | return False |
43 | return True |
44 | |
45 | + _sql_constraints = [ |
46 | + ('inheritance_mode', |
47 | + "CHECK (mode != 'extension' OR inherit_id IS NOT NULL)", |
48 | + "Invalid inheritance mode: if the mode is 'extension', the view must" |
49 | + " extend an other view"), |
50 | + ] |
51 | _constraints = [ |
52 | - (_check_xml, 'Invalid view definition', ['arch']) |
53 | + (_check_xml, 'Invalid view definition', ['arch']), |
54 | ] |
55 | |
56 | def _auto_init(self, cr, context=None): |
57 | @@ -177,6 +211,12 @@ |
58 | if not cr.fetchone(): |
59 | cr.execute('CREATE INDEX ir_ui_view_model_type_inherit_id ON ir_ui_view (model, inherit_id)') |
60 | |
61 | + def _compute_defaults(self, cr, uid, values, context=None): |
62 | + if 'inherit_id' in values: |
63 | + values.setdefault( |
64 | + 'mode', 'extension' if values['inherit_id'] else 'primary') |
65 | + return values |
66 | + |
67 | def create(self, cr, uid, values, context=None): |
68 | if 'type' not in values: |
69 | if values.get('inherit_id'): |
70 | @@ -185,10 +225,13 @@ |
71 | values['type'] = etree.fromstring(values['arch']).tag |
72 | |
73 | if not values.get('name'): |
74 | - values['name'] = "%s %s" % (values['model'], values['type']) |
75 | + values['name'] = "%s %s" % (values.get('model'), values['type']) |
76 | |
77 | self.read_template.clear_cache(self) |
78 | - return super(view, self).create(cr, uid, values, context) |
79 | + return super(view, self).create( |
80 | + cr, uid, |
81 | + self._compute_defaults(cr, uid, values, context=context), |
82 | + context=context) |
83 | |
84 | def write(self, cr, uid, ids, vals, context=None): |
85 | if not isinstance(ids, (list, tuple)): |
86 | @@ -202,17 +245,43 @@ |
87 | if custom_view_ids: |
88 | self.pool.get('ir.ui.view.custom').unlink(cr, uid, custom_view_ids) |
89 | |
90 | + if vals.get('application') == 'disabled': |
91 | + from_always = self.search( |
92 | + cr, uid, [('id', 'in', ids), ('application', '=', 'always')], context=context) |
93 | + if from_always: |
94 | + raise ValueError( |
95 | + "Can't disable views %s marked as always applied" % ( |
96 | + ', '.join(map(str, from_always)))) |
97 | + |
98 | self.read_template.clear_cache(self) |
99 | - ret = super(view, self).write(cr, uid, ids, vals, context) |
100 | + ret = super(view, self).write( |
101 | + cr, uid, ids, |
102 | + self._compute_defaults(cr, uid, vals, context=context), |
103 | + context) |
104 | |
105 | # if arch is modified views become noupdatable |
106 | - if 'arch' in vals and not context.get('install_mode', False): |
107 | + if 'arch' in vals and not context.get('install_mode'): |
108 | # TODO: should be doable in a read and a write |
109 | for view_ in self.browse(cr, uid, ids, context=context): |
110 | if view_.model_data_id: |
111 | self.pool.get('ir.model.data').write(cr, openerp.SUPERUSER_ID, view_.model_data_id.id, {'noupdate': True}) |
112 | return ret |
113 | |
114 | + def toggle(self, cr, uid, ids, context=None): |
115 | + """ Switches between enabled and disabled application statuses |
116 | + """ |
117 | + for view in self.browse(cr, uid, ids, context=context): |
118 | + if view.application == 'enabled': |
119 | + view.write({'application': 'disabled'}) |
120 | + elif view.application == 'disabled': |
121 | + view.write({'application': 'enabled'}) |
122 | + else: |
123 | + raise ValueError(_("Can't toggle view %d with application %r") % ( |
124 | + view.id, |
125 | + view.application, |
126 | + )) |
127 | + |
128 | + |
129 | def copy(self, cr, uid, id, default=None, context=None): |
130 | if not default: |
131 | default = {} |
132 | @@ -224,7 +293,7 @@ |
133 | # default view selection |
134 | def default_view(self, cr, uid, model, view_type, context=None): |
135 | """ Fetches the default view for the provided (model, view_type) pair: |
136 | - view with no parent (inherit_id=Fase) with the lowest priority. |
137 | + primary view with the lowest priority. |
138 | |
139 | :param str model: |
140 | :param int view_type: |
141 | @@ -234,7 +303,7 @@ |
142 | domain = [ |
143 | ['model', '=', model], |
144 | ['type', '=', view_type], |
145 | - ['inherit_id', '=', False], |
146 | + ['mode', '=', 'primary'], |
147 | ] |
148 | ids = self.search(cr, uid, domain, limit=1, order='priority', context=context) |
149 | if not ids: |
150 | @@ -260,15 +329,19 @@ |
151 | |
152 | user_groups = frozenset(self.pool.get('res.users').browse(cr, 1, uid, context).groups_id) |
153 | |
154 | - check_view_ids = context and context.get('check_view_ids') or (0,) |
155 | - conditions = [['inherit_id', '=', view_id], ['model', '=', model]] |
156 | + conditions = [ |
157 | + ['inherit_id', '=', view_id], |
158 | + ['model', '=', model], |
159 | + ['mode', '=', 'extension'], |
160 | + ['application', 'in', ['always', 'enabled']], |
161 | + ] |
162 | if self.pool._init: |
163 | # Module init currently in progress, only consider views from |
164 | # modules whose code is already loaded |
165 | conditions.extend([ |
166 | '|', |
167 | ['model_ids.module', 'in', tuple(self.pool._init_modules)], |
168 | - ['id', 'in', check_view_ids], |
169 | + ['id', 'in', context and context.get('check_view_ids') or (0,)], |
170 | ]) |
171 | view_ids = self.search(cr, uid, conditions, context=context) |
172 | |
173 | @@ -424,7 +497,7 @@ |
174 | if context is None: context = {} |
175 | if root_id is None: |
176 | root_id = source_id |
177 | - sql_inherit = self.pool.get('ir.ui.view').get_inheriting_views_arch(cr, uid, source_id, model, context=context) |
178 | + sql_inherit = self.pool['ir.ui.view'].get_inheriting_views_arch(cr, uid, source_id, model, context=context) |
179 | for (specs, view_id) in sql_inherit: |
180 | specs_tree = etree.fromstring(specs.encode('utf-8')) |
181 | if context.get('inherit_branding'): |
182 | @@ -447,7 +520,7 @@ |
183 | |
184 | # if view_id is not a root view, climb back to the top. |
185 | base = v = self.browse(cr, uid, view_id, context=context) |
186 | - while v.inherit_id: |
187 | + while v.mode != 'primary': |
188 | v = v.inherit_id |
189 | root_id = v.id |
190 | |
191 | @@ -457,7 +530,16 @@ |
192 | |
193 | # read the view arch |
194 | [view] = self.read(cr, uid, [root_id], fields=fields, context=context) |
195 | - arch_tree = etree.fromstring(view['arch'].encode('utf-8')) |
196 | + view_arch = etree.fromstring(view['arch'].encode('utf-8')) |
197 | + if not v.inherit_id: |
198 | + arch_tree = view_arch |
199 | + else: |
200 | + parent_view = self.read_combined( |
201 | + cr, uid, v.inherit_id.id, fields=fields, context=context) |
202 | + arch_tree = etree.fromstring(parent_view['arch']) |
203 | + self.apply_inheritance_specs( |
204 | + cr, uid, arch_tree, view_arch, parent_view['id'], context=context) |
205 | + |
206 | |
207 | if context.get('inherit_branding'): |
208 | arch_tree.attrib.update({ |
209 | |
210 | === modified file 'openerp/addons/base/tests/test_views.py' |
211 | --- openerp/addons/base/tests/test_views.py 2014-04-08 11:49:36 +0000 |
212 | +++ openerp/addons/base/tests/test_views.py 2014-05-06 14:57:36 +0000 |
213 | @@ -1,12 +1,16 @@ |
214 | # -*- encoding: utf-8 -*- |
215 | from functools import partial |
216 | +import itertools |
217 | |
218 | import unittest2 |
219 | |
220 | from lxml import etree as ET |
221 | from lxml.builder import E |
222 | |
223 | +from psycopg2 import IntegrityError |
224 | + |
225 | from openerp.tests import common |
226 | +import openerp.tools |
227 | |
228 | Field = E.field |
229 | |
230 | @@ -14,9 +18,22 @@ |
231 | def setUp(self): |
232 | super(ViewCase, self).setUp() |
233 | self.addTypeEqualityFunc(ET._Element, self.assertTreesEqual) |
234 | + self.Views = self.registry('ir.ui.view') |
235 | + |
236 | + def browse(self, id, context=None): |
237 | + return self.Views.browse(self.cr, self.uid, id, context=context) |
238 | + def create(self, value, context=None): |
239 | + return self.Views.create(self.cr, self.uid, value, context=context) |
240 | + |
241 | + def read_combined(self, id): |
242 | + return self.Views.read_combined( |
243 | + self.cr, self.uid, |
244 | + id, ['arch'], |
245 | + context={'check_view_ids': self.Views.search(self.cr, self.uid, [])} |
246 | + ) |
247 | |
248 | def assertTreesEqual(self, n1, n2, msg=None): |
249 | - self.assertEqual(n1.tag, n2.tag) |
250 | + self.assertEqual(n1.tag, n2.tag, msg) |
251 | self.assertEqual((n1.text or '').strip(), (n2.text or '').strip(), msg) |
252 | self.assertEqual((n1.tail or '').strip(), (n2.tail or '').strip(), msg) |
253 | |
254 | @@ -24,8 +41,8 @@ |
255 | # equality (!?!?!?!) |
256 | self.assertEqual(dict(n1.attrib), dict(n2.attrib), msg) |
257 | |
258 | - for c1, c2 in zip(n1, n2): |
259 | - self.assertTreesEqual(c1, c2, msg) |
260 | + for c1, c2 in itertools.izip_longest(n1, n2): |
261 | + self.assertEqual(c1, c2, msg) |
262 | |
263 | |
264 | class TestNodeLocator(common.TransactionCase): |
265 | @@ -374,13 +391,6 @@ |
266 | """ Applies a sequence of modificator archs to a base view |
267 | """ |
268 | |
269 | -class TestViewCombined(ViewCase): |
270 | - """ |
271 | - Test fallback operations of View.read_combined: |
272 | - * defaults mapping |
273 | - * ? |
274 | - """ |
275 | - |
276 | class TestNoModel(ViewCase): |
277 | def test_create_view_nomodel(self): |
278 | View = self.registry('ir.ui.view') |
279 | @@ -628,6 +638,8 @@ |
280 | def _insert_view(self, **kw): |
281 | """Insert view into database via a query to passtrough validation""" |
282 | kw.pop('id', None) |
283 | + kw.setdefault('mode', 'extension' if kw.get('inherit_id') else 'primary') |
284 | + kw.setdefault('application', 'always') |
285 | |
286 | keys = sorted(kw.keys()) |
287 | fields = ','.join('"%s"' % (k.replace('"', r'\"'),) for k in keys) |
288 | @@ -804,3 +816,351 @@ |
289 | E.button(name="action_next", type="object", string="New button")), |
290 | string="Replacement title", version="7.0" |
291 | )) |
292 | + |
293 | +class ViewModeField(ViewCase): |
294 | + """ |
295 | + This should probably, eventually, be folded back into other test case |
296 | + classes, integrating the test (or not) of the mode field to regular cases |
297 | + """ |
298 | + |
299 | + def testModeImplicitValue(self): |
300 | + """ mode is auto-generated from inherit_id: |
301 | + * inherit_id -> mode=extension |
302 | + * not inherit_id -> mode=primary |
303 | + """ |
304 | + view = self.browse(self.create({ |
305 | + 'inherit_id': None, |
306 | + 'arch': '<qweb/>' |
307 | + })) |
308 | + self.assertEqual(view.mode, 'primary') |
309 | + |
310 | + view2 = self.browse(self.create({ |
311 | + 'inherit_id': view.id, |
312 | + 'arch': '<qweb/>' |
313 | + })) |
314 | + self.assertEqual(view2.mode, 'extension') |
315 | + |
316 | + @openerp.tools.mute_logger('openerp.sql_db') |
317 | + def testModeExplicit(self): |
318 | + view = self.browse(self.create({ |
319 | + 'inherit_id': None, |
320 | + 'arch': '<qweb/>' |
321 | + })) |
322 | + view2 = self.browse(self.create({ |
323 | + 'inherit_id': view.id, |
324 | + 'mode': 'primary', |
325 | + 'arch': '<qweb/>' |
326 | + })) |
327 | + self.assertEqual(view.mode, 'primary') |
328 | + |
329 | + with self.assertRaises(IntegrityError): |
330 | + self.create({ |
331 | + 'inherit_id': None, |
332 | + 'mode': 'extension', |
333 | + 'arch': '<qweb/>' |
334 | + }) |
335 | + |
336 | + @openerp.tools.mute_logger('openerp.sql_db') |
337 | + def testPurePrimaryToExtension(self): |
338 | + """ |
339 | + A primary view with inherit_id=None can't be converted to extension |
340 | + """ |
341 | + view_pure_primary = self.browse(self.create({ |
342 | + 'inherit_id': None, |
343 | + 'arch': '<qweb/>' |
344 | + })) |
345 | + with self.assertRaises(IntegrityError): |
346 | + view_pure_primary.write({'mode': 'extension'}) |
347 | + |
348 | + def testInheritPrimaryToExtension(self): |
349 | + """ |
350 | + A primary view with an inherit_id can be converted to extension |
351 | + """ |
352 | + base = self.create({'inherit_id': None, 'arch': '<qweb/>'}) |
353 | + view = self.browse(self.create({ |
354 | + 'inherit_id': base, |
355 | + 'mode': 'primary', |
356 | + 'arch': '<qweb/>' |
357 | + })) |
358 | + |
359 | + view.write({'mode': 'extension'}) |
360 | + |
361 | + def testDefaultExtensionToPrimary(self): |
362 | + """ |
363 | + An extension view can be converted to primary |
364 | + """ |
365 | + base = self.create({'inherit_id': None, 'arch': '<qweb/>'}) |
366 | + view = self.browse(self.create({ |
367 | + 'inherit_id': base, |
368 | + 'arch': '<qweb/>' |
369 | + })) |
370 | + |
371 | + view.write({'mode': 'primary'}) |
372 | + |
373 | +class TestDefaultView(ViewCase): |
374 | + def testDefaultViewBase(self): |
375 | + self.create({ |
376 | + 'inherit_id': False, |
377 | + 'priority': 10, |
378 | + 'mode': 'primary', |
379 | + 'arch': '<qweb/>', |
380 | + }) |
381 | + v2 = self.create({ |
382 | + 'inherit_id': False, |
383 | + 'priority': 1, |
384 | + 'mode': 'primary', |
385 | + 'arch': '<qweb/>', |
386 | + }) |
387 | + |
388 | + default = self.Views.default_view(self.cr, self.uid, False, 'qweb') |
389 | + self.assertEqual( |
390 | + default, v2, |
391 | + "default_view should get the view with the lowest priority for " |
392 | + "a (model, view_type) pair" |
393 | + ) |
394 | + |
395 | + def testDefaultViewPrimary(self): |
396 | + v1 = self.create({ |
397 | + 'inherit_id': False, |
398 | + 'priority': 10, |
399 | + 'mode': 'primary', |
400 | + 'arch': '<qweb/>', |
401 | + }) |
402 | + self.create({ |
403 | + 'inherit_id': False, |
404 | + 'priority': 5, |
405 | + 'mode': 'primary', |
406 | + 'arch': '<qweb/>', |
407 | + }) |
408 | + v3 = self.create({ |
409 | + 'inherit_id': v1, |
410 | + 'priority': 1, |
411 | + 'mode': 'primary', |
412 | + 'arch': '<qweb/>', |
413 | + }) |
414 | + |
415 | + default = self.Views.default_view(self.cr, self.uid, False, 'qweb') |
416 | + self.assertEqual( |
417 | + default, v3, |
418 | + "default_view should get the view with the lowest priority for " |
419 | + "a (model, view_type) pair in all the primary tables" |
420 | + ) |
421 | + |
422 | +class TestViewCombined(ViewCase): |
423 | + """ |
424 | + * When asked for a view, instead of looking for the closest parent with |
425 | + inherit_id=False look for mode=primary |
426 | + * If root.inherit_id, resolve the arch for root.inherit_id (?using which |
427 | + model?), then apply root's inheritance specs to it |
428 | + * Apply inheriting views on top |
429 | + """ |
430 | + |
431 | + def setUp(self): |
432 | + super(TestViewCombined, self).setUp() |
433 | + |
434 | + self.a1 = self.create({ |
435 | + 'model': 'a', |
436 | + 'arch': '<qweb><a1/></qweb>' |
437 | + }) |
438 | + self.a2 = self.create({ |
439 | + 'model': 'a', |
440 | + 'inherit_id': self.a1, |
441 | + 'priority': 5, |
442 | + 'arch': '<xpath expr="//a1" position="after"><a2/></xpath>' |
443 | + }) |
444 | + self.a3 = self.create({ |
445 | + 'model': 'a', |
446 | + 'inherit_id': self.a1, |
447 | + 'arch': '<xpath expr="//a1" position="after"><a3/></xpath>' |
448 | + }) |
449 | + # mode=primary should be an inheritance boundary in both direction, |
450 | + # even within a model it should not extend the parent |
451 | + self.a4 = self.create({ |
452 | + 'model': 'a', |
453 | + 'inherit_id': self.a1, |
454 | + 'mode': 'primary', |
455 | + 'arch': '<xpath expr="//a1" position="after"><a4/></xpath>', |
456 | + }) |
457 | + |
458 | + self.b1 = self.create({ |
459 | + 'model': 'b', |
460 | + 'inherit_id': self.a3, |
461 | + 'mode': 'primary', |
462 | + 'arch': '<xpath expr="//a1" position="after"><b1/></xpath>' |
463 | + }) |
464 | + self.b2 = self.create({ |
465 | + 'model': 'b', |
466 | + 'inherit_id': self.b1, |
467 | + 'arch': '<xpath expr="//a1" position="after"><b2/></xpath>' |
468 | + }) |
469 | + |
470 | + self.c1 = self.create({ |
471 | + 'model': 'c', |
472 | + 'inherit_id': self.a1, |
473 | + 'mode': 'primary', |
474 | + 'arch': '<xpath expr="//a1" position="after"><c1/></xpath>' |
475 | + }) |
476 | + self.c2 = self.create({ |
477 | + 'model': 'c', |
478 | + 'inherit_id': self.c1, |
479 | + 'priority': 5, |
480 | + 'arch': '<xpath expr="//a1" position="after"><c2/></xpath>' |
481 | + }) |
482 | + self.c3 = self.create({ |
483 | + 'model': 'c', |
484 | + 'inherit_id': self.c2, |
485 | + 'priority': 10, |
486 | + 'arch': '<xpath expr="//a1" position="after"><c3/></xpath>' |
487 | + }) |
488 | + |
489 | + self.d1 = self.create({ |
490 | + 'model': 'd', |
491 | + 'inherit_id': self.b1, |
492 | + 'mode': 'primary', |
493 | + 'arch': '<xpath expr="//a1" position="after"><d1/></xpath>' |
494 | + }) |
495 | + |
496 | + def test_basic_read(self): |
497 | + arch = self.read_combined(self.a1)['arch'] |
498 | + self.assertEqual( |
499 | + ET.fromstring(arch), |
500 | + E.qweb( |
501 | + E.a1(), |
502 | + E.a3(), |
503 | + E.a2(), |
504 | + ), arch) |
505 | + |
506 | + def test_read_from_child(self): |
507 | + arch = self.read_combined(self.a3)['arch'] |
508 | + self.assertEqual( |
509 | + ET.fromstring(arch), |
510 | + E.qweb( |
511 | + E.a1(), |
512 | + E.a3(), |
513 | + E.a2(), |
514 | + ), arch) |
515 | + |
516 | + def test_read_from_child_primary(self): |
517 | + arch = self.read_combined(self.a4)['arch'] |
518 | + self.assertEqual( |
519 | + ET.fromstring(arch), |
520 | + E.qweb( |
521 | + E.a1(), |
522 | + E.a4(), |
523 | + E.a3(), |
524 | + E.a2(), |
525 | + ), arch) |
526 | + |
527 | + def test_cross_model_simple(self): |
528 | + arch = self.read_combined(self.c2)['arch'] |
529 | + self.assertEqual( |
530 | + ET.fromstring(arch), |
531 | + E.qweb( |
532 | + E.a1(), |
533 | + E.c3(), |
534 | + E.c2(), |
535 | + E.c1(), |
536 | + E.a3(), |
537 | + E.a2(), |
538 | + ), arch) |
539 | + |
540 | + def test_cross_model_double(self): |
541 | + arch = self.read_combined(self.d1)['arch'] |
542 | + self.assertEqual( |
543 | + ET.fromstring(arch), |
544 | + E.qweb( |
545 | + E.a1(), |
546 | + E.d1(), |
547 | + E.b2(), |
548 | + E.b1(), |
549 | + E.a3(), |
550 | + E.a2(), |
551 | + ), arch) |
552 | + |
553 | +class TestOptionalViews(ViewCase): |
554 | + """ |
555 | + Tests ability to enable/disable inherited views, formerly known as |
556 | + inherit_option_id |
557 | + """ |
558 | + |
559 | + def setUp(self): |
560 | + super(TestOptionalViews, self).setUp() |
561 | + self.v0 = self.create({ |
562 | + 'model': 'a', |
563 | + 'arch': '<qweb><base/></qweb>', |
564 | + }) |
565 | + self.v1 = self.create({ |
566 | + 'model': 'a', |
567 | + 'inherit_id': self.v0, |
568 | + 'application': 'always', |
569 | + 'priority': 10, |
570 | + 'arch': '<xpath expr="//base" position="after"><v1/></xpath>', |
571 | + }) |
572 | + self.v2 = self.create({ |
573 | + 'model': 'a', |
574 | + 'inherit_id': self.v0, |
575 | + 'application': 'enabled', |
576 | + 'priority': 9, |
577 | + 'arch': '<xpath expr="//base" position="after"><v2/></xpath>', |
578 | + }) |
579 | + self.v3 = self.create({ |
580 | + 'model': 'a', |
581 | + 'inherit_id': self.v0, |
582 | + 'application': 'disabled', |
583 | + 'priority': 8, |
584 | + 'arch': '<xpath expr="//base" position="after"><v3/></xpath>' |
585 | + }) |
586 | + |
587 | + def test_applied(self): |
588 | + """ mandatory and enabled views should be applied |
589 | + """ |
590 | + arch = self.read_combined(self.v0)['arch'] |
591 | + self.assertEqual( |
592 | + ET.fromstring(arch), |
593 | + E.qweb( |
594 | + E.base(), |
595 | + E.v1(), |
596 | + E.v2(), |
597 | + ) |
598 | + ) |
599 | + |
600 | + def test_applied_state_toggle(self): |
601 | + """ Change application states of v2 and v3, check that the results |
602 | + are as expected |
603 | + """ |
604 | + self.browse(self.v2).write({'application': 'disabled'}) |
605 | + arch = self.read_combined(self.v0)['arch'] |
606 | + self.assertEqual( |
607 | + ET.fromstring(arch), |
608 | + E.qweb( |
609 | + E.base(), |
610 | + E.v1(), |
611 | + ) |
612 | + ) |
613 | + |
614 | + self.browse(self.v3).write({'application': 'enabled'}) |
615 | + arch = self.read_combined(self.v0)['arch'] |
616 | + self.assertEqual( |
617 | + ET.fromstring(arch), |
618 | + E.qweb( |
619 | + E.base(), |
620 | + E.v1(), |
621 | + E.v3(), |
622 | + ) |
623 | + ) |
624 | + |
625 | + self.browse(self.v2).write({'application': 'enabled'}) |
626 | + arch = self.read_combined(self.v0)['arch'] |
627 | + self.assertEqual( |
628 | + ET.fromstring(arch), |
629 | + E.qweb( |
630 | + E.base(), |
631 | + E.v1(), |
632 | + E.v2(), |
633 | + E.v3(), |
634 | + ) |
635 | + ) |
636 | + |
637 | + def test_mandatory_no_disabled(self): |
638 | + with self.assertRaises(Exception): |
639 | + self.browse(self.v1).write({'application': 'disabled'}) |
640 | |
641 | === modified file 'openerp/import_xml.rng' |
642 | --- openerp/import_xml.rng 2014-01-16 09:17:16 +0000 |
643 | +++ openerp/import_xml.rng 2014-05-06 14:57:36 +0000 |
644 | @@ -217,9 +217,23 @@ |
645 | <rng:optional><rng:attribute name="priority"/></rng:optional> |
646 | <rng:choice> |
647 | <rng:group> |
648 | - <rng:optional><rng:attribute name="inherit_id"/></rng:optional> |
649 | - <rng:optional><rng:attribute name="inherit_option_id"/></rng:optional> |
650 | + <rng:optional> |
651 | + <rng:attribute name="inherit_id"/> |
652 | + <rng:optional> |
653 | + <rng:attribute name="primary"> |
654 | + <rng:value>True</rng:value> |
655 | + </rng:attribute> |
656 | + </rng:optional> |
657 | + </rng:optional> |
658 | <rng:optional><rng:attribute name="groups"/></rng:optional> |
659 | + <rng:optional> |
660 | + <rng:attribute name="optional"> |
661 | + <rng:choice> |
662 | + <rng:value>enabled</rng:value> |
663 | + <rng:value>disabled</rng:value> |
664 | + </rng:choice> |
665 | + </rng:attribute> |
666 | + </rng:optional> |
667 | </rng:group> |
668 | <rng:optional> |
669 | <rng:attribute name="page"><rng:value>True</rng:value></rng:attribute> |
670 | |
671 | === modified file 'openerp/osv/orm.py' |
672 | --- openerp/osv/orm.py 2014-04-16 14:34:31 +0000 |
673 | +++ openerp/osv/orm.py 2014-05-06 14:57:36 +0000 |
674 | @@ -729,7 +729,6 @@ |
675 | _all_columns = {} |
676 | |
677 | _table = None |
678 | - _invalids = set() |
679 | _log_create = False |
680 | _sql_constraints = [] |
681 | _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'] |
682 | @@ -1543,9 +1542,6 @@ |
683 | |
684 | yield dbid, xid, converted, dict(extras, record=stream.index) |
685 | |
686 | - def get_invalid_fields(self, cr, uid): |
687 | - return list(self._invalids) |
688 | - |
689 | def _validate(self, cr, uid, ids, context=None): |
690 | context = context or {} |
691 | lng = context.get('lang') |
692 | @@ -1566,12 +1562,9 @@ |
693 | # Check presence of __call__ directly instead of using |
694 | # callable() because it will be deprecated as of Python 3.0 |
695 | if hasattr(msg, '__call__'): |
696 | - tmp_msg = msg(self, cr, uid, ids, context=context) |
697 | - if isinstance(tmp_msg, tuple): |
698 | - tmp_msg, params = tmp_msg |
699 | - translated_msg = tmp_msg % params |
700 | - else: |
701 | - translated_msg = tmp_msg |
702 | + translated_msg = msg(self, cr, uid, ids, context=context) |
703 | + if isinstance(translated_msg, tuple): |
704 | + translated_msg = translated_msg[0] % translated_msg[1] |
705 | else: |
706 | translated_msg = trans._get_source(cr, uid, self._name, 'constraint', lng, msg) |
707 | if extra_error: |
708 | @@ -1579,11 +1572,8 @@ |
709 | error_msgs.append( |
710 | _("The field(s) `%s` failed against a constraint: %s") % (', '.join(fields), translated_msg) |
711 | ) |
712 | - self._invalids.update(fields) |
713 | if error_msgs: |
714 | raise except_orm('ValidateError', '\n'.join(error_msgs)) |
715 | - else: |
716 | - self._invalids.clear() |
717 | |
718 | def default_get(self, cr, uid, fields_list, context=None): |
719 | """ |
720 | |
721 | === modified file 'openerp/tools/convert.py' |
722 | --- openerp/tools/convert.py 2014-04-24 13:14:05 +0000 |
723 | +++ openerp/tools/convert.py 2014-05-06 14:57:36 +0000 |
724 | @@ -861,7 +861,7 @@ |
725 | if '.' not in full_tpl_id: |
726 | full_tpl_id = '%s.%s' % (self.module, tpl_id) |
727 | # set the full template name for qweb <module>.<id> |
728 | - if not (el.get('inherit_id') or el.get('inherit_option_id')): |
729 | + if not el.get('inherit_id'): |
730 | el.set('t-name', full_tpl_id) |
731 | el.tag = 't' |
732 | else: |
733 | @@ -884,15 +884,18 @@ |
734 | record.append(Field("qweb", name='type')) |
735 | record.append(Field(el.get('priority', "16"), name='priority')) |
736 | record.append(Field(el, name="arch", type="xml")) |
737 | - for field_name in ('inherit_id','inherit_option_id'): |
738 | - value = el.attrib.pop(field_name, None) |
739 | - if value: record.append(Field(name=field_name, ref=value)) |
740 | + if 'inherit_id' in el.attrib: |
741 | + record.append(Field(name='inherit_id', ref=el.get('inherit_id'))) |
742 | groups = el.attrib.pop('groups', None) |
743 | if groups: |
744 | grp_lst = map(lambda x: "ref('%s')" % x, groups.split(',')) |
745 | record.append(Field(name="groups_id", eval="[(6, 0, ["+', '.join(grp_lst)+"])]")) |
746 | if el.attrib.pop('page', None) == 'True': |
747 | record.append(Field(name="page", eval="True")) |
748 | + if el.get('primary') == 'True': |
749 | + record.append(Field('primary', name='mode')) |
750 | + if el.get('optional'): |
751 | + record.append(Field(el.get('optional'), name='application')) |
752 | |
753 | return self._tag_record(cr, record, data_node) |
754 |