1 # -*- coding: utf-8 -*-
4 import time # used to eval time.strftime expressions
5 from datetime import datetime, timedelta
9 import openerp.sql_db as sql_db
10 import openerp.workflow
12 from config import config
16 from lxml import etree
17 from openerp import SUPERUSER_ID
19 # YAML import needs both safe and unsafe eval, but let's
22 from safe_eval import safe_eval as eval
24 import assertion_report
26 _logger = logging.getLogger(__name__)
28 class YamlImportException(Exception):
31 class YamlImportAbortion(Exception):
34 def _is_yaml_mapping(node, tag_constructor):
35 value = isinstance(node, types.DictionaryType) \
36 and len(node.keys()) == 1 \
37 and isinstance(node.keys()[0], tag_constructor)
41 return isinstance(node, types.StringTypes)
44 return isinstance(node, yaml_tag.Assert) \
45 or _is_yaml_mapping(node, yaml_tag.Assert)
48 return _is_yaml_mapping(node, yaml_tag.Record)
51 return _is_yaml_mapping(node, yaml_tag.Python)
53 def is_menuitem(node):
54 return isinstance(node, yaml_tag.Menuitem) \
55 or _is_yaml_mapping(node, yaml_tag.Menuitem)
57 def is_function(node):
58 return isinstance(node, yaml_tag.Function) \
59 or _is_yaml_mapping(node, yaml_tag.Function)
62 return isinstance(node, yaml_tag.Report)
64 def is_workflow(node):
65 return isinstance(node, yaml_tag.Workflow)
67 def is_act_window(node):
68 return isinstance(node, yaml_tag.ActWindow)
71 return isinstance(node, yaml_tag.Delete)
74 return isinstance(node, yaml_tag.Context)
77 return isinstance(node, yaml_tag.Url)
80 return isinstance(node, yaml_tag.Eval)
83 return isinstance(node, yaml_tag.Ref) \
84 or _is_yaml_mapping(node, yaml_tag.Ref)
87 return _is_yaml_mapping(node, yaml_tag.IrSet)
90 return isinstance(node, basestring)
92 class RecordDictWrapper(dict):
94 Used to pass a record as locals in eval:
95 records do not strictly behave like dict, so we force them to.
97 def __init__(self, record):
99 def __getitem__(self, key):
100 if key in self.record:
101 return self.record[key]
102 return dict.__getitem__(self, key)
104 class YamlInterpreter(object):
105 def __init__(self, cr, module, id_map, mode, filename, report=None, noupdate=False, loglevel=logging.DEBUG):
110 self.filename = filename
112 report = assertion_report.assertion_report()
113 self.assertion_report = report
114 self.noupdate = noupdate
115 self.loglevel = loglevel
116 self.pool = openerp.registry(cr.dbname)
118 self.context = {} # opererp context
119 self.eval_context = {'ref': self._ref(),
120 '_ref': self._ref(), # added '_ref' so that record['ref'] is possible
122 'datetime': datetime,
123 'timedelta': timedelta}
125 def _log(self, *args, **kwargs):
126 _logger.log(self.loglevel, *args, **kwargs)
129 return lambda xml_id: self.get_id(xml_id)
131 def get_model(self, model_name):
132 return self.pool[model_name]
134 def validate_xml_id(self, xml_id):
137 module, id = xml_id.split('.', 1)
138 assert '.' not in id, "The ID reference '%s' must contains maximum one dot.\n" \
139 "It is used to refer to other modules ID, in the form: module.record_id" \
141 if module != self.module:
142 module_count = self.pool['ir.module.module'].search_count(self.cr, self.uid, \
143 ['&', ('name', '=', module), ('state', 'in', ['installed'])])
144 assert module_count == 1, 'The ID "%s" refers to an uninstalled module.' % (xml_id,)
145 if len(id) > 64: # TODO where does 64 come from (DB is 128)? should be a constant or loaded form DB
146 _logger.error('id: %s is to long (max: 64)', id)
148 def get_id(self, xml_id):
149 if xml_id is False or xml_id is None:
152 # raise YamlImportException("The xml_id should be a non empty string.")
153 elif isinstance(xml_id, types.IntType):
155 elif xml_id in self.id_map:
156 id = self.id_map[xml_id]
159 module, checked_xml_id = xml_id.split('.', 1)
162 checked_xml_id = xml_id
164 _, id = self.pool['ir.model.data'].get_object_reference(self.cr, self.uid, module, checked_xml_id)
165 self.id_map[xml_id] = id
167 raise ValueError("""%s not found when processing %s.
168 This Yaml file appears to depend on missing data. This often happens for
169 tests that belong to a module's test suite and depend on each other.""" % (checked_xml_id, self.filename))
173 def get_context(self, node, eval_dict):
174 context = self.context.copy()
176 context.update(eval(node.context, eval_dict))
179 def isnoupdate(self, node):
180 return self.noupdate or node.noupdate or False
182 def _get_first_result(self, results, default=False):
185 if isinstance(value, types.TupleType):
191 def process_comment(self, node):
194 def _log_assert_failure(self, msg, *args):
195 self.assertion_report.record_failure()
196 _logger.error(msg, *args)
198 def _get_assertion_id(self, assertion):
200 ids = [self.get_id(assertion.id)]
201 elif assertion.search:
202 q = eval(assertion.search, self.eval_context)
203 ids = self.pool[assertion.model].search(self.cr, self.uid, q, context=assertion.context)
205 raise YamlImportException('Nothing to assert: you must give either an id or a search criteria.')
208 def process_assert(self, node):
209 if isinstance(node, dict):
210 assertion, expressions = node.items()[0]
212 assertion, expressions = node, []
214 if self.isnoupdate(assertion) and self.mode != 'init':
215 _logger.warning('This assertion was not evaluated ("%s").', assertion.string)
217 model = self.get_model(assertion.model)
218 ids = self._get_assertion_id(assertion)
219 if assertion.count is not None and len(ids) != assertion.count:
220 msg = 'assertion "%s" failed!\n' \
221 ' Incorrect search count:\n' \
222 ' expected count: %d\n' \
223 ' obtained count: %d\n'
224 args = (assertion.string, assertion.count, len(ids))
225 self._log_assert_failure(msg, *args)
227 context = self.get_context(assertion, self.eval_context)
229 record = model.browse(self.cr, self.uid, id, context)
230 for test in expressions:
232 success = unsafe_eval(test, self.eval_context, RecordDictWrapper(record))
234 _logger.debug('Exception during evaluation of !assert block in yaml_file %s.', self.filename, exc_info=True)
235 raise YamlImportAbortion(e)
237 msg = 'Assertion "%s" FAILED\ntest: %s\n'
238 args = (assertion.string, test)
239 for aop in ('==', '!=', '<>', 'in', 'not in', '>=', '<=', '>', '<'):
241 left, right = test.split(aop,1)
245 lmsg = unsafe_eval(left, self.eval_context, RecordDictWrapper(record))
250 rmsg = unsafe_eval(right, self.eval_context, RecordDictWrapper(record))
254 msg += 'values: ! %s %s %s'
255 args += ( lmsg, aop, rmsg )
258 self._log_assert_failure(msg, *args)
260 else: # all tests were successful for this assertion tag (no break)
261 self.assertion_report.record_success()
263 def _coerce_bool(self, value, default=False):
264 if isinstance(value, types.BooleanType):
266 if isinstance(value, types.StringTypes):
267 b = value.strip().lower() not in ('0', 'false', 'off', 'no')
268 elif isinstance(value, types.IntType):
274 def create_osv_memory_record(self, record, fields):
275 model = self.get_model(record.model)
276 context = self.get_context(record, self.eval_context)
277 record_dict = self._create_record(model, fields)
278 id_new = model.create(self.cr, self.uid, record_dict, context=context)
279 self.id_map[record.id] = int(id_new)
282 def process_record(self, node):
283 record, fields = node.items()[0]
284 model = self.get_model(record.model)
286 view_id = record.view
287 if view_id and (view_id is not True) and isinstance(view_id, basestring):
290 module, view_id = view_id.split('.',1)
291 view_id = self.pool['ir.model.data'].get_object_reference(self.cr, SUPERUSER_ID, module, view_id)[1]
293 if model.is_transient():
294 record_dict=self.create_osv_memory_record(record, fields)
296 self.validate_xml_id(record.id)
298 self.pool['ir.model.data']._get_id(self.cr, SUPERUSER_ID, self.module, record.id)
303 if self.isnoupdate(record) and self.mode != 'init':
304 id = self.pool['ir.model.data']._update_dummy(self.cr, SUPERUSER_ID, record.model, self.module, record.id)
305 # check if the resource already existed at the last update
307 self.id_map[record] = int(id)
310 if not self._coerce_bool(record.forcecreate):
314 #context = self.get_context(record, self.eval_context)
315 #TOFIX: record.context like {'withoutemployee':True} should pass from self.eval_context. example: test_project.yml in project module
316 context = record.context
320 if view_id is True: varg = False
321 view_info = model.fields_view_get(self.cr, SUPERUSER_ID, varg, 'form', context)
323 record_dict = self._create_record(model, fields, view_info, default=default)
324 _logger.debug("RECORD_DICT %s" % record_dict)
325 id = self.pool['ir.model.data']._update(self.cr, SUPERUSER_ID, record.model, \
326 self.module, record_dict, record.id, noupdate=self.isnoupdate(record), mode=self.mode, context=context)
327 self.id_map[record.id] = int(id)
328 if config.get('import_partial'):
331 def _create_record(self, model, fields, view_info=False, parent={}, default=True):
332 """This function processes the !record tag in yalm files. It simulates the record creation through an xml
333 view (either specified on the !record tag or the default one for this object), including the calls to
334 on_change() functions, and sending only values for fields that aren't set as readonly.
335 :param model: model instance
336 :param fields: dictonary mapping the field names and their values
337 :param view_info: result of fields_view_get() called on the object
338 :param parent: dictionary containing the values already computed for the parent, in case of one2many fields
339 :param default: if True, the default values must be processed too or not
340 :return: dictionary mapping the field names and their values, ready to use when calling the create() function
343 def _get_right_one2many_view(fg, field_name, view_type):
344 one2many_view = fg[field_name]['views'].get(view_type)
345 # if the view is not defined inline, we call fields_view_get()
346 if not one2many_view:
347 one2many_view = self.pool[fg[field_name]['relation']].fields_view_get(self.cr, SUPERUSER_ID, False, view_type, self.context)
350 def process_val(key, val):
351 if fg[key]['type'] == 'many2one':
352 if type(val) in (tuple,list):
354 elif fg[key]['type'] == 'one2many':
355 if val and isinstance(val, (list,tuple)) and isinstance(val[0], dict):
356 # we want to return only the fields that aren't readonly
357 # For that, we need to first get the right tree view to consider for the field `key´
358 one2many_tree_view = _get_right_one2many_view(fg, key, 'tree')
359 arch = etree.fromstring(one2many_tree_view['arch'].encode('utf-8'))
361 # make a copy for the iteration, as we will alter `rec´
362 rec_copy = rec.copy()
363 for field_key in rec_copy:
364 # if field is missing in view or has a readonly modifier, drop it
365 field_elem = arch.xpath("//field[@name='%s']" % field_key)
366 if field_elem and (field_elem[0].get('modifiers', '{}').find('"readonly": true') >= 0):
367 # TODO: currently we only support if readonly is True in the modifiers. Some improvement may be done in
368 # order to support also modifiers that look like {"readonly": [["state", "not in", ["draft", "confirm"]]]}
370 # now that unwanted values have been removed from val, we can encapsulate it in a tuple as returned value
371 val = map(lambda x: (0,0,x), val)
372 elif fg[key]['type'] == 'many2many':
373 if val and isinstance(val,(list,tuple)) and isinstance(val[0], (int,long)):
376 # we want to return only the fields that aren't readonly
377 if el.get('modifiers', '{}').find('"readonly": true') >= 0:
378 # TODO: currently we only support if readonly is True in the modifiers. Some improvement may be done in
379 # order to support also modifiers that look like {"readonly": [["state", "not in", ["draft", "confirm"]]]}
385 arch = etree.fromstring(view_info['arch'].decode('utf-8'))
386 view = arch if len(arch) else False
389 fields = fields or {}
390 if view is not False:
391 fg = view_info['fields']
392 # gather the default values on the object. (Can't use `fields´ as parameter instead of {} because we may
393 # have references like `base.main_company´ in the yaml file and it's not compatible with the function)
394 defaults = default and model._add_missing_default_values(self.cr, SUPERUSER_ID, {}, context=self.context) or {}
396 # copy the default values in record_dict, only if they are in the view (because that's what the client does)
397 # the other default values will be added later on by the create().
398 record_dict = dict([(key, val) for key, val in defaults.items() if key in fg])
400 # Process all on_change calls
405 field_name = el.attrib['name']
406 assert field_name in fg, "The field '%s' is defined in the form view but not on the object '%s'!" % (field_name, model._name)
407 if field_name in fields:
408 one2many_form_view = None
409 if (view is not False) and (fg[field_name]['type']=='one2many'):
410 # for one2many fields, we want to eval them using the inline form view defined on the parent
411 one2many_form_view = _get_right_one2many_view(fg, field_name, 'form')
413 field_value = self._eval_field(model, field_name, fields[field_name], one2many_form_view or view_info, parent=record_dict, default=default)
415 #call process_val to not update record_dict if values were given for readonly fields
416 val = process_val(field_name, field_value)
418 record_dict[field_name] = val
419 #if (field_name in defaults) and defaults[field_name] == field_value:
420 # print '*** You can remove these lines:', field_name, field_value
422 #if field_name has a default value or a value is given in the yaml file, we must call its on_change()
423 elif field_name not in defaults:
426 if not el.attrib.get('on_change', False):
428 match = re.match("([a-z_1-9A-Z]+)\((.*)\)", el.attrib['on_change'])
429 assert match, "Unable to parse the on_change '%s'!" % (el.attrib['on_change'], )
431 # creating the context
432 class parent2(object):
433 def __init__(self, d):
435 def __getattr__(self, name):
436 return self.d.get(name, False)
438 ctx = record_dict.copy()
439 ctx['context'] = self.context
440 ctx['uid'] = SUPERUSER_ID
441 ctx['parent'] = parent2(parent)
444 ctx[a] = process_val(a, defaults.get(a, False))
447 args = map(lambda x: eval(x, ctx), match.group(2).split(','))
448 result = getattr(model, match.group(1))(self.cr, SUPERUSER_ID, [], *args)
449 for key, val in (result or {}).get('value', {}).items():
450 assert key in fg, "The returning field '%s' from your on_change call '%s' does not exist either on the object '%s', either in the view '%s' used for the creation" % (key, match.group(1), model._name, view_info['name'])
451 record_dict[key] = process_val(key, val)
452 #if (key in fields) and record_dict[key] == process_val(key, val):
453 # print '*** You can remove these lines:', key, val
455 nodes = list(el) + nodes
459 for field_name, expression in fields.items():
460 if field_name in record_dict:
462 field_value = self._eval_field(model, field_name, expression, default=False)
463 record_dict[field_name] = field_value
466 def process_ref(self, node, column=None):
467 assert node.search or node.id, '!ref node should have a `search` attribute or `id` attribute'
470 model_name = node.model
472 model_name = column._obj
474 raise YamlImportException('You need to give a model for the search, or a column to infer it.')
475 model = self.get_model(model_name)
476 q = eval(node.search, self.eval_context)
477 ids = model.search(self.cr, self.uid, q)
479 instances = model.browse(self.cr, self.uid, ids)
480 value = [inst[node.use] for inst in instances]
484 value = self.get_id(node.id)
489 def process_eval(self, node):
490 return eval(node.expression, self.eval_context)
492 def _eval_field(self, model, field_name, expression, view_info=False, parent={}, default=True):
493 # TODO this should be refactored as something like model.get_field() in bin/osv
494 if field_name in model._columns:
495 column = model._columns[field_name]
496 elif field_name in model._inherit_fields:
497 column = model._inherit_fields[field_name][2]
499 raise KeyError("Object '%s' does not contain field '%s'" % (model, field_name))
500 if is_ref(expression):
501 elements = self.process_ref(expression, column)
502 if column._type in ("many2many", "one2many"):
503 value = [(6, 0, elements)]
505 if isinstance(elements, (list,tuple)):
506 value = self._get_first_result(elements)
509 elif column._type == "many2one":
510 value = self.get_id(expression)
511 elif column._type == "one2many":
512 other_model = self.get_model(column._obj)
513 value = [(0, 0, self._create_record(other_model, fields, view_info, parent, default=default)) for fields in expression]
514 elif column._type == "many2many":
515 ids = [self.get_id(xml_id) for xml_id in expression]
516 value = [(6, 0, ids)]
517 elif column._type == "date" and is_string(expression):
518 # enforce ISO format for string date values, to be locale-agnostic during tests
519 time.strptime(expression, misc.DEFAULT_SERVER_DATE_FORMAT)
521 elif column._type == "datetime" and is_string(expression):
522 # enforce ISO format for string datetime values, to be locale-agnostic during tests
523 time.strptime(expression, misc.DEFAULT_SERVER_DATETIME_FORMAT)
526 if is_eval(expression):
527 value = self.process_eval(expression)
530 # raise YamlImportException('Unsupported column "%s" or value %s:%s' % (field_name, type(expression), expression))
533 def process_context(self, node):
534 self.context = node.__dict__
536 self.uid = self.get_id(node.uid)
538 self.noupdate = node.noupdate
540 def process_python(self, node):
541 python, statements = node.items()[0]
542 model = self.get_model(python.model)
543 statements = statements.replace("\r\n", "\n")
549 'context': self.context,
552 code_context.update({'self': model}) # remove me when no !python block test uses 'self' anymore
554 code_obj = compile(statements, self.filename, 'exec')
555 unsafe_eval(code_obj, {'ref': self.get_id}, code_context)
556 except AssertionError, e:
557 self._log_assert_failure('AssertionError in Python code %s: %s', python.name, e)
560 _logger.debug('Exception during evaluation of !python block in yaml_file %s.', self.filename, exc_info=True)
563 self.assertion_report.record_success()
565 def process_workflow(self, node):
566 workflow, values = node.items()[0]
567 if self.isnoupdate(workflow) and self.mode != 'init':
570 id = self.get_id(workflow.ref)
573 raise YamlImportException('You must define a child node if you do not give a ref.')
574 if not len(values) == 1:
575 raise YamlImportException('Only one child node is accepted (%d given).' % len(values))
577 if not 'model' in value and (not 'eval' in value or not 'search' in value):
578 raise YamlImportException('You must provide a "model" and an "eval" or "search" to evaluate.')
579 value_model = self.get_model(value['model'])
580 local_context = {'obj': lambda x: value_model.browse(self.cr, self.uid, x, context=self.context)}
581 local_context.update(self.id_map)
582 id = eval(value['eval'], self.eval_context, local_context)
584 if workflow.uid is not None:
588 self.cr.execute('select distinct signal from wkf_transition')
589 signals=[x['signal'] for x in self.cr.dictfetchall()]
590 if workflow.action not in signals:
591 raise YamlImportException('Incorrect action %s. No such action defined' % workflow.action)
592 openerp.workflow.trg_validate(uid, workflow.model, id, workflow.action, self.cr)
594 def _eval_params(self, model, params):
596 for i, param in enumerate(params):
597 if isinstance(param, types.ListType):
598 value = self._eval_params(model, param)
600 value = self.process_ref(param)
602 value = self.process_eval(param)
603 elif isinstance(param, types.DictionaryType): # supports XML syntax
604 param_model = self.get_model(param.get('model', model))
605 if 'search' in param:
606 q = eval(param['search'], self.eval_context)
607 ids = param_model.search(self.cr, self.uid, q)
608 value = self._get_first_result(ids)
609 elif 'eval' in param:
610 local_context = {'obj': lambda x: param_model.browse(self.cr, self.uid, x, self.context)}
611 local_context.update(self.id_map)
612 value = eval(param['eval'], self.eval_context, local_context)
614 raise YamlImportException('You must provide either a !ref or at least a "eval" or a "search" to function parameter #%d.' % i)
616 value = param # scalar value
620 def process_function(self, node):
621 function, params = node.items()[0]
622 if self.isnoupdate(function) and self.mode != 'init':
624 model = self.get_model(function.model)
626 args = self.process_eval(function.eval)
628 args = self._eval_params(function.model, params)
629 method = function.name
630 getattr(model, method)(self.cr, self.uid, *args)
632 def _set_group_values(self, node, values):
634 group_names = node.groups.split(',')
636 for group in group_names:
637 if group.startswith('-'):
638 group_id = self.get_id(group[1:])
639 groups_value.append((3, group_id))
641 group_id = self.get_id(group)
642 groups_value.append((4, group_id))
643 values['groups_id'] = groups_value
645 def process_menuitem(self, node):
646 self.validate_xml_id(node.id)
650 self.cr.execute('select id from ir_ui_menu where parent_id is null and name=%s', (node.name,))
651 res = self.cr.fetchone()
652 values = {'parent_id': parent_id, 'name': node.name}
654 parent_id = self.get_id(node.parent)
655 values = {'parent_id': parent_id}
657 values['name'] = node.name
659 res = [ self.get_id(node.id) ]
660 except: # which exception ?
664 action_type = node.type or 'act_window'
666 "act_window": 'STOCK_NEW',
667 "report.xml": 'STOCK_PASTE',
668 "wizard": 'STOCK_EXECUTE',
669 "url": 'STOCK_JUMP_TO',
671 values['icon'] = icons.get(action_type, 'STOCK_NEW')
672 if action_type == 'act_window':
673 action_id = self.get_id(node.action)
674 self.cr.execute('select view_type,view_mode,name,view_id,target from ir_act_window where id=%s', (action_id,))
675 ir_act_window_result = self.cr.fetchone()
676 assert ir_act_window_result, "No window action defined for this id %s !\n" \
677 "Verify that this is a window action or add a type argument." % (node.action,)
678 action_type, action_mode, action_name, view_id, target = ir_act_window_result
680 self.cr.execute('SELECT type FROM ir_ui_view WHERE id=%s', (view_id,))
681 # TODO guess why action_mode is ir_act_window.view_mode above and ir_ui_view.type here
682 action_mode = self.cr.fetchone()
683 self.cr.execute('SELECT view_mode FROM ir_act_window_view WHERE act_window_id=%s ORDER BY sequence LIMIT 1', (action_id,))
685 action_mode = self.cr.fetchone()
686 if action_type == 'tree':
687 values['icon'] = 'STOCK_INDENT'
688 elif action_mode and action_mode.startswith('tree'):
689 values['icon'] = 'STOCK_JUSTIFY_FILL'
690 elif action_mode and action_mode.startswith('graph'):
691 values['icon'] = 'terp-graph'
692 elif action_mode and action_mode.startswith('calendar'):
693 values['icon'] = 'terp-calendar'
695 values['icon'] = 'STOCK_EXECUTE'
696 if not values.get('name', False):
697 values['name'] = action_name
698 elif action_type == 'wizard':
699 action_id = self.get_id(node.action)
700 self.cr.execute('select name from ir_act_wizard where id=%s', (action_id,))
701 ir_act_wizard_result = self.cr.fetchone()
702 if (not values.get('name', False)) and ir_act_wizard_result:
703 values['name'] = ir_act_wizard_result[0]
705 raise YamlImportException("Unsupported type '%s' in menuitem tag." % action_type)
707 values['sequence'] = node.sequence
709 values['icon'] = node.icon
711 self._set_group_values(node, values)
713 pid = self.pool['ir.model.data']._update(self.cr, SUPERUSER_ID, \
714 'ir.ui.menu', self.module, values, node.id, mode=self.mode, \
715 noupdate=self.isnoupdate(node), res_id=res and res[0] or False)
717 if node.id and parent_id:
718 self.id_map[node.id] = int(parent_id)
720 if node.action and pid:
721 action_type = node.type or 'act_window'
722 action_id = self.get_id(node.action)
723 action = "ir.actions.%s,%d" % (action_type, action_id)
724 self.pool['ir.model.data'].ir_set(self.cr, SUPERUSER_ID, 'action', \
725 'tree_but_open', 'Menuitem', [('ir.ui.menu', int(parent_id))], action, True, True, xml_id=node.id)
727 def process_act_window(self, node):
728 assert getattr(node, 'id'), "Attribute %s of act_window is empty !" % ('id',)
729 assert getattr(node, 'name'), "Attribute %s of act_window is empty !" % ('name',)
730 assert getattr(node, 'res_model'), "Attribute %s of act_window is empty !" % ('res_model',)
731 self.validate_xml_id(node.id)
734 view_id = self.get_id(node.view)
737 context = eval(str(node.context), self.eval_context)
740 'type': node.type or 'ir.actions.act_window',
742 'domain': node.domain,
744 'res_model': node.res_model,
745 'src_model': node.src_model,
746 'view_type': node.view_type or 'form',
747 'view_mode': node.view_mode or 'tree,form',
750 'auto_refresh': node.auto_refresh,
751 'multi': getattr(node, 'multi', False),
754 self._set_group_values(node, values)
757 values['target'] = node.target
758 id = self.pool['ir.model.data']._update(self.cr, SUPERUSER_ID, \
759 'ir.actions.act_window', self.module, values, node.id, mode=self.mode)
760 self.id_map[node.id] = int(id)
763 keyword = 'client_action_relate'
764 value = 'ir.actions.act_window,%s' % id
765 replace = node.replace or True
766 self.pool['ir.model.data'].ir_set(self.cr, SUPERUSER_ID, 'action', keyword, \
767 node.id, [node.src_model], value, replace=replace, noupdate=self.isnoupdate(node), isobject=True, xml_id=node.id)
768 # TODO add remove ir.model.data
770 def process_delete(self, node):
771 assert getattr(node, 'model'), "Attribute %s of delete tag is empty !" % ('model',)
772 if node.model in self.pool:
774 ids = self.pool[node.model].search(self.cr, self.uid, eval(node.search, self.eval_context))
776 ids = [self.get_id(node.id)]
778 self.pool[node.model].unlink(self.cr, self.uid, ids)
780 self._log("Record not deleted.")
782 def process_url(self, node):
783 self.validate_xml_id(node.id)
785 res = {'name': node.name, 'url': node.url, 'target': node.target}
787 id = self.pool['ir.model.data']._update(self.cr, SUPERUSER_ID, \
788 "ir.actions.act_url", self.module, res, node.id, mode=self.mode)
789 self.id_map[node.id] = int(id)
791 if (not node.menu or eval(node.menu)) and id:
792 keyword = node.keyword or 'client_action_multi'
793 value = 'ir.actions.act_url,%s' % id
794 replace = node.replace or True
795 self.pool['ir.model.data'].ir_set(self.cr, SUPERUSER_ID, 'action', \
796 keyword, node.url, ["ir.actions.act_url"], value, replace=replace, \
797 noupdate=self.isnoupdate(node), isobject=True, xml_id=node.id)
799 def process_ir_set(self, node):
800 if not self.mode == 'init':
802 _, fields = node.items()[0]
804 for fieldname, expression in fields.items():
805 if is_eval(expression):
806 value = eval(expression.expression, self.eval_context)
809 res[fieldname] = value
810 self.pool['ir.model.data'].ir_set(self.cr, SUPERUSER_ID, res['key'], res['key2'], \
811 res['name'], res['models'], res['value'], replace=res.get('replace',True), \
812 isobject=res.get('isobject', False), meta=res.get('meta',None))
814 def process_report(self, node):
816 for dest, f in (('name','string'), ('model','model'), ('report_name','name')):
817 values[dest] = getattr(node, f)
818 assert values[dest], "Attribute %s of report is empty !" % (f,)
819 for field,dest in (('rml','report_rml'),('file','report_rml'),('xml','report_xml'),('xsl','report_xsl'),('attachment','attachment'),('attachment_use','attachment_use')):
820 if getattr(node, field):
821 values[dest] = getattr(node, field)
823 values['auto'] = eval(node.auto)
825 sxw_file = misc.file_open(node.sxw)
827 sxw_content = sxw_file.read()
828 values['report_sxw_content'] = sxw_content
832 values['header'] = eval(node.header)
833 values['multi'] = node.multi and eval(node.multi)
835 self.validate_xml_id(xml_id)
837 self._set_group_values(node, values)
839 id = self.pool['ir.model.data']._update(self.cr, SUPERUSER_ID, "ir.actions.report.xml", \
840 self.module, values, xml_id, noupdate=self.isnoupdate(node), mode=self.mode)
841 self.id_map[xml_id] = int(id)
843 if not node.menu or eval(node.menu):
844 keyword = node.keyword or 'client_print_multi'
845 value = 'ir.actions.report.xml,%s' % id
846 replace = node.replace or True
847 self.pool['ir.model.data'].ir_set(self.cr, SUPERUSER_ID, 'action', \
848 keyword, values['name'], [values['model']], value, replace=replace, isobject=True, xml_id=xml_id)
850 def process_none(self):
852 Empty node or commented node should not pass silently.
854 self._log_assert_failure("You have an empty block in your tests.")
857 def process(self, yaml_string):
859 Processes a Yaml string. Custom tags are interpreted by 'process_' instance methods.
861 yaml_tag.add_constructors()
863 is_preceded_by_comment = False
864 for node in yaml.load(yaml_string):
865 is_preceded_by_comment = self._log_node(node, is_preceded_by_comment)
867 self._process_node(node)
872 def _process_node(self, node):
874 self.process_comment(node)
875 elif is_assert(node):
876 self.process_assert(node)
877 elif is_record(node):
878 self.process_record(node)
879 elif is_python(node):
880 self.process_python(node)
881 elif is_menuitem(node):
882 self.process_menuitem(node)
883 elif is_delete(node):
884 self.process_delete(node)
886 self.process_url(node)
887 elif is_context(node):
888 self.process_context(node)
889 elif is_ir_set(node):
890 self.process_ir_set(node)
891 elif is_act_window(node):
892 self.process_act_window(node)
893 elif is_report(node):
894 self.process_report(node)
895 elif is_workflow(node):
896 if isinstance(node, types.DictionaryType):
897 self.process_workflow(node)
899 self.process_workflow({node: []})
900 elif is_function(node):
901 if isinstance(node, types.DictionaryType):
902 self.process_function(node)
904 self.process_function({node: []})
908 raise YamlImportException("Can not process YAML block: %s" % node)
910 def _log_node(self, node, is_preceded_by_comment):
912 is_preceded_by_comment = True
914 elif not is_preceded_by_comment:
915 if isinstance(node, types.DictionaryType):
916 msg = "Creating %s\n with %s"
917 args = node.items()[0]
918 self._log(msg, *args)
922 is_preceded_by_comment = False
923 return is_preceded_by_comment
925 def yaml_import(cr, module, yamlfile, kind, idref=None, mode='init', noupdate=False, report=None):
928 loglevel = logging.INFO if kind == 'test' else logging.DEBUG
929 yaml_string = yamlfile.read()
930 yaml_interpreter = YamlInterpreter(cr, module, idref, mode, filename=yamlfile.name, report=report, noupdate=noupdate, loglevel=loglevel)
931 yaml_interpreter.process(yaml_string)
933 # keeps convention of convert.py
934 convert_yaml_import = yaml_import
936 def threaded_yaml_import(db_name, module_name, file_name, delay=0):
942 cr = sql_db.db_connect(db_name).cursor()
943 fp = misc.file_open(file_name)
944 convert_yaml_import(cr, module_name, fp, {}, 'update', True)
948 threading.Thread(target=f).start()
951 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: