[IMP]:ValueError Message when running a YAML test that belong to a module test suite...
[odoo/odoo.git] / openerp / tools / yaml_import.py
1 # -*- encoding: utf-8 -*-
2 import types
3 import time # used to eval time.strftime expressions
4 from datetime import datetime, timedelta
5 import logging
6
7 import openerp.pooler as pooler
8 import misc
9 from config import config
10 import yaml_tag
11 import yaml
12
13 # YAML import needs both safe and unsafe eval, but let's
14 # default to /safe/.
15 unsafe_eval = eval
16 from safe_eval import safe_eval as eval
17
18 logger_channel = 'tests'
19
20 class YamlImportException(Exception):
21     pass
22
23 class YamlImportAbortion(Exception):
24     pass
25
26 def _is_yaml_mapping(node, tag_constructor):
27     value = isinstance(node, types.DictionaryType) \
28         and len(node.keys()) == 1 \
29         and isinstance(node.keys()[0], tag_constructor)
30     return value
31
32 def is_comment(node):
33     return isinstance(node, types.StringTypes)
34
35 def is_assert(node):
36     return isinstance(node, yaml_tag.Assert) \
37         or _is_yaml_mapping(node, yaml_tag.Assert)
38
39 def is_record(node):
40     return _is_yaml_mapping(node, yaml_tag.Record)
41
42 def is_python(node):
43     return _is_yaml_mapping(node, yaml_tag.Python)
44
45 def is_menuitem(node):
46     return isinstance(node, yaml_tag.Menuitem) \
47         or _is_yaml_mapping(node, yaml_tag.Menuitem)
48
49 def is_function(node):
50     return isinstance(node, yaml_tag.Function) \
51         or _is_yaml_mapping(node, yaml_tag.Function)
52
53 def is_report(node):
54     return isinstance(node, yaml_tag.Report)
55
56 def is_workflow(node):
57     return isinstance(node, yaml_tag.Workflow)
58
59 def is_act_window(node):
60     return isinstance(node, yaml_tag.ActWindow)
61
62 def is_delete(node):
63     return isinstance(node, yaml_tag.Delete)
64
65 def is_context(node):
66     return isinstance(node, yaml_tag.Context)
67
68 def is_url(node):
69     return isinstance(node, yaml_tag.Url)
70
71 def is_eval(node):
72     return isinstance(node, yaml_tag.Eval)
73
74 def is_ref(node):
75     return isinstance(node, yaml_tag.Ref) \
76         or _is_yaml_mapping(node, yaml_tag.Ref)
77
78 def is_ir_set(node):
79     return _is_yaml_mapping(node, yaml_tag.IrSet)
80
81 def is_string(node):
82     return isinstance(node, basestring)
83
84 class TestReport(object):
85     def __init__(self):
86         self._report = {}
87
88     def record(self, success, severity):
89         """
90         Records the result of an assertion for the failed/success count.
91         Returns success.
92         """
93         if severity in self._report:
94             self._report[severity][success] += 1
95         else:
96             self._report[severity] = {success: 1, not success: 0}
97         return success
98
99     def __str__(self):
100         res = []
101         res.append('\nAssertions report:\nLevel\tsuccess\tfailure')
102         success = failure = 0
103         for severity in self._report:
104             res.append("%s\t%s\t%s" % (severity, self._report[severity][True], self._report[severity][False]))
105             success += self._report[severity][True]
106             failure += self._report[severity][False]
107         res.append("total\t%s\t%s" % (success, failure))
108         res.append("end of report (%s assertion(s) checked)" % success + failure)
109         return "\n".join(res)
110
111 class RecordDictWrapper(dict):
112     """
113     Used to pass a record as locals in eval:
114     records do not strictly behave like dict, so we force them to.
115     """
116     def __init__(self, record):
117         self.record = record
118     def __getitem__(self, key):
119         if key in self.record:
120             return self.record[key]
121         return dict.__getitem__(self, key)
122
123 class YamlInterpreter(object):
124     def __init__(self, cr, module, id_map, mode, filename, noupdate=False):
125         self.cr = cr
126         self.module = module
127         self.id_map = id_map
128         self.mode = mode
129         self.filename = filename
130         self.assert_report = TestReport()
131         self.noupdate = noupdate
132         self.logger = logging.getLogger("%s.%s" % (logger_channel, self.module))
133         self.pool = pooler.get_pool(cr.dbname)
134         self.uid = 1
135         self.context = {} # opererp context
136         self.eval_context = {'ref': self._ref(),
137                              '_ref': self._ref(), # added '_ref' so that record['ref'] is possible
138                              'time': time,
139                              'datetime': datetime,
140                              'timedelta': timedelta}
141
142     def _ref(self):
143         return lambda xml_id: self.get_id(xml_id)
144
145     def get_model(self, model_name):
146         model = self.pool.get(model_name)
147         assert model, "The model %s does not exist." % (model_name,)
148         return model
149
150     def validate_xml_id(self, xml_id):
151         id = xml_id
152         if '.' in xml_id:
153             module, id = xml_id.split('.', 1)
154             assert '.' not in id, "The ID reference '%s' must contains maximum one dot.\n" \
155                                   "It is used to refer to other modules ID, in the form: module.record_id" \
156                                   % (xml_id,)
157             if module != self.module:
158                 module_count = self.pool.get('ir.module.module').search_count(self.cr, self.uid, \
159                         ['&', ('name', '=', module), ('state', 'in', ['installed'])])
160                 assert module_count == 1, 'The ID "%s" refers to an uninstalled module.' % (xml_id,)
161         if len(id) > 64: # TODO where does 64 come from (DB is 128)? should be a constant or loaded form DB
162             self.logger.log(logging.ERROR, 'id: %s is to long (max: 64)', id)
163
164     def get_id(self, xml_id):
165         if not xml_id:
166             raise YamlImportException("The xml_id should be a non empty string.")
167         if isinstance(xml_id, types.IntType):
168             id = xml_id
169         elif xml_id in self.id_map:
170             id = self.id_map[xml_id]
171         else:
172             if '.' in xml_id:
173                 module, checked_xml_id = xml_id.split('.', 1)
174             else:
175                 module = self.module
176                 checked_xml_id = xml_id
177             try:
178                 _, id = self.pool.get('ir.model.data').get_object_reference(self.cr, self.uid, module, checked_xml_id)
179                 self.id_map[xml_id] = id
180             except ValueError, e:
181                 raise YamlImportException(""" This Yaml file appears to depend on data that is missing. This often happens for
182                                         tests that belong to a module's test suite and depend on each other
183                                         (they are supposed to be executed in batch, not standalone).
184                                         You might solve the issue by first forcing the other tests to commit
185                                         their changes using --test-commit during a module update.""")
186                 self.logger.exception(e)
187
188         return id
189
190     def get_context(self, node, eval_dict):
191         context = self.context.copy()
192         if node.context:
193             context.update(eval(node.context, eval_dict))
194         return context
195
196     def isnoupdate(self, node):
197         return self.noupdate or node.noupdate or False
198
199     def _get_first_result(self, results, default=False):
200         if len(results):
201             value = results[0]
202             if isinstance(value, types.TupleType):
203                 value = value[0]
204         else:
205             value = default
206         return value
207
208     def process_comment(self, node):
209         return node
210
211     def _log_assert_failure(self, severity, msg, *args):
212         if isinstance(severity, types.StringTypes):
213             levelname = severity.strip().upper()
214             level = logging.getLevelName(levelname)
215         else:
216             level = severity
217             levelname = logging.getLevelName(level)
218         self.assert_report.record(False, levelname)
219         self.logger.log(level, msg, *args)
220         if level >= config['assert_exit_level']:
221             raise YamlImportAbortion('Severe assertion failure (%s), aborting.' % levelname)
222         return
223
224     def _get_assertion_id(self, assertion):
225         if assertion.id:
226             ids = [self.get_id(assertion.id)]
227         elif assertion.search:
228             q = eval(assertion.search, self.eval_context)
229             ids = self.pool.get(assertion.model).search(self.cr, self.uid, q, context=assertion.context)
230         else:
231             raise YamlImportException('Nothing to assert: you must give either an id or a search criteria.')
232         return ids
233
234     def process_assert(self, node):
235         if isinstance(node, dict):
236             assertion, expressions = node.items()[0]
237         else:
238             assertion, expressions = node, []
239
240         if self.isnoupdate(assertion) and self.mode != 'init':
241             self.logger.warn('This assertion was not evaluated ("%s").' % assertion.string)
242             return
243         model = self.get_model(assertion.model)
244         ids = self._get_assertion_id(assertion)
245         if assertion.count is not None and len(ids) != assertion.count:
246             msg = 'assertion "%s" failed!\n'   \
247                   ' Incorrect search count:\n' \
248                   ' expected count: %d\n'      \
249                   ' obtained count: %d\n'
250             args = (assertion.string, assertion.count, len(ids))
251             self._log_assert_failure(assertion.severity, msg, *args)
252         else:
253             context = self.get_context(assertion, self.eval_context)
254             for id in ids:
255                 record = model.browse(self.cr, self.uid, id, context)
256                 for test in expressions:
257                     try:
258                         success = unsafe_eval(test, self.eval_context, RecordDictWrapper(record))
259                     except Exception, e:
260                         self.logger.debug('Exception during evaluation of !assert block in yaml_file %s.', self.filename, exc_info=True)
261                         raise YamlImportAbortion(e)
262                     if not success:
263                         msg = 'Assertion "%s" FAILED\ntest: %s\n'
264                         args = (assertion.string, test)
265                         for aop in ('==', '!=', '<>', 'in', 'not in', '>=', '<=', '>', '<'):
266                             if aop in test:
267                                 left, right = test.split(aop,1)
268                                 lmsg = ''
269                                 rmsg = ''
270                                 try:
271                                     lmsg = unsafe_eval(left, self.eval_context, RecordDictWrapper(record))
272                                 except Exception, e:
273                                     lmsg = '<exc>'
274
275                                 try:
276                                     rmsg = unsafe_eval(right, self.eval_context, RecordDictWrapper(record))
277                                 except Exception, e:
278                                     rmsg = '<exc>'
279
280                                 msg += 'values: ! %s %s %s'
281                                 args += ( lmsg, aop, rmsg )
282                                 break
283
284                         self._log_assert_failure(assertion.severity, msg, *args)
285                         return
286             else: # all tests were successful for this assertion tag (no break)
287                 self.assert_report.record(True, assertion.severity)
288
289     def _coerce_bool(self, value, default=False):
290         if isinstance(value, types.BooleanType):
291             b = value
292         if isinstance(value, types.StringTypes):
293             b = value.strip().lower() not in ('0', 'false', 'off', 'no')
294         elif isinstance(value, types.IntType):
295             b = bool(value)
296         else:
297             b = default
298         return b
299
300     def create_osv_memory_record(self, record, fields):
301         model = self.get_model(record.model)
302         record_dict = self._create_record(model, fields)
303         id_new=model.create(self.cr, self.uid, record_dict, context=self.context)
304         self.id_map[record.id] = int(id_new)
305         return record_dict
306
307     def process_record(self, node):
308         import openerp.osv as osv
309         record, fields = node.items()[0]
310         model = self.get_model(record.model)
311         if isinstance(model, osv.osv.osv_memory):
312             record_dict=self.create_osv_memory_record(record, fields)
313         else:
314             self.validate_xml_id(record.id)
315             if self.isnoupdate(record) and self.mode != 'init':
316                 id = self.pool.get('ir.model.data')._update_dummy(self.cr, 1, record.model, self.module, record.id)
317                 # check if the resource already existed at the last update
318                 if id:
319                     self.id_map[record] = int(id)
320                     return None
321                 else:
322                     if not self._coerce_bool(record.forcecreate):
323                         return None
324
325             record_dict = self._create_record(model, fields)
326             self.logger.debug("RECORD_DICT %s" % record_dict)
327             #context = self.get_context(record, self.eval_context)
328             context = record.context #TOFIX: record.context like {'withoutemployee':True} should pass from self.eval_context. example: test_project.yml in project module
329             id = self.pool.get('ir.model.data')._update(self.cr, 1, record.model, \
330                     self.module, record_dict, record.id, noupdate=self.isnoupdate(record), mode=self.mode, context=context)
331             self.id_map[record.id] = int(id)
332             if config.get('import_partial'):
333                 self.cr.commit()
334
335     def _create_record(self, model, fields):
336         record_dict = {}
337         for field_name, expression in fields.items():
338             field_value = self._eval_field(model, field_name, expression)
339             record_dict[field_name] = field_value
340         return record_dict
341
342     def process_ref(self, node, column=None):
343         if node.search:
344             if node.model:
345                 model_name = node.model
346             elif column:
347                 model_name = column._obj
348             else:
349                 raise YamlImportException('You need to give a model for the search, or a column to infer it.')
350             model = self.get_model(model_name)
351             q = eval(node.search, self.eval_context)
352             ids = model.search(self.cr, self.uid, q)
353             if node.use:
354                 instances = model.browse(self.cr, self.uid, ids)
355                 value = [inst[node.use] for inst in instances]
356             else:
357                 value = ids
358         elif node.id:
359             value = self.get_id(node.id)
360         else:
361             value = None
362         return value
363
364     def process_eval(self, node):
365         return eval(node.expression, self.eval_context)
366
367     def _eval_field(self, model, field_name, expression):
368         # TODO this should be refactored as something like model.get_field() in bin/osv
369         if field_name in model._columns:
370             column = model._columns[field_name]
371         elif field_name in model._inherit_fields:
372             column = model._inherit_fields[field_name][2]
373         else:
374             raise KeyError("Object '%s' does not contain field '%s'" % (model, field_name))
375         if is_ref(expression):
376             elements = self.process_ref(expression, column)
377             if column._type in ("many2many", "one2many"):
378                 value = [(6, 0, elements)]
379             else: # many2one
380                 value = self._get_first_result(elements)
381         elif column._type == "many2one":
382             value = self.get_id(expression)
383         elif column._type == "one2many":
384             other_model = self.get_model(column._obj)
385             value = [(0, 0, self._create_record(other_model, fields)) for fields in expression]
386         elif column._type == "many2many":
387             ids = [self.get_id(xml_id) for xml_id in expression]
388             value = [(6, 0, ids)]
389         elif column._type == "date" and is_string(expression):
390             # enforce ISO format for string date values, to be locale-agnostic during tests
391             time.strptime(expression, misc.DEFAULT_SERVER_DATE_FORMAT)
392             value = expression
393         elif column._type == "datetime" and is_string(expression):
394             # enforce ISO format for string datetime values, to be locale-agnostic during tests
395             time.strptime(expression, misc.DEFAULT_SERVER_DATETIME_FORMAT)
396             value = expression
397         else: # scalar field
398             if is_eval(expression):
399                 value = self.process_eval(expression)
400             else:
401                 value = expression
402             # raise YamlImportException('Unsupported column "%s" or value %s:%s' % (field_name, type(expression), expression))
403         return value
404
405     def process_context(self, node):
406         self.context = node.__dict__
407         if node.uid:
408             self.uid = self.get_id(node.uid)
409         if node.noupdate:
410             self.noupdate = node.noupdate
411
412     def process_python(self, node):
413         def log(msg, *args):
414             self.logger.log(logging.TEST, msg, *args)
415         python, statements = node.items()[0]
416         model = self.get_model(python.model)
417         statements = statements.replace("\r\n", "\n")
418         code_context = {'model': model, 'cr': self.cr, 'uid': self.uid, 'log': log, 'context': self.context}
419         code_context.update({'self': model}) # remove me when no !python block test uses 'self' anymore
420         try:
421             code_obj = compile(statements, self.filename, 'exec')
422             unsafe_eval(code_obj, {'ref': self.get_id}, code_context)
423         except AssertionError, e:
424             self._log_assert_failure(python.severity, 'AssertionError in Python code %s: %s', python.name, e)
425             return
426         except Exception, e:
427             self.logger.debug('Exception during evaluation of !python block in yaml_file %s.', self.filename, exc_info=True)
428             raise
429         else:
430             self.assert_report.record(True, python.severity)
431
432     def process_workflow(self, node):
433         workflow, values = node.items()[0]
434         if self.isnoupdate(workflow) and self.mode != 'init':
435             return
436         if workflow.ref:
437             id = self.get_id(workflow.ref)
438         else:
439             if not values:
440                 raise YamlImportException('You must define a child node if you do not give a ref.')
441             if not len(values) == 1:
442                 raise YamlImportException('Only one child node is accepted (%d given).' % len(values))
443             value = values[0]
444             if not 'model' in value and (not 'eval' in value or not 'search' in value):
445                 raise YamlImportException('You must provide a "model" and an "eval" or "search" to evaluate.')
446             value_model = self.get_model(value['model'])
447             local_context = {'obj': lambda x: value_model.browse(self.cr, self.uid, x, context=self.context)}
448             local_context.update(self.id_map)
449             id = eval(value['eval'], self.eval_context, local_context)
450
451         if workflow.uid is not None:
452             uid = workflow.uid
453         else:
454             uid = self.uid
455         self.cr.execute('select distinct signal from wkf_transition')
456         signals=[x['signal'] for x in self.cr.dictfetchall()]
457         if workflow.action not in signals:
458             raise YamlImportException('Incorrect action %s. No such action defined' % workflow.action)
459         import openerp.netsvc as netsvc
460         wf_service = netsvc.LocalService("workflow")
461         wf_service.trg_validate(uid, workflow.model, id, workflow.action, self.cr)
462
463     def _eval_params(self, model, params):
464         args = []
465         for i, param in enumerate(params):
466             if isinstance(param, types.ListType):
467                 value = self._eval_params(model, param)
468             elif is_ref(param):
469                 value = self.process_ref(param)
470             elif is_eval(param):
471                 value = self.process_eval(param)
472             elif isinstance(param, types.DictionaryType): # supports XML syntax
473                 param_model = self.get_model(param.get('model', model))
474                 if 'search' in param:
475                     q = eval(param['search'], self.eval_context)
476                     ids = param_model.search(self.cr, self.uid, q)
477                     value = self._get_first_result(ids)
478                 elif 'eval' in param:
479                     local_context = {'obj': lambda x: param_model.browse(self.cr, self.uid, x, self.context)}
480                     local_context.update(self.id_map)
481                     value = eval(param['eval'], self.eval_context, local_context)
482                 else:
483                     raise YamlImportException('You must provide either a !ref or at least a "eval" or a "search" to function parameter #%d.' % i)
484             else:
485                 value = param # scalar value
486             args.append(value)
487         return args
488
489     def process_function(self, node):
490         function, params = node.items()[0]
491         if self.isnoupdate(function) and self.mode != 'init':
492             return
493         model = self.get_model(function.model)
494         context = self.get_context(function, self.eval_context)
495         if function.eval:
496             args = self.process_eval(function.eval)
497         else:
498             args = self._eval_params(function.model, params)
499         method = function.name
500         getattr(model, method)(self.cr, self.uid, *args)
501
502     def _set_group_values(self, node, values):
503         if node.groups:
504             group_names = node.groups.split(',')
505             groups_value = []
506             for group in group_names:
507                 if group.startswith('-'):
508                     group_id = self.get_id(group[1:])
509                     groups_value.append((3, group_id))
510                 else:
511                     group_id = self.get_id(group)
512                     groups_value.append((4, group_id))
513             values['groups_id'] = groups_value
514
515     def process_menuitem(self, node):
516         self.validate_xml_id(node.id)
517
518         if not node.parent:
519             parent_id = False
520             self.cr.execute('select id from ir_ui_menu where parent_id is null and name=%s', (node.name,))
521             res = self.cr.fetchone()
522             values = {'parent_id': parent_id, 'name': node.name}
523         else:
524             parent_id = self.get_id(node.parent)
525             values = {'parent_id': parent_id}
526             if node.name:
527                 values['name'] = node.name
528             try:
529                 res = [ self.get_id(node.id) ]
530             except: # which exception ?
531                 res = None
532
533         if node.action:
534             action_type = node.type or 'act_window'
535             icons = {
536                 "act_window": 'STOCK_NEW',
537                 "report.xml": 'STOCK_PASTE',
538                 "wizard": 'STOCK_EXECUTE',
539                 "url": 'STOCK_JUMP_TO',
540             }
541             values['icon'] = icons.get(action_type, 'STOCK_NEW')
542             if action_type == 'act_window':
543                 action_id = self.get_id(node.action)
544                 self.cr.execute('select view_type,view_mode,name,view_id,target from ir_act_window where id=%s', (action_id,))
545                 ir_act_window_result = self.cr.fetchone()
546                 assert ir_act_window_result, "No window action defined for this id %s !\n" \
547                         "Verify that this is a window action or add a type argument." % (node.action,)
548                 action_type, action_mode, action_name, view_id, target = ir_act_window_result
549                 if view_id:
550                     self.cr.execute('SELECT type FROM ir_ui_view WHERE id=%s', (view_id,))
551                     # TODO guess why action_mode is ir_act_window.view_mode above and ir_ui_view.type here
552                     action_mode = self.cr.fetchone()
553                 self.cr.execute('SELECT view_mode FROM ir_act_window_view WHERE act_window_id=%s ORDER BY sequence LIMIT 1', (action_id,))
554                 if self.cr.rowcount:
555                     action_mode = self.cr.fetchone()
556                 if action_type == 'tree':
557                     values['icon'] = 'STOCK_INDENT'
558                 elif action_mode and action_mode.startswith('tree'):
559                     values['icon'] = 'STOCK_JUSTIFY_FILL'
560                 elif action_mode and action_mode.startswith('graph'):
561                     values['icon'] = 'terp-graph'
562                 elif action_mode and action_mode.startswith('calendar'):
563                     values['icon'] = 'terp-calendar'
564                 if target == 'new':
565                     values['icon'] = 'STOCK_EXECUTE'
566                 if not values.get('name', False):
567                     values['name'] = action_name
568             elif action_type == 'wizard':
569                 action_id = self.get_id(node.action)
570                 self.cr.execute('select name from ir_act_wizard where id=%s', (action_id,))
571                 ir_act_wizard_result = self.cr.fetchone()
572                 if (not values.get('name', False)) and ir_act_wizard_result:
573                     values['name'] = ir_act_wizard_result[0]
574             else:
575                 raise YamlImportException("Unsupported type '%s' in menuitem tag." % action_type)
576         if node.sequence:
577             values['sequence'] = node.sequence
578         if node.icon:
579             values['icon'] = node.icon
580
581         self._set_group_values(node, values)
582
583         pid = self.pool.get('ir.model.data')._update(self.cr, 1, \
584                 'ir.ui.menu', self.module, values, node.id, mode=self.mode, \
585                 noupdate=self.isnoupdate(node), res_id=res and res[0] or False)
586
587         if node.id and parent_id:
588             self.id_map[node.id] = int(parent_id)
589
590         if node.action and pid:
591             action_type = node.type or 'act_window'
592             action_id = self.get_id(node.action)
593             action = "ir.actions.%s,%d" % (action_type, action_id)
594             self.pool.get('ir.model.data').ir_set(self.cr, 1, 'action', \
595                     'tree_but_open', 'Menuitem', [('ir.ui.menu', int(parent_id))], action, True, True, xml_id=node.id)
596
597     def process_act_window(self, node):
598         assert getattr(node, 'id'), "Attribute %s of act_window is empty !" % ('id',)
599         assert getattr(node, 'name'), "Attribute %s of act_window is empty !" % ('name',)
600         assert getattr(node, 'res_model'), "Attribute %s of act_window is empty !" % ('res_model',)
601         self.validate_xml_id(node.id)
602         view_id = False
603         if node.view:
604             view_id = self.get_id(node.view)
605         if not node.context:
606             node.context={}
607         context = eval(str(node.context), self.eval_context)
608         values = {
609             'name': node.name,
610             'type': node.type or 'ir.actions.act_window',
611             'view_id': view_id,
612             'domain': node.domain,
613             'context': context,
614             'res_model': node.res_model,
615             'src_model': node.src_model,
616             'view_type': node.view_type or 'form',
617             'view_mode': node.view_mode or 'tree,form',
618             'usage': node.usage,
619             'limit': node.limit,
620             'auto_refresh': node.auto_refresh,
621             'multi': getattr(node, 'multi', False),
622         }
623
624         self._set_group_values(node, values)
625
626         if node.target:
627             values['target'] = node.target
628         id = self.pool.get('ir.model.data')._update(self.cr, 1, \
629                 'ir.actions.act_window', self.module, values, node.id, mode=self.mode)
630         self.id_map[node.id] = int(id)
631
632         if node.src_model:
633             keyword = 'client_action_relate'
634             value = 'ir.actions.act_window,%s' % id
635             replace = node.replace or True
636             self.pool.get('ir.model.data').ir_set(self.cr, 1, 'action', keyword, \
637                     node.id, [node.src_model], value, replace=replace, noupdate=self.isnoupdate(node), isobject=True, xml_id=node.id)
638         # TODO add remove ir.model.data
639
640     def process_delete(self, node):
641         assert getattr(node, 'model'), "Attribute %s of delete tag is empty !" % ('model',)
642         if self.pool.get(node.model):
643             if len(node.search):
644                 ids = self.pool.get(node.model).search(self.cr, self.uid, eval(node.search, self.eval_context))
645             else:
646                 ids = [self.get_id(node.id)]
647             if len(ids):
648                 self.pool.get(node.model).unlink(self.cr, self.uid, ids)
649                 self.pool.get('ir.model.data')._unlink(self.cr, 1, node.model, ids)
650         else:
651             self.logger.log(logging.TEST, "Record not deleted.")
652
653     def process_url(self, node):
654         self.validate_xml_id(node.id)
655
656         res = {'name': node.name, 'url': node.url, 'target': node.target}
657
658         id = self.pool.get('ir.model.data')._update(self.cr, 1, \
659                 "ir.actions.url", self.module, res, node.id, mode=self.mode)
660         self.id_map[node.id] = int(id)
661         # ir_set
662         if (not node.menu or eval(node.menu)) and id:
663             keyword = node.keyword or 'client_action_multi'
664             value = 'ir.actions.url,%s' % id
665             replace = node.replace or True
666             self.pool.get('ir.model.data').ir_set(self.cr, 1, 'action', \
667                     keyword, node.url, ["ir.actions.url"], value, replace=replace, \
668                     noupdate=self.isnoupdate(node), isobject=True, xml_id=node.id)
669
670     def process_ir_set(self, node):
671         if not self.mode == 'init':
672             return False
673         _, fields = node.items()[0]
674         res = {}
675         for fieldname, expression in fields.items():
676             if is_eval(expression):
677                 value = eval(expression.expression, self.eval_context)
678             else:
679                 value = expression
680             res[fieldname] = value
681         self.pool.get('ir.model.data').ir_set(self.cr, 1, res['key'], res['key2'], \
682                 res['name'], res['models'], res['value'], replace=res.get('replace',True), \
683                 isobject=res.get('isobject', False), meta=res.get('meta',None))
684
685     def process_report(self, node):
686         values = {}
687         for dest, f in (('name','string'), ('model','model'), ('report_name','name')):
688             values[dest] = getattr(node, f)
689             assert values[dest], "Attribute %s of report is empty !" % (f,)
690         for field,dest in (('rml','report_rml'),('file','report_rml'),('xml','report_xml'),('xsl','report_xsl'),('attachment','attachment'),('attachment_use','attachment_use')):
691             if getattr(node, field):
692                 values[dest] = getattr(node, field)
693         if node.auto:
694             values['auto'] = eval(node.auto)
695         if node.sxw:
696             sxw_file = misc.file_open(node.sxw)
697             try:
698                 sxw_content = sxw_file.read()
699                 values['report_sxw_content'] = sxw_content
700             finally:
701                 sxw_file.close()
702         if node.header:
703             values['header'] = eval(node.header)
704         values['multi'] = node.multi and eval(node.multi)
705         xml_id = node.id
706         self.validate_xml_id(xml_id)
707
708         self._set_group_values(node, values)
709
710         id = self.pool.get('ir.model.data')._update(self.cr, 1, "ir.actions.report.xml", \
711                 self.module, values, xml_id, noupdate=self.isnoupdate(node), mode=self.mode)
712         self.id_map[xml_id] = int(id)
713
714         if not node.menu or eval(node.menu):
715             keyword = node.keyword or 'client_print_multi'
716             value = 'ir.actions.report.xml,%s' % id
717             replace = node.replace or True
718             self.pool.get('ir.model.data').ir_set(self.cr, 1, 'action', \
719                     keyword, values['name'], [values['model']], value, replace=replace, isobject=True, xml_id=xml_id)
720
721     def process_none(self):
722         """
723         Empty node or commented node should not pass silently.
724         """
725         self._log_assert_failure(logging.WARNING, "You have an empty block in your tests.")
726
727
728     def process(self, yaml_string):
729         """
730         Processes a Yaml string. Custom tags are interpreted by 'process_' instance methods.
731         """
732         yaml_tag.add_constructors()
733
734         is_preceded_by_comment = False
735         for node in yaml.load(yaml_string):
736             is_preceded_by_comment = self._log(node, is_preceded_by_comment)
737             try:
738                 self._process_node(node)
739             except YamlImportException, e:
740                 self.logger.exception(e)
741             except Exception, e:
742                 self.logger.exception(e)
743                 raise
744
745     def _process_node(self, node):
746         if is_comment(node):
747             self.process_comment(node)
748         elif is_assert(node):
749             self.process_assert(node)
750         elif is_record(node):
751             self.process_record(node)
752         elif is_python(node):
753             self.process_python(node)
754         elif is_menuitem(node):
755             self.process_menuitem(node)
756         elif is_delete(node):
757             self.process_delete(node)
758         elif is_url(node):
759             self.process_url(node)
760         elif is_context(node):
761             self.process_context(node)
762         elif is_ir_set(node):
763             self.process_ir_set(node)
764         elif is_act_window(node):
765             self.process_act_window(node)
766         elif is_report(node):
767             self.process_report(node)
768         elif is_workflow(node):
769             if isinstance(node, types.DictionaryType):
770                 self.process_workflow(node)
771             else:
772                 self.process_workflow({node: []})
773         elif is_function(node):
774             if isinstance(node, types.DictionaryType):
775                 self.process_function(node)
776             else:
777                 self.process_function({node: []})
778         elif node is None:
779             self.process_none()
780         else:
781             raise YamlImportException("Can not process YAML block: %s" % node)
782
783     def _log(self, node, is_preceded_by_comment):
784         if is_comment(node):
785             is_preceded_by_comment = True
786             self.logger.log(logging.TEST, node)
787         elif not is_preceded_by_comment:
788             if isinstance(node, types.DictionaryType):
789                 msg = "Creating %s\n with %s"
790                 args = node.items()[0]
791                 self.logger.log(logging.TEST, msg, *args)
792             else:
793                 self.logger.log(logging.TEST, node)
794         else:
795             is_preceded_by_comment = False
796         return is_preceded_by_comment
797
798 def yaml_import(cr, module, yamlfile, idref=None, mode='init', noupdate=False, report=None):
799     if idref is None:
800         idref = {}
801     yaml_string = yamlfile.read()
802     yaml_interpreter = YamlInterpreter(cr, module, idref, mode, filename=yamlfile.name, noupdate=noupdate)
803     yaml_interpreter.process(yaml_string)
804
805 # keeps convention of convert.py
806 convert_yaml_import = yaml_import
807
808 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: