Merge lp:~openerp-dev/openobject-server/trunk-import-parsers-refactor-thu into lp:openobject-server

Proposed by Vo Minh Thu
Status: Work in progress
Proposed branch: lp:~openerp-dev/openobject-server/trunk-import-parsers-refactor-thu
Merge into: lp:openobject-server
Diff against target: 853 lines (+840/-0)
2 files modified
openerp/tools/convert.py (+16/-0)
parser_xml.py (+824/-0)
To merge this branch: bzr merge lp:~openerp-dev/openobject-server/trunk-import-parsers-refactor-thu
Reviewer Review Type Date Requested Status
OpenERP Core Team Pending
Review via email: mp+150525@code.launchpad.net
To post a comment you must log in.

Unmerged revisions

4761. By Vo Minh Thu

[IMP] Started to refactor openerp/tools/convert.

A parser_xml.py file has been added. It is a copy of refactor openerp/tools/convert
where the result of parsing is just a list of records ((string, dict) pairs for now).

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'openerp/tools/convert.py'
2--- openerp/tools/convert.py 2013-02-25 16:40:28 +0000
3+++ openerp/tools/convert.py 2013-02-26 11:04:52 +0000
4@@ -952,4 +952,20 @@
5 return True
6
7
8+if __name__ == '__main__':
9+ import sys
10+ import openerp
11+
12+ assert len(sys.argv) == 4
13+ database_name = sys.argv[1]
14+ module_name = sys.argv[2]
15+ filename = sys.argv[3]
16+
17+ xml = etree.parse(filename)
18+
19+ registry = openerp.modules.registry.RegistryManager.get(database_name)
20+ with registry.cursor() as cr:
21+ xml_import(cr, 'base', {}, 'init', None, False).parse(xml.getroot())
22+
23+
24 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
25
26=== added file 'parser_xml.py'
27--- parser_xml.py 1970-01-01 00:00:00 +0000
28+++ parser_xml.py 2013-02-26 11:04:52 +0000
29@@ -0,0 +1,824 @@
30+# -*- coding: utf-8 -*-
31+##############################################################################
32+#
33+# OpenERP, Open Source Management Solution
34+# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
35+#
36+# This program is free software: you can redistribute it and/or modify
37+# it under the terms of the GNU Affero General Public License as
38+# published by the Free Software Foundation, either version 3 of the
39+# License, or (at your option) any later version.
40+#
41+# This program is distributed in the hope that it will be useful,
42+# but WITHOUT ANY WARRANTY; without even the implied warranty of
43+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
44+# GNU Affero General Public License for more details.
45+#
46+# You should have received a copy of the GNU Affero General Public License
47+# along with this program. If not, see <http://www.gnu.org/licenses/>.
48+#
49+##############################################################################
50+
51+import cStringIO
52+import csv
53+import logging
54+import os.path
55+import pickle
56+import re
57+
58+# for eval context:
59+import time
60+import openerp.release as release
61+
62+_logger = logging.getLogger(__name__)
63+
64+try:
65+ import pytz
66+except:
67+ _logger.warning('could not find pytz library, please install it')
68+ class pytzclass(object):
69+ all_timezones=[]
70+ pytz=pytzclass()
71+
72+
73+from datetime import datetime, timedelta
74+from lxml import etree
75+
76+import openerp.tools.misc as misc
77+import openerp.loglevels as loglevels
78+import openerp.pooler as pooler
79+
80+# List of etree._Element subclasses that we choose to ignore when parsing XML.
81+from openerp.tools.misc import SKIPPED_ELEMENT_TYPES
82+
83+from openerp.tools.misc import unquote
84+
85+# Import of XML records requires the unsafe eval as well,
86+# almost everywhere, which is ok because it supposedly comes
87+# from trusted data, but at least we make it obvious now.
88+unsafe_eval = eval
89+from openerp.tools.safe_eval import safe_eval as eval
90+
91+class ConvertError(Exception):
92+ def __init__(self, doc, orig_excpt):
93+ self.d = doc
94+ self.orig = orig_excpt
95+
96+ def __str__(self):
97+ return 'Exception:\n\t%s\nUsing file:\n%s' % (self.orig, self.d)
98+
99+def _ref(self, cr):
100+ return lambda x: self.id_get(cr, x)
101+
102+def _obj(pool, cr, uid, model_str, context=None):
103+ model = pool.get(model_str)
104+ return lambda x: model.browse(cr, uid, x, context=context)
105+
106+def _get_idref(self, cr, uid, model_str, context, idref):
107+ idref2 = dict(idref,
108+ time=time,
109+ DateTime=datetime,
110+ timedelta=timedelta,
111+ version=release.major_version,
112+ ref=_ref(self, cr),
113+ pytz=pytz)
114+ if len(model_str):
115+ idref2['obj'] = _obj(self.pool, cr, uid, model_str, context=context)
116+ return idref2
117+
118+def _fix_multiple_roots(node):
119+ """
120+ Surround the children of the ``node`` element of an XML field with a
121+ single root "data" element, to prevent having a document with multiple
122+ roots once parsed separately.
123+
124+ XML nodes should have one root only, but we'd like to support
125+ direct multiple roots in our partial documents (like inherited view architectures).
126+ As a convention we'll surround multiple root with a container "data" element, to be
127+ ignored later when parsing.
128+ """
129+ real_nodes = [x for x in node if not isinstance(x, SKIPPED_ELEMENT_TYPES)]
130+ if len(real_nodes) > 1:
131+ data_node = etree.Element("data")
132+ for child in node:
133+ data_node.append(child)
134+ node.append(data_node)
135+
136+def node_content(node):
137+ """Implement a little hack"""
138+ if node.get('type', 'char') == 'xml':
139+ _fix_multiple_roots(node)
140+ return '<?xml version="1.0"?>\n'\
141+ + "".join([etree.tostring(n, encoding='utf-8') for n in node])
142+ else:
143+ return node.text
144+
145+def _eval_xml(self, node, pool, cr, uid, idref, context=None):
146+ if context is None:
147+ context = {}
148+ if node.tag in ('field','value'):
149+ t = node.get('type','char')
150+ f_model = node.get('model', '').encode('utf-8')
151+ if node.get('search'):
152+ f_search = node.get("search",'').encode('utf-8')
153+ f_use = node.get("use",'id').encode('utf-8')
154+ f_name = node.get("name",'').encode('utf-8')
155+ idref2 = {}
156+ if f_search:
157+ idref2 = _get_idref(self, cr, uid, f_model, context, idref)
158+ q = unsafe_eval(f_search, idref2)
159+ ids = pool.get(f_model).search(cr, uid, q)
160+ if f_use != 'id':
161+ ids = map(lambda x: x[f_use], pool.get(f_model).read(cr, uid, ids, [f_use]))
162+ _cols = pool.get(f_model)._columns
163+ if (f_name in _cols) and _cols[f_name]._type=='many2many':
164+ return ids
165+ f_val = False
166+ if len(ids):
167+ f_val = ids[0]
168+ if isinstance(f_val, tuple):
169+ f_val = f_val[0]
170+ return f_val
171+ a_eval = node.get('eval','')
172+ idref2 = {}
173+ if a_eval:
174+ idref2 = _get_idref(self, cr, uid, f_model, context, idref)
175+ try:
176+ return unsafe_eval(a_eval, idref2)
177+ except Exception:
178+ _logger.warning('could not eval(%s) for %s in %s' % (a_eval, node.get('name'), context), exc_info=True)
179+ return ""
180+ def _process(s, idref):
181+ m = re.findall('[^%]%\((.*?)\)[ds]', s)
182+ for id in m:
183+ if not id in idref:
184+ idref[id]=self.id_get(cr, id)
185+ return s % idref
186+ if t == 'xml':
187+ _fix_multiple_roots(node)
188+ return '<?xml version="1.0"?>\n'\
189+ +_process("".join([etree.tostring(n, encoding='utf-8')
190+ for n in node]), idref)
191+ if t == 'html':
192+ return _process("".join([etree.tostring(n, encoding='utf-8')
193+ for n in node]), idref)
194+ if t in ('char', 'int', 'float'):
195+ d = node.text
196+ if t == 'int':
197+ d = d.strip()
198+ if d == 'None':
199+ return None
200+ else:
201+ return int(d.strip())
202+ elif t == 'float':
203+ return float(d.strip())
204+ return d
205+ elif t in ('list','tuple'):
206+ res=[]
207+ for n in node.findall('./value'):
208+ res.append(_eval_xml(self,n,pool,cr,uid,idref))
209+ if t=='tuple':
210+ return tuple(res)
211+ return res
212+ elif node.tag == "getitem":
213+ for n in node:
214+ res=_eval_xml(self,n,pool,cr,uid,idref)
215+ if not res:
216+ raise LookupError
217+ elif node.get('type') in ("int", "list"):
218+ return res[int(node.get('index'))]
219+ else:
220+ return res[node.get('index','').encode("utf8")]
221+ elif node.tag == "function":
222+ args = []
223+ a_eval = node.get('eval','')
224+ if a_eval:
225+ idref['ref'] = lambda x: self.id_get(cr, x)
226+ args = unsafe_eval(a_eval, idref)
227+ for n in node:
228+ return_val = _eval_xml(self,n, pool, cr, uid, idref, context)
229+ if return_val is not None:
230+ args.append(return_val)
231+ model = pool.get(node.get('model',''))
232+ method = node.get('name','')
233+ res = getattr(model, method)(cr, uid, *args)
234+ return res
235+ elif node.tag == "test":
236+ return node.text
237+
238+escape_re = re.compile(r'(?<!\\)/')
239+def escape(x):
240+ return x.replace('\\/', '/')
241+
242+class parser(object):
243+ @staticmethod
244+ def nodeattr2bool(node, attr, default=False):
245+ val = node.get(attr, '').strip().lower()
246+ if not val:
247+ return default
248+ if val in ('0', 'false', 'off'):
249+ return False
250+ if val in ('1', 'true', 'on'):
251+ return True
252+ print "Oh noes"
253+ return True
254+
255+ def isnoupdate(self, data_node):
256+ # TODO is taking the lenght of data_node necessary?
257+ return self.noupdate or (len(data_node) and self.nodeattr2bool(data_node, 'noupdate', False))
258+
259+ def get_context(self, data_node, node, eval_dict):
260+ data_node_context = (len(data_node) and data_node.get('context','').encode('utf8'))
261+ node_context = node.get("context",'').encode('utf8')
262+ context = {}
263+ for ctx in (data_node_context, node_context):
264+ if ctx:
265+ try:
266+ ctx_res = unsafe_eval(ctx, eval_dict)
267+ if isinstance(context, dict):
268+ context.update(ctx_res)
269+ else:
270+ context = ctx_res
271+ except NameError:
272+ # TODO is this still true ?
273+ # Some contexts contain references that are only valid at runtime at
274+ # client-side, so in that case we keep the original context string
275+ # as it is. We also log it, just in case.
276+ context = ctx
277+ _logger.debug('Context value (%s) for element with id "%s" or its data node does not parse '\
278+ 'at server-side, keeping original string, in case it\'s meant for client side only',
279+ ctx, node.get('id','n/a'), exc_info=True)
280+ return context
281+
282+ def get_uid(self, cr, uid, data_node, node):
283+ node_uid = node.get('uid','') or (len(data_node) and data_node.get('uid',''))
284+ if node_uid:
285+ return self.id_get(cr, node_uid)
286+ return uid
287+
288+ def _test_xml_id(self, xml_id):
289+ id = xml_id
290+ if '.' in xml_id:
291+ module, id = xml_id.split('.', 1)
292+ assert '.' not in id, """The ID reference "%s" must contain
293+maximum one dot. They are used to refer to other modules ID, in the
294+form: module.record_id""" % (xml_id,)
295+ if module != self.module:
296+ modcnt = self.pool.get('ir.module.module').search_count(self.cr, self.uid, ['&', ('name', '=', module), ('state', 'in', ['installed'])])
297+ assert modcnt == 1, """The ID "%s" refers to an uninstalled module""" % (xml_id,)
298+
299+ if len(id) > 64:
300+ _logger.error('id: %s is to long (max: 64)', id)
301+
302+ def _tag_delete(self, cr, rec, data_node=None):
303+ d_model = rec.get("model",'')
304+ d_search = rec.get("search",'').encode('utf-8')
305+ d_id = rec.get("id",'')
306+ ids = []
307+
308+ if d_search:
309+ idref = _get_idref(self, cr, self.uid, d_model, context={}, idref={})
310+ ids = self.pool.get(d_model).search(cr, self.uid, unsafe_eval(d_search, idref))
311+ if d_id:
312+ try:
313+ ids.append(self.id_get(cr, d_id))
314+ except:
315+ # d_id cannot be found. doesn't matter in this case
316+ pass
317+ if ids:
318+ self.pool.get(d_model).unlink(cr, self.uid, ids)
319+
320+ def _remove_ir_values(self, cr, name, value, model):
321+ ir_values_obj = self.pool.get('ir.values')
322+ ir_value_ids = ir_values_obj.search(cr, self.uid, [('name','=',name),('value','=',value),('model','=',model)])
323+ if ir_value_ids:
324+ ir_values_obj.unlink(cr, self.uid, ir_value_ids)
325+
326+ return True
327+
328+ def _tag_report(self, cr, rec, data_node=None):
329+ res = {}
330+ for dest,f in (('name','string'),('model','model'),('report_name','name')):
331+ res[dest] = rec.get(f,'').encode('utf8')
332+ assert res[dest], "Attribute %s of report is empty !" % (f,)
333+ for field,dest in (('rml','report_rml'),('file','report_rml'),('xml','report_xml'),('xsl','report_xsl'),
334+ ('attachment','attachment'),('attachment_use','attachment_use'), ('usage','usage')):
335+ if rec.get(field):
336+ res[dest] = rec.get(field).encode('utf8')
337+ if rec.get('auto'):
338+ res['auto'] = eval(rec.get('auto','False'))
339+ if rec.get('sxw'):
340+ sxw_content = misc.file_open(rec.get('sxw')).read()
341+ res['report_sxw_content'] = sxw_content
342+ if rec.get('header'):
343+ res['header'] = eval(rec.get('header','False'))
344+ if rec.get('report_type'):
345+ res['report_type'] = rec.get('report_type')
346+
347+ res['multi'] = rec.get('multi') and eval(rec.get('multi','False'))
348+
349+ xml_id = rec.get('id','').encode('utf8')
350+ self._test_xml_id(xml_id)
351+
352+ if rec.get('groups'):
353+ g_names = rec.get('groups','').split(',')
354+ groups_value = []
355+ for group in g_names:
356+ if group.startswith('-'):
357+ group_id = self.id_get(cr, group[1:])
358+ groups_value.append((3, group_id))
359+ else:
360+ group_id = self.id_get(cr, group)
361+ groups_value.append((4, group_id))
362+ res['groups_id'] = groups_value
363+
364+ id = self.pool.get('ir.model.data')._update(cr, self.uid, "ir.actions.report.xml", self.module, res, xml_id, noupdate=self.isnoupdate(data_node), mode=self.mode)
365+ self.idref[xml_id] = int(id)
366+
367+ if not rec.get('menu') or eval(rec.get('menu','False')):
368+ keyword = str(rec.get('keyword', 'client_print_multi'))
369+ value = 'ir.actions.report.xml,'+str(id)
370+ replace = rec.get('replace', True)
371+ self.pool.get('ir.model.data').ir_set(cr, self.uid, 'action', keyword, res['name'], [res['model']], value, replace=replace, isobject=True, xml_id=xml_id)
372+ elif self.mode=='update' and eval(rec.get('menu','False'))==False:
373+ # Special check for report having attribute menu=False on update
374+ value = 'ir.actions.report.xml,'+str(id)
375+ self._remove_ir_values(cr, res['name'], value, res['model'])
376+ return id
377+
378+ def _tag_function(self, rec):
379+ return ('TODO_TAG_FUNCTION', {})
380+
381+ def _tag_wizard(self, cr, rec, data_node=None):
382+ string = rec.get("string",'').encode('utf8')
383+ model = rec.get("model",'').encode('utf8')
384+ name = rec.get("name",'').encode('utf8')
385+ xml_id = rec.get('id','').encode('utf8')
386+ self._test_xml_id(xml_id)
387+ multi = rec.get('multi','') and eval(rec.get('multi','False'))
388+ res = {'name': string, 'wiz_name': name, 'multi': multi, 'model': model}
389+
390+ if rec.get('groups'):
391+ g_names = rec.get('groups','').split(',')
392+ groups_value = []
393+ for group in g_names:
394+ if group.startswith('-'):
395+ group_id = self.id_get(cr, group[1:])
396+ groups_value.append((3, group_id))
397+ else:
398+ group_id = self.id_get(cr, group)
399+ groups_value.append((4, group_id))
400+ res['groups_id'] = groups_value
401+
402+ id = self.pool.get('ir.model.data')._update(cr, self.uid, "ir.actions.wizard", self.module, res, xml_id, noupdate=self.isnoupdate(data_node), mode=self.mode)
403+ self.idref[xml_id] = int(id)
404+ # ir_set
405+ if (not rec.get('menu') or eval(rec.get('menu','False'))) and id:
406+ keyword = str(rec.get('keyword','') or 'client_action_multi')
407+ value = 'ir.actions.wizard,'+str(id)
408+ replace = rec.get("replace",'') or True
409+ self.pool.get('ir.model.data').ir_set(cr, self.uid, 'action', keyword, string, [model], value, replace=replace, isobject=True, xml_id=xml_id)
410+ elif self.mode=='update' and (rec.get('menu') and eval(rec.get('menu','False'))==False):
411+ # Special check for wizard having attribute menu=False on update
412+ value = 'ir.actions.wizard,'+str(id)
413+ self._remove_ir_values(cr, string, value, model)
414+
415+ def _tag_url(self, cr, rec, data_node=None):
416+ url = rec.get("url",'').encode('utf8')
417+ target = rec.get("target",'').encode('utf8')
418+ name = rec.get("name",'').encode('utf8')
419+ xml_id = rec.get('id','').encode('utf8')
420+ self._test_xml_id(xml_id)
421+
422+ res = {'name': name, 'url': url, 'target':target}
423+
424+ id = self.pool.get('ir.model.data')._update(cr, self.uid, "ir.actions.url", self.module, res, xml_id, noupdate=self.isnoupdate(data_node), mode=self.mode)
425+ self.idref[xml_id] = int(id)
426+ # ir_set
427+ if (not rec.get('menu') or eval(rec.get('menu','False'))) and id:
428+ keyword = str(rec.get('keyword','') or 'client_action_multi')
429+ value = 'ir.actions.url,'+str(id)
430+ replace = rec.get("replace",'') or True
431+ self.pool.get('ir.model.data').ir_set(cr, self.uid, 'action', keyword, url, ["ir.actions.url"], value, replace=replace, isobject=True, xml_id=xml_id)
432+ elif self.mode=='update' and (rec.get('menu') and eval(rec.get('menu','False'))==False):
433+ # Special check for URL having attribute menu=False on update
434+ value = 'ir.actions.url,'+str(id)
435+ self._remove_ir_values(cr, url, value, "ir.actions.url")
436+
437+ def _tag_act_window(self, cr, rec, data_node=None):
438+ name = rec.get('name','').encode('utf-8')
439+ xml_id = rec.get('id','').encode('utf8')
440+ self._test_xml_id(xml_id)
441+ type = rec.get('type','').encode('utf-8') or 'ir.actions.act_window'
442+ view_id = False
443+ if rec.get('view_id'):
444+ view_id = self.id_get(cr, rec.get('view_id','').encode('utf-8'))
445+ domain = rec.get('domain','').encode('utf-8') or '[]'
446+ res_model = rec.get('res_model','').encode('utf-8')
447+ src_model = rec.get('src_model','').encode('utf-8')
448+ view_type = rec.get('view_type','').encode('utf-8') or 'form'
449+ view_mode = rec.get('view_mode','').encode('utf-8') or 'tree,form'
450+ usage = rec.get('usage','').encode('utf-8')
451+ limit = rec.get('limit','').encode('utf-8')
452+ auto_refresh = rec.get('auto_refresh','').encode('utf-8')
453+ uid = self.uid
454+
455+ # Act_window's 'domain' and 'context' contain mostly literals
456+ # but they can also refer to the variables provided below
457+ # in eval_context, so we need to eval() them before storing.
458+ # Among the context variables, 'active_id' refers to
459+ # the currently selected items in a list view, and only
460+ # takes meaning at runtime on the client side. For this
461+ # reason it must remain a bare variable in domain and context,
462+ # even after eval() at server-side. We use the special 'unquote'
463+ # class to achieve this effect: a string which has itself, unquoted,
464+ # as representation.
465+ active_id = unquote("active_id")
466+ active_ids = unquote("active_ids")
467+ active_model = unquote("active_model")
468+
469+ def ref(str_id):
470+ return self.id_get(cr, str_id)
471+
472+ # Include all locals() in eval_context, for backwards compatibility
473+ eval_context = {
474+ 'name': name,
475+ 'xml_id': xml_id,
476+ 'type': type,
477+ 'view_id': view_id,
478+ 'domain': domain,
479+ 'res_model': res_model,
480+ 'src_model': src_model,
481+ 'view_type': view_type,
482+ 'view_mode': view_mode,
483+ 'usage': usage,
484+ 'limit': limit,
485+ 'auto_refresh': auto_refresh,
486+ 'uid' : uid,
487+ 'active_id': active_id,
488+ 'active_ids': active_ids,
489+ 'active_model': active_model,
490+ 'ref' : ref,
491+ }
492+ context = self.get_context(data_node, rec, eval_context)
493+
494+ try:
495+ domain = unsafe_eval(domain, eval_context)
496+ except NameError:
497+ # Some domains contain references that are only valid at runtime at
498+ # client-side, so in that case we keep the original domain string
499+ # as it is. We also log it, just in case.
500+ _logger.debug('Domain value (%s) for element with id "%s" does not parse '\
501+ 'at server-side, keeping original string, in case it\'s meant for client side only',
502+ domain, xml_id or 'n/a', exc_info=True)
503+ res = {
504+ 'name': name,
505+ 'type': type,
506+ 'view_id': view_id,
507+ 'domain': domain,
508+ 'context': context,
509+ 'res_model': res_model,
510+ 'src_model': src_model,
511+ 'view_type': view_type,
512+ 'view_mode': view_mode,
513+ 'usage': usage,
514+ 'limit': limit,
515+ 'auto_refresh': auto_refresh,
516+ }
517+
518+ if rec.get('groups'):
519+ g_names = rec.get('groups','').split(',')
520+ groups_value = []
521+ for group in g_names:
522+ if group.startswith('-'):
523+ group_id = self.id_get(cr, group[1:])
524+ groups_value.append((3, group_id))
525+ else:
526+ group_id = self.id_get(cr, group)
527+ groups_value.append((4, group_id))
528+ res['groups_id'] = groups_value
529+
530+ if rec.get('target'):
531+ res['target'] = rec.get('target','')
532+ if rec.get('multi'):
533+ res['multi'] = rec.get('multi', False)
534+ id = self.pool.get('ir.model.data')._update(cr, self.uid, 'ir.actions.act_window', self.module, res, xml_id, noupdate=self.isnoupdate(data_node), mode=self.mode)
535+ self.idref[xml_id] = int(id)
536+
537+ if src_model:
538+ #keyword = 'client_action_relate'
539+ keyword = rec.get('key2','').encode('utf-8') or 'client_action_relate'
540+ value = 'ir.actions.act_window,'+str(id)
541+ replace = rec.get('replace','') or True
542+ self.pool.get('ir.model.data').ir_set(cr, self.uid, 'action', keyword, xml_id, [src_model], value, replace=replace, isobject=True, xml_id=xml_id)
543+ # TODO add remove ir.model.data
544+
545+ def _tag_ir_set(self, cr, rec, data_node=None):
546+ if self.mode != 'init':
547+ return
548+ res = {}
549+ for field in rec.findall('./field'):
550+ f_name = field.get("name",'').encode('utf-8')
551+ f_val = _eval_xml(self,field,self.pool, cr, self.uid, self.idref)
552+ res[f_name] = f_val
553+ self.pool.get('ir.model.data').ir_set(cr, self.uid, res['key'], res['key2'], res['name'], res['models'], res['value'], replace=res.get('replace',True), isobject=res.get('isobject', False), meta=res.get('meta',None))
554+
555+ def _tag_workflow(self, cr, rec, data_node=None):
556+ if self.isnoupdate(data_node) and self.mode != 'init':
557+ return
558+ model = str(rec.get('model',''))
559+ w_ref = rec.get('ref','')
560+ if w_ref:
561+ id = self.id_get(cr, w_ref)
562+ else:
563+ number_children = len(rec)
564+ assert number_children > 0,\
565+ 'You must define a child node if you dont give a ref'
566+ assert number_children == 1,\
567+ 'Only one child node is accepted (%d given)' % number_children
568+ id = _eval_xml(self, rec[0], self.pool, cr, self.uid, self.idref)
569+
570+ uid = self.get_uid(cr, self.uid, data_node, rec)
571+ import openerp.netsvc as netsvc
572+ wf_service = netsvc.LocalService("workflow")
573+ wf_service.trg_validate(uid, model,
574+ id,
575+ str(rec.get('action','')), cr)
576+
577+ #
578+ # Support two types of notation:
579+ # name="Inventory Control/Sending Goods"
580+ # or
581+ # action="action_id"
582+ # parent="parent_id"
583+ #
584+ def _tag_menuitem(self, cr, rec, data_node=None):
585+ rec_id = rec.get("id",'').encode('ascii')
586+ self._test_xml_id(rec_id)
587+ m_l = map(escape, escape_re.split(rec.get("name",'').encode('utf8')))
588+
589+ values = {'parent_id': False}
590+ if rec.get('parent', False) is False and len(m_l) > 1:
591+ # No parent attribute specified and the menu name has several menu components,
592+ # try to determine the ID of the parent according to menu path
593+ pid = False
594+ res = None
595+ values['name'] = m_l[-1]
596+ m_l = m_l[:-1] # last part is our name, not a parent
597+ for idx, menu_elem in enumerate(m_l):
598+ if pid:
599+ cr.execute('select id from ir_ui_menu where parent_id=%s and name=%s', (pid, menu_elem))
600+ else:
601+ cr.execute('select id from ir_ui_menu where parent_id is null and name=%s', (menu_elem,))
602+ res = cr.fetchone()
603+ if res:
604+ pid = res[0]
605+ else:
606+ # the menuitem does't exist but we are in branch (not a leaf)
607+ _logger.warning('Warning no ID for submenu %s of menu %s !', menu_elem, str(m_l))
608+ pid = self.pool.get('ir.ui.menu').create(cr, self.uid, {'parent_id' : pid, 'name' : menu_elem})
609+ values['parent_id'] = pid
610+ else:
611+ # The parent attribute was specified, if non-empty determine its ID, otherwise
612+ # explicitly make a top-level menu
613+ if rec.get('parent'):
614+ menu_parent_id = self.id_get(cr, rec.get('parent',''))
615+ else:
616+ # we get here with <menuitem parent="">, explicit clear of parent, or
617+ # if no parent attribute at all but menu name is not a menu path
618+ menu_parent_id = False
619+ values = {'parent_id': menu_parent_id}
620+ if rec.get('name'):
621+ values['name'] = rec.get('name')
622+ try:
623+ res = [ self.id_get(cr, rec.get('id','')) ]
624+ except:
625+ res = None
626+
627+ if rec.get('action'):
628+ a_action = rec.get('action','').encode('utf8')
629+
630+ # determine the type of action
631+ a_type, a_id = self.model_id_get(cr, a_action)
632+ a_type = a_type.split('.')[-1] # keep only type part
633+
634+ icons = {
635+ "act_window": 'STOCK_NEW',
636+ "report.xml": 'STOCK_PASTE',
637+ "wizard": 'STOCK_EXECUTE',
638+ "url": 'STOCK_JUMP_TO',
639+ "client": 'STOCK_EXECUTE',
640+ "server": 'STOCK_EXECUTE',
641+ }
642+ values['icon'] = icons.get(a_type,'STOCK_NEW')
643+
644+ if a_type=='act_window':
645+ cr.execute('select view_type,view_mode,name,view_id,target from ir_act_window where id=%s', (int(a_id),))
646+ rrres = cr.fetchone()
647+ assert rrres, "No window action defined for this id %s !\n" \
648+ "Verify that this is a window action or add a type argument." % (a_action,)
649+ action_type,action_mode,action_name,view_id,target = rrres
650+ if view_id:
651+ cr.execute('SELECT arch FROM ir_ui_view WHERE id=%s', (int(view_id),))
652+ arch, = cr.fetchone()
653+ action_mode = etree.fromstring(arch.encode('utf8')).tag
654+ cr.execute('SELECT view_mode FROM ir_act_window_view WHERE act_window_id=%s ORDER BY sequence LIMIT 1', (int(a_id),))
655+ if cr.rowcount:
656+ action_mode, = cr.fetchone()
657+ if action_type=='tree':
658+ values['icon'] = 'STOCK_INDENT'
659+ elif action_mode and action_mode.startswith(('tree','kanban','gantt')):
660+ values['icon'] = 'STOCK_JUSTIFY_FILL'
661+ elif action_mode and action_mode.startswith('graph'):
662+ values['icon'] = 'terp-graph'
663+ elif action_mode and action_mode.startswith('calendar'):
664+ values['icon'] = 'terp-calendar'
665+ if target=='new':
666+ values['icon'] = 'STOCK_EXECUTE'
667+ if not values.get('name', False):
668+ values['name'] = action_name
669+
670+ elif a_type in ['wizard', 'url', 'client', 'server'] and not values.get('name'):
671+ a_table = 'ir_act_%s' % a_type
672+ cr.execute('select name from %s where id=%%s' % a_table, (int(a_id),))
673+ resw = cr.fetchone()
674+ if resw:
675+ values['name'] = resw[0]
676+
677+ if not values.get('name'):
678+ # ensure menu has a name
679+ values['name'] = rec_id or '?'
680+
681+ if rec.get('sequence'):
682+ values['sequence'] = int(rec.get('sequence'))
683+ if rec.get('icon'):
684+ values['icon'] = str(rec.get('icon'))
685+ if rec.get('web_icon'):
686+ values['web_icon'] = "%s,%s" %(self.module, str(rec.get('web_icon')))
687+ if rec.get('web_icon_hover'):
688+ values['web_icon_hover'] = "%s,%s" %(self.module, str(rec.get('web_icon_hover')))
689+
690+ if rec.get('groups'):
691+ g_names = rec.get('groups','').split(',')
692+ groups_value = []
693+ for group in g_names:
694+ if group.startswith('-'):
695+ group_id = self.id_get(cr, group[1:])
696+ groups_value.append((3, group_id))
697+ else:
698+ group_id = self.id_get(cr, group)
699+ groups_value.append((4, group_id))
700+ values['groups_id'] = groups_value
701+
702+ pid = self.pool.get('ir.model.data')._update(cr, self.uid, 'ir.ui.menu', self.module, values, rec_id, noupdate=self.isnoupdate(data_node), mode=self.mode, res_id=res and res[0] or False)
703+
704+ if rec_id and pid:
705+ self.idref[rec_id] = int(pid)
706+
707+ if rec.get('action') and pid:
708+ action = "ir.actions.%s,%d" % (a_type, a_id)
709+ self.pool.get('ir.model.data').ir_set(cr, self.uid, 'action', 'tree_but_open', 'Menuitem', [('ir.ui.menu', int(pid))], action, True, True, xml_id=rec_id)
710+ return ('ir.ui.menu', pid)
711+
712+ def _assert_equals(self, f1, f2, prec=4):
713+ return not round(f1 - f2, prec)
714+
715+ def _tag_assert(self, cr, rec, data_node=None):
716+ if self.isnoupdate(data_node) and self.mode != 'init':
717+ return
718+
719+ rec_model = rec.get("model",'').encode('ascii')
720+ model = self.pool.get(rec_model)
721+ assert model, "The model %s does not exist !" % (rec_model,)
722+ rec_id = rec.get("id",'').encode('ascii')
723+ self._test_xml_id(rec_id)
724+ rec_src = rec.get("search",'').encode('utf8')
725+ rec_src_count = rec.get("count")
726+
727+ rec_string = rec.get("string",'').encode('utf8') or 'unknown'
728+
729+ ids = None
730+ eval_dict = {'ref': _ref(self, cr)}
731+ context = self.get_context(data_node, rec, eval_dict)
732+ uid = self.get_uid(cr, self.uid, data_node, rec)
733+ if rec_id:
734+ ids = [self.id_get(cr, rec_id)]
735+ elif rec_src:
736+ q = unsafe_eval(rec_src, eval_dict)
737+ ids = self.pool.get(rec_model).search(cr, uid, q, context=context)
738+ if rec_src_count:
739+ count = int(rec_src_count)
740+ if len(ids) != count:
741+ self.assertion_report.record_failure()
742+ msg = 'assertion "%s" failed!\n' \
743+ ' Incorrect search count:\n' \
744+ ' expected count: %d\n' \
745+ ' obtained count: %d\n' \
746+ % (rec_string, count, len(ids))
747+ _logger.error(msg)
748+ return
749+
750+ assert ids is not None,\
751+ 'You must give either an id or a search criteria'
752+ ref = _ref(self, cr)
753+ for id in ids:
754+ brrec = model.browse(cr, uid, id, context)
755+ class d(dict):
756+ def __getitem__(self2, key):
757+ if key in brrec:
758+ return brrec[key]
759+ return dict.__getitem__(self2, key)
760+ globals_dict = d()
761+ globals_dict['floatEqual'] = self._assert_equals
762+ globals_dict['ref'] = ref
763+ globals_dict['_ref'] = ref
764+ for test in rec.findall('./test'):
765+ f_expr = test.get("expr",'').encode('utf-8')
766+ expected_value = _eval_xml(self, test, self.pool, cr, uid, self.idref, context=context) or True
767+ expression_value = unsafe_eval(f_expr, globals_dict)
768+ if expression_value != expected_value: # assertion failed
769+ self.assertion_report.record_failure()
770+ msg = 'assertion "%s" failed!\n' \
771+ ' xmltag: %s\n' \
772+ ' expected value: %r\n' \
773+ ' obtained value: %r\n' \
774+ % (rec_string, etree.tostring(test), expected_value, expression_value)
775+ _logger.error(msg)
776+ return
777+ else: # all tests were successful for this assertion tag (no break)
778+ self.assertion_report.record_success()
779+
780+ def _tag_record(self, rec):
781+ rec_model = rec.get('model').encode('ascii')
782+ rec_id = rec.get('id','').encode('ascii')
783+ rec_context = rec.get('context', None)
784+
785+ rec_fields = {}
786+ for field in rec.findall('./field'):
787+ field_name = field.get('name').encode('utf-8')
788+ rec_fields[field_name] = {}
789+ for x in ('ref', 'search', 'model', 'use', 'eval'):
790+ x_ = field.get(x,'').encode('utf-8')
791+ if x_: rec_fields[field_name][x] = x_
792+ if field.text is not None:
793+ rec_fields[field_name]['content'] = node_content(field)
794+
795+ return ('RECORD', {
796+ 'model': rec_model,
797+ 'xid': rec_id,
798+ 'context': rec_context,
799+ 'fields': rec_fields,
800+ })
801+
802+ def __iter__(self):
803+ assert self.xml.tag == 'openerp'
804+ _tags = {
805+ 'menuitem': self._tag_menuitem,
806+ 'record': self._tag_record,
807+ 'assert': self._tag_assert,
808+ 'report': self._tag_report,
809+ 'wizard': self._tag_wizard,
810+ 'delete': self._tag_delete,
811+ 'ir_set': self._tag_ir_set,
812+ 'function': self._tag_function,
813+ 'workflow': self._tag_workflow,
814+ 'act_window': self._tag_act_window,
815+ 'url': self._tag_url
816+ }
817+ for data in self.xml.findall('./data'):
818+ for element in data:
819+ if element.tag in _tags:
820+ yield _tags[element.tag](element)
821+ else:
822+ yield ('UNKNOWN', {'tag': element.tag})
823+
824+ def __init__(self, module_name, xml):
825+
826+ self.module_name = module_name
827+ self.xml = xml.getroot()
828+
829+def valid_xml(xml):
830+ schema_xml = etree.parse('openerp/import_xml.rng' )
831+ schema = etree.RelaxNG(schema_xml)
832+ try:
833+ schema.assert_(xml)
834+ except Exception:
835+ print "Can't validate the file `%s` against the RelaxNG schema." % filename
836+ return False
837+ else:
838+ return True
839+
840+if __name__ == '__main__':
841+ import sys
842+ assert len(sys.argv) == 3
843+ module_name = sys.argv[1]
844+ filename = sys.argv[2]
845+
846+ xml = etree.parse(filename)
847+ if not valid_xml(xml):
848+ sys.exit(1)
849+
850+ for x in parser(module_name, xml):
851+ print x
852+
853+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: