1 # -*- coding: utf-8 -*-
4 import time # used to eval time.strftime expressions
5 from datetime import datetime, timedelta
8 import openerp.pooler as pooler
9 import openerp.sql_db as sql_db
11 from config import config
15 from lxml import etree
16 from openerp import SUPERUSER_ID
18 # YAML import needs both safe and unsafe eval, but let's
21 from safe_eval import safe_eval as eval
23 import assertion_report
25 _logger = logging.getLogger(__name__)
27 class YamlImportException(Exception):
30 class YamlImportAbortion(Exception):
33 def _is_yaml_mapping(node, tag_constructor):
34 value = isinstance(node, types.DictionaryType) \
35 and len(node.keys()) == 1 \
36 and isinstance(node.keys()[0], tag_constructor)
40 return isinstance(node, types.StringTypes)
43 return isinstance(node, yaml_tag.Assert) \
44 or _is_yaml_mapping(node, yaml_tag.Assert)
47 return _is_yaml_mapping(node, yaml_tag.Record)
50 return _is_yaml_mapping(node, yaml_tag.Python)
52 def is_menuitem(node):
53 return isinstance(node, yaml_tag.Menuitem) \
54 or _is_yaml_mapping(node, yaml_tag.Menuitem)
56 def is_function(node):
57 return isinstance(node, yaml_tag.Function) \
58 or _is_yaml_mapping(node, yaml_tag.Function)
61 return isinstance(node, yaml_tag.Report)
63 def is_workflow(node):
64 return isinstance(node, yaml_tag.Workflow)
66 def is_act_window(node):
67 return isinstance(node, yaml_tag.ActWindow)
70 return isinstance(node, yaml_tag.Delete)
73 return isinstance(node, yaml_tag.Context)
76 return isinstance(node, yaml_tag.Url)
79 return isinstance(node, yaml_tag.Eval)
82 return isinstance(node, yaml_tag.Ref) \
83 or _is_yaml_mapping(node, yaml_tag.Ref)
86 return _is_yaml_mapping(node, yaml_tag.IrSet)
89 return isinstance(node, basestring)
91 class RecordDictWrapper(dict):
93 Used to pass a record as locals in eval:
94 records do not strictly behave like dict, so we force them to.
96 def __init__(self, record):
98 def __getitem__(self, key):
99 if key in self.record:
100 return self.record[key]
101 return dict.__getitem__(self, key)
103 class YamlInterpreter(object):
104 def __init__(self, cr, module, id_map, mode, filename, report=None, noupdate=False, loglevel=logging.DEBUG):
109 self.filename = filename
111 report = assertion_report.assertion_report()
112 self.assertion_report = report
113 self.noupdate = noupdate
114 self.loglevel = loglevel
115 self.pool = pooler.get_pool(cr.dbname)
117 self.context = {} # opererp context
118 self.eval_context = {'ref': self._ref(),
119 '_ref': self._ref(), # added '_ref' so that record['ref'] is possible
121 'datetime': datetime,
122 'timedelta': timedelta}
124 def _log(self, *args, **kwargs):
125 _logger.log(self.loglevel, *args, **kwargs)
128 return lambda xml_id: self.get_id(xml_id)
130 def get_model(self, model_name):
131 model = self.pool.get(model_name)
132 assert model, "The model %s does not exist." % (model_name,)
135 def validate_xml_id(self, xml_id):
138 module, id = xml_id.split('.', 1)
139 assert '.' not in id, "The ID reference '%s' must contains maximum one dot.\n" \
140 "It is used to refer to other modules ID, in the form: module.record_id" \
142 if module != self.module:
143 module_count = self.pool.get('ir.module.module').search_count(self.cr, self.uid, \
144 ['&', ('name', '=', module), ('state', 'in', ['installed'])])
145 assert module_count == 1, 'The ID "%s" refers to an uninstalled module.' % (xml_id,)
146 if len(id) > 64: # TODO where does 64 come from (DB is 128)? should be a constant or loaded form DB
147 _logger.error('id: %s is to long (max: 64)', id)
149 def get_id(self, xml_id):
150 if xml_id is False or xml_id is None:
153 # raise YamlImportException("The xml_id should be a non empty string.")
154 elif isinstance(xml_id, types.IntType):
156 elif xml_id in self.id_map:
157 id = self.id_map[xml_id]
160 module, checked_xml_id = xml_id.split('.', 1)
163 checked_xml_id = xml_id
165 _, id = self.pool.get('ir.model.data').get_object_reference(self.cr, self.uid, module, checked_xml_id)
166 self.id_map[xml_id] = id
168 raise ValueError("""%s not found when processing %s.
169 This Yaml file appears to depend on missing data. This often happens for
170 tests that belong to a module's test suite and depend on each other.""" % (checked_xml_id, self.filename))
174 def get_context(self, node, eval_dict):
175 context = self.context.copy()
177 context.update(eval(node.context, eval_dict))
180 def isnoupdate(self, node):
181 return self.noupdate or node.noupdate or False
183 def _get_first_result(self, results, default=False):
186 if isinstance(value, types.TupleType):
192 def process_comment(self, node):
195 def _log_assert_failure(self, msg, *args):
196 self.assertion_report.record_failure()
197 _logger.error(msg, *args)
199 def _get_assertion_id(self, assertion):
201 ids = [self.get_id(assertion.id)]
202 elif assertion.search:
203 q = eval(assertion.search, self.eval_context)
204 ids = self.pool.get(assertion.model).search(self.cr, self.uid, q, context=assertion.context)
206 raise YamlImportException('Nothing to assert: you must give either an id or a search criteria.')
209 def process_assert(self, node):
210 if isinstance(node, dict):
211 assertion, expressions = node.items()[0]
213 assertion, expressions = node, []
215 if self.isnoupdate(assertion) and self.mode != 'init':
216 _logger.warning('This assertion was not evaluated ("%s").', assertion.string)
218 model = self.get_model(assertion.model)
219 ids = self._get_assertion_id(assertion)
220 if assertion.count is not None and len(ids) != assertion.count:
221 msg = 'assertion "%s" failed!\n' \
222 ' Incorrect search count:\n' \
223 ' expected count: %d\n' \
224 ' obtained count: %d\n'
225 args = (assertion.string, assertion.count, len(ids))
226 self._log_assert_failure(msg, *args)
228 context = self.get_context(assertion, self.eval_context)
230 record = model.browse(self.cr, self.uid, id, context)
231 for test in expressions:
233 success = unsafe_eval(test, self.eval_context, RecordDictWrapper(record))
235 _logger.debug('Exception during evaluation of !assert block in yaml_file %s.', self.filename, exc_info=True)
236 raise YamlImportAbortion(e)
238 msg = 'Assertion "%s" FAILED\ntest: %s\n'
239 args = (assertion.string, test)
240 for aop in ('==', '!=', '<>', 'in', 'not in', '>=', '<=', '>', '<'):
242 left, right = test.split(aop,1)
246 lmsg = unsafe_eval(left, self.eval_context, RecordDictWrapper(record))
251 rmsg = unsafe_eval(right, self.eval_context, RecordDictWrapper(record))
255 msg += 'values: ! %s %s %s'
256 args += ( lmsg, aop, rmsg )
259 self._log_assert_failure(msg, *args)
261 else: # all tests were successful for this assertion tag (no break)
262 self.assertion_report.record_success()
264 def _coerce_bool(self, value, default=False):
265 if isinstance(value, types.BooleanType):
267 if isinstance(value, types.StringTypes):
268 b = value.strip().lower() not in ('0', 'false', 'off', 'no')
269 elif isinstance(value, types.IntType):
275 def create_osv_memory_record(self, record, fields):
276 model = self.get_model(record.model)
277 context = self.get_context(record, self.eval_context)
278 record_dict = self._create_record(model, fields)
279 id_new = model.create(self.cr, self.uid, record_dict, context=context)
280 self.id_map[record.id] = int(id_new)
283 def process_record(self, node):
284 import openerp.osv as osv
285 record, fields = node.items()[0]
286 model = self.get_model(record.model)
288 view_id = record.view
289 if view_id and (view_id is not True) and isinstance(view_id, basestring):
292 module, view_id = view_id.split('.',1)
293 view_id = self.pool.get('ir.model.data').get_object_reference(self.cr, SUPERUSER_ID, module, view_id)[1]
295 if model.is_transient():
296 record_dict=self.create_osv_memory_record(record, fields)
298 self.validate_xml_id(record.id)
300 self.pool.get('ir.model.data')._get_id(self.cr, SUPERUSER_ID, self.module, record.id)
305 if self.isnoupdate(record) and self.mode != 'init':
306 id = self.pool.get('ir.model.data')._update_dummy(self.cr, SUPERUSER_ID, record.model, self.module, record.id)
307 # check if the resource already existed at the last update
309 self.id_map[record] = int(id)
312 if not self._coerce_bool(record.forcecreate):
316 #context = self.get_context(record, self.eval_context)
317 #TOFIX: record.context like {'withoutemployee':True} should pass from self.eval_context. example: test_project.yml in project module
318 context = record.context
322 if view_id is True: varg = False
323 view_info = model.fields_view_get(self.cr, SUPERUSER_ID, varg, 'form', context)
325 record_dict = self._create_record(model, fields, view_info, default=default)
326 _logger.debug("RECORD_DICT %s" % record_dict)
327 id = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, record.model, \
328 self.module, record_dict, record.id, noupdate=self.isnoupdate(record), mode=self.mode, context=context)
329 self.id_map[record.id] = int(id)
330 if config.get('import_partial'):
333 def _create_record(self, model, fields, view_info=False, parent={}, default=True):
334 """This function processes the !record tag in yalm files. It simulates the record creation through an xml
335 view (either specified on the !record tag or the default one for this object), including the calls to
336 on_change() functions, and sending only values for fields that aren't set as readonly.
337 :param model: model instance
338 :param fields: dictonary mapping the field names and their values
339 :param view_info: result of fields_view_get() called on the object
340 :param parent: dictionary containing the values already computed for the parent, in case of one2many fields
341 :param default: if True, the default values must be processed too or not
342 :return: dictionary mapping the field names and their values, ready to use when calling the create() function
345 def _get_right_one2many_view(fg, field_name, view_type):
346 one2many_view = fg[field_name]['views'].get(view_type)
347 # if the view is not defined inline, we call fields_view_get()
348 if not one2many_view:
349 one2many_view = self.pool.get(fg[field_name]['relation']).fields_view_get(self.cr, SUPERUSER_ID, False, view_type, self.context)
352 def process_val(key, val):
353 if fg[key]['type'] == 'many2one':
354 if type(val) in (tuple,list):
356 elif fg[key]['type'] == 'one2many':
357 if val and isinstance(val, (list,tuple)) and isinstance(val[0], dict):
358 # we want to return only the fields that aren't readonly
359 # For that, we need to first get the right tree view to consider for the field `key´
360 one2many_tree_view = _get_right_one2many_view(fg, key, 'tree')
361 arch = etree.fromstring(one2many_tree_view['arch'].encode('utf-8'))
363 # make a copy for the iteration, as we will alter `rec´
364 rec_copy = rec.copy()
365 for field_key in rec_copy:
366 # if field is missing in view or has a readonly modifier, drop it
367 field_elem = arch.xpath("//field[@name='%s']" % field_key)
368 if field_elem and (field_elem[0].get('modifiers', '{}').find('"readonly": true') >= 0):
369 # TODO: currently we only support if readonly is True in the modifiers. Some improvement may be done in
370 # order to support also modifiers that look like {"readonly": [["state", "not in", ["draft", "confirm"]]]}
372 # now that unwanted values have been removed from val, we can encapsulate it in a tuple as returned value
373 val = map(lambda x: (0,0,x), val)
374 elif fg[key]['type'] == 'many2many':
375 if val and isinstance(val,(list,tuple)) and isinstance(val[0], (int,long)):
378 # we want to return only the fields that aren't readonly
379 if el.get('modifiers', '{}').find('"readonly": true') >= 0:
380 # TODO: currently we only support if readonly is True in the modifiers. Some improvement may be done in
381 # order to support also modifiers that look like {"readonly": [["state", "not in", ["draft", "confirm"]]]}
387 arch = etree.fromstring(view_info['arch'].decode('utf-8'))
388 view = arch if len(arch) else False
391 fields = fields or {}
392 if view is not False:
393 fg = view_info['fields']
394 # gather the default values on the object. (Can't use `fields´ as parameter instead of {} because we may
395 # have references like `base.main_company´ in the yaml file and it's not compatible with the function)
396 defaults = default and model._add_missing_default_values(self.cr, SUPERUSER_ID, {}, context=self.context) or {}
398 # copy the default values in record_dict, only if they are in the view (because that's what the client does)
399 # the other default values will be added later on by the create().
400 record_dict = dict([(key, val) for key, val in defaults.items() if key in fg])
402 # Process all on_change calls
407 field_name = el.attrib['name']
408 assert field_name in fg, "The field '%s' is defined in the form view but not on the object '%s'!" % (field_name, model._name)
409 if field_name in fields:
410 one2many_form_view = None
411 if (view is not False) and (fg[field_name]['type']=='one2many'):
412 # for one2many fields, we want to eval them using the inline form view defined on the parent
413 one2many_form_view = _get_right_one2many_view(fg, field_name, 'form')
415 field_value = self._eval_field(model, field_name, fields[field_name], one2many_form_view or view_info, parent=record_dict, default=default)
417 #call process_val to not update record_dict if values were given for readonly fields
418 val = process_val(field_name, field_value)
420 record_dict[field_name] = val
421 #if (field_name in defaults) and defaults[field_name] == field_value:
422 # print '*** You can remove these lines:', field_name, field_value
424 #if field_name has a default value or a value is given in the yaml file, we must call its on_change()
425 elif field_name not in defaults:
428 if not el.attrib.get('on_change', False):
430 match = re.match("([a-z_1-9A-Z]+)\((.*)\)", el.attrib['on_change'])
431 assert match, "Unable to parse the on_change '%s'!" % (el.attrib['on_change'], )
433 # creating the context
434 class parent2(object):
435 def __init__(self, d):
437 def __getattr__(self, name):
438 return self.d.get(name, False)
440 ctx = record_dict.copy()
441 ctx['context'] = self.context
442 ctx['uid'] = SUPERUSER_ID
443 ctx['parent'] = parent2(parent)
446 ctx[a] = process_val(a, defaults.get(a, False))
449 args = map(lambda x: eval(x, ctx), match.group(2).split(','))
450 result = getattr(model, match.group(1))(self.cr, SUPERUSER_ID, [], *args)
451 for key, val in (result or {}).get('value', {}).items():
452 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'])
453 record_dict[key] = process_val(key, val)
454 #if (key in fields) and record_dict[key] == process_val(key, val):
455 # print '*** You can remove these lines:', key, val
457 nodes = list(el) + nodes
461 for field_name, expression in fields.items():
462 if field_name in record_dict:
464 field_value = self._eval_field(model, field_name, expression, default=False)
465 record_dict[field_name] = field_value
468 def process_ref(self, node, column=None):
469 assert node.search or node.id, '!ref node should have a `search` attribute or `id` attribute'
472 model_name = node.model
474 model_name = column._obj
476 raise YamlImportException('You need to give a model for the search, or a column to infer it.')
477 model = self.get_model(model_name)
478 q = eval(node.search, self.eval_context)
479 ids = model.search(self.cr, self.uid, q)
481 instances = model.browse(self.cr, self.uid, ids)
482 value = [inst[node.use] for inst in instances]
486 value = self.get_id(node.id)
491 def process_eval(self, node):
492 return eval(node.expression, self.eval_context)
494 def _eval_field(self, model, field_name, expression, view_info=False, parent={}, default=True):
495 # TODO this should be refactored as something like model.get_field() in bin/osv
496 if field_name in model._columns:
497 column = model._columns[field_name]
498 elif field_name in model._inherit_fields:
499 column = model._inherit_fields[field_name][2]
501 raise KeyError("Object '%s' does not contain field '%s'" % (model, field_name))
502 if is_ref(expression):
503 elements = self.process_ref(expression, column)
504 if column._type in ("many2many", "one2many"):
505 value = [(6, 0, elements)]
507 if isinstance(elements, (list,tuple)):
508 value = self._get_first_result(elements)
511 elif column._type == "many2one":
512 value = self.get_id(expression)
513 elif column._type == "one2many":
514 other_model = self.get_model(column._obj)
515 value = [(0, 0, self._create_record(other_model, fields, view_info, parent, default=default)) for fields in expression]
516 elif column._type == "many2many":
517 ids = [self.get_id(xml_id) for xml_id in expression]
518 value = [(6, 0, ids)]
519 elif column._type == "date" and is_string(expression):
520 # enforce ISO format for string date values, to be locale-agnostic during tests
521 time.strptime(expression, misc.DEFAULT_SERVER_DATE_FORMAT)
523 elif column._type == "datetime" and is_string(expression):
524 # enforce ISO format for string datetime values, to be locale-agnostic during tests
525 time.strptime(expression, misc.DEFAULT_SERVER_DATETIME_FORMAT)
528 if is_eval(expression):
529 value = self.process_eval(expression)
532 # raise YamlImportException('Unsupported column "%s" or value %s:%s' % (field_name, type(expression), expression))
535 def process_context(self, node):
536 self.context = node.__dict__
538 self.uid = self.get_id(node.uid)
540 self.noupdate = node.noupdate
542 def process_python(self, node):
543 python, statements = node.items()[0]
544 model = self.get_model(python.model)
545 statements = statements.replace("\r\n", "\n")
546 code_context = { 'model': model, 'cr': self.cr, 'uid': self.uid, 'log': self._log, 'context': self.context }
547 code_context.update({'self': model}) # remove me when no !python block test uses 'self' anymore
549 code_obj = compile(statements, self.filename, 'exec')
550 unsafe_eval(code_obj, {'ref': self.get_id}, code_context)
551 except AssertionError, e:
552 self._log_assert_failure('AssertionError in Python code %s: %s', python.name, e)
555 _logger.debug('Exception during evaluation of !python block in yaml_file %s.', self.filename, exc_info=True)
558 self.assertion_report.record_success()
560 def process_workflow(self, node):
561 workflow, values = node.items()[0]
562 if self.isnoupdate(workflow) and self.mode != 'init':
565 id = self.get_id(workflow.ref)
568 raise YamlImportException('You must define a child node if you do not give a ref.')
569 if not len(values) == 1:
570 raise YamlImportException('Only one child node is accepted (%d given).' % len(values))
572 if not 'model' in value and (not 'eval' in value or not 'search' in value):
573 raise YamlImportException('You must provide a "model" and an "eval" or "search" to evaluate.')
574 value_model = self.get_model(value['model'])
575 local_context = {'obj': lambda x: value_model.browse(self.cr, self.uid, x, context=self.context)}
576 local_context.update(self.id_map)
577 id = eval(value['eval'], self.eval_context, local_context)
579 if workflow.uid is not None:
583 self.cr.execute('select distinct signal from wkf_transition')
584 signals=[x['signal'] for x in self.cr.dictfetchall()]
585 if workflow.action not in signals:
586 raise YamlImportException('Incorrect action %s. No such action defined' % workflow.action)
587 import openerp.netsvc as netsvc
588 wf_service = netsvc.LocalService("workflow")
589 wf_service.trg_validate(uid, workflow.model, id, workflow.action, self.cr)
591 def _eval_params(self, model, params):
593 for i, param in enumerate(params):
594 if isinstance(param, types.ListType):
595 value = self._eval_params(model, param)
597 value = self.process_ref(param)
599 value = self.process_eval(param)
600 elif isinstance(param, types.DictionaryType): # supports XML syntax
601 param_model = self.get_model(param.get('model', model))
602 if 'search' in param:
603 q = eval(param['search'], self.eval_context)
604 ids = param_model.search(self.cr, self.uid, q)
605 value = self._get_first_result(ids)
606 elif 'eval' in param:
607 local_context = {'obj': lambda x: param_model.browse(self.cr, self.uid, x, self.context)}
608 local_context.update(self.id_map)
609 value = eval(param['eval'], self.eval_context, local_context)
611 raise YamlImportException('You must provide either a !ref or at least a "eval" or a "search" to function parameter #%d.' % i)
613 value = param # scalar value
617 def process_function(self, node):
618 function, params = node.items()[0]
619 if self.isnoupdate(function) and self.mode != 'init':
621 model = self.get_model(function.model)
623 args = self.process_eval(function.eval)
625 args = self._eval_params(function.model, params)
626 method = function.name
627 getattr(model, method)(self.cr, self.uid, *args)
629 def _set_group_values(self, node, values):
631 group_names = node.groups.split(',')
633 for group in group_names:
634 if group.startswith('-'):
635 group_id = self.get_id(group[1:])
636 groups_value.append((3, group_id))
638 group_id = self.get_id(group)
639 groups_value.append((4, group_id))
640 values['groups_id'] = groups_value
642 def process_menuitem(self, node):
643 self.validate_xml_id(node.id)
647 self.cr.execute('select id from ir_ui_menu where parent_id is null and name=%s', (node.name,))
648 res = self.cr.fetchone()
649 values = {'parent_id': parent_id, 'name': node.name}
651 parent_id = self.get_id(node.parent)
652 values = {'parent_id': parent_id}
654 values['name'] = node.name
656 res = [ self.get_id(node.id) ]
657 except: # which exception ?
661 action_type = node.type or 'act_window'
663 "act_window": 'STOCK_NEW',
664 "report.xml": 'STOCK_PASTE',
665 "wizard": 'STOCK_EXECUTE',
666 "url": 'STOCK_JUMP_TO',
668 values['icon'] = icons.get(action_type, 'STOCK_NEW')
669 if action_type == 'act_window':
670 action_id = self.get_id(node.action)
671 self.cr.execute('select view_type,view_mode,name,view_id,target from ir_act_window where id=%s', (action_id,))
672 ir_act_window_result = self.cr.fetchone()
673 assert ir_act_window_result, "No window action defined for this id %s !\n" \
674 "Verify that this is a window action or add a type argument." % (node.action,)
675 action_type, action_mode, action_name, view_id, target = ir_act_window_result
677 self.cr.execute('SELECT type FROM ir_ui_view WHERE id=%s', (view_id,))
678 # TODO guess why action_mode is ir_act_window.view_mode above and ir_ui_view.type here
679 action_mode = self.cr.fetchone()
680 self.cr.execute('SELECT view_mode FROM ir_act_window_view WHERE act_window_id=%s ORDER BY sequence LIMIT 1', (action_id,))
682 action_mode = self.cr.fetchone()
683 if action_type == 'tree':
684 values['icon'] = 'STOCK_INDENT'
685 elif action_mode and action_mode.startswith('tree'):
686 values['icon'] = 'STOCK_JUSTIFY_FILL'
687 elif action_mode and action_mode.startswith('graph'):
688 values['icon'] = 'terp-graph'
689 elif action_mode and action_mode.startswith('calendar'):
690 values['icon'] = 'terp-calendar'
692 values['icon'] = 'STOCK_EXECUTE'
693 if not values.get('name', False):
694 values['name'] = action_name
695 elif action_type == 'wizard':
696 action_id = self.get_id(node.action)
697 self.cr.execute('select name from ir_act_wizard where id=%s', (action_id,))
698 ir_act_wizard_result = self.cr.fetchone()
699 if (not values.get('name', False)) and ir_act_wizard_result:
700 values['name'] = ir_act_wizard_result[0]
702 raise YamlImportException("Unsupported type '%s' in menuitem tag." % action_type)
704 values['sequence'] = node.sequence
706 values['icon'] = node.icon
708 self._set_group_values(node, values)
710 pid = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, \
711 'ir.ui.menu', self.module, values, node.id, mode=self.mode, \
712 noupdate=self.isnoupdate(node), res_id=res and res[0] or False)
714 if node.id and parent_id:
715 self.id_map[node.id] = int(parent_id)
717 if node.action and pid:
718 action_type = node.type or 'act_window'
719 action_id = self.get_id(node.action)
720 action = "ir.actions.%s,%d" % (action_type, action_id)
721 self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, 'action', \
722 'tree_but_open', 'Menuitem', [('ir.ui.menu', int(parent_id))], action, True, True, xml_id=node.id)
724 def process_act_window(self, node):
725 assert getattr(node, 'id'), "Attribute %s of act_window is empty !" % ('id',)
726 assert getattr(node, 'name'), "Attribute %s of act_window is empty !" % ('name',)
727 assert getattr(node, 'res_model'), "Attribute %s of act_window is empty !" % ('res_model',)
728 self.validate_xml_id(node.id)
731 view_id = self.get_id(node.view)
734 context = eval(str(node.context), self.eval_context)
737 'type': node.type or 'ir.actions.act_window',
739 'domain': node.domain,
741 'res_model': node.res_model,
742 'src_model': node.src_model,
743 'view_type': node.view_type or 'form',
744 'view_mode': node.view_mode or 'tree,form',
747 'auto_refresh': node.auto_refresh,
748 'multi': getattr(node, 'multi', False),
751 self._set_group_values(node, values)
754 values['target'] = node.target
755 id = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, \
756 'ir.actions.act_window', self.module, values, node.id, mode=self.mode)
757 self.id_map[node.id] = int(id)
760 keyword = 'client_action_relate'
761 value = 'ir.actions.act_window,%s' % id
762 replace = node.replace or True
763 self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, 'action', keyword, \
764 node.id, [node.src_model], value, replace=replace, noupdate=self.isnoupdate(node), isobject=True, xml_id=node.id)
765 # TODO add remove ir.model.data
767 def process_delete(self, node):
768 assert getattr(node, 'model'), "Attribute %s of delete tag is empty !" % ('model',)
769 if self.pool.get(node.model):
771 ids = self.pool.get(node.model).search(self.cr, self.uid, eval(node.search, self.eval_context))
773 ids = [self.get_id(node.id)]
775 self.pool.get(node.model).unlink(self.cr, self.uid, ids)
777 self._log("Record not deleted.")
779 def process_url(self, node):
780 self.validate_xml_id(node.id)
782 res = {'name': node.name, 'url': node.url, 'target': node.target}
784 id = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, \
785 "ir.actions.act_url", self.module, res, node.id, mode=self.mode)
786 self.id_map[node.id] = int(id)
788 if (not node.menu or eval(node.menu)) and id:
789 keyword = node.keyword or 'client_action_multi'
790 value = 'ir.actions.act_url,%s' % id
791 replace = node.replace or True
792 self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, 'action', \
793 keyword, node.url, ["ir.actions.act_url"], value, replace=replace, \
794 noupdate=self.isnoupdate(node), isobject=True, xml_id=node.id)
796 def process_ir_set(self, node):
797 if not self.mode == 'init':
799 _, fields = node.items()[0]
801 for fieldname, expression in fields.items():
802 if is_eval(expression):
803 value = eval(expression.expression, self.eval_context)
806 res[fieldname] = value
807 self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, res['key'], res['key2'], \
808 res['name'], res['models'], res['value'], replace=res.get('replace',True), \
809 isobject=res.get('isobject', False), meta=res.get('meta',None))
811 def process_report(self, node):
813 for dest, f in (('name','string'), ('model','model'), ('report_name','name')):
814 values[dest] = getattr(node, f)
815 assert values[dest], "Attribute %s of report is empty !" % (f,)
816 for field,dest in (('rml','report_rml'),('file','report_rml'),('xml','report_xml'),('xsl','report_xsl'),('attachment','attachment'),('attachment_use','attachment_use')):
817 if getattr(node, field):
818 values[dest] = getattr(node, field)
820 values['auto'] = eval(node.auto)
822 sxw_file = misc.file_open(node.sxw)
824 sxw_content = sxw_file.read()
825 values['report_sxw_content'] = sxw_content
829 values['header'] = eval(node.header)
830 values['multi'] = node.multi and eval(node.multi)
832 self.validate_xml_id(xml_id)
834 self._set_group_values(node, values)
836 id = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, "ir.actions.report.xml", \
837 self.module, values, xml_id, noupdate=self.isnoupdate(node), mode=self.mode)
838 self.id_map[xml_id] = int(id)
840 if not node.menu or eval(node.menu):
841 keyword = node.keyword or 'client_print_multi'
842 value = 'ir.actions.report.xml,%s' % id
843 replace = node.replace or True
844 self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, 'action', \
845 keyword, values['name'], [values['model']], value, replace=replace, isobject=True, xml_id=xml_id)
847 def process_none(self):
849 Empty node or commented node should not pass silently.
851 self._log_assert_failure("You have an empty block in your tests.")
854 def process(self, yaml_string):
856 Processes a Yaml string. Custom tags are interpreted by 'process_' instance methods.
858 yaml_tag.add_constructors()
860 is_preceded_by_comment = False
861 for node in yaml.load(yaml_string):
862 is_preceded_by_comment = self._log_node(node, is_preceded_by_comment)
864 self._process_node(node)
869 def _process_node(self, node):
871 self.process_comment(node)
872 elif is_assert(node):
873 self.process_assert(node)
874 elif is_record(node):
875 self.process_record(node)
876 elif is_python(node):
877 self.process_python(node)
878 elif is_menuitem(node):
879 self.process_menuitem(node)
880 elif is_delete(node):
881 self.process_delete(node)
883 self.process_url(node)
884 elif is_context(node):
885 self.process_context(node)
886 elif is_ir_set(node):
887 self.process_ir_set(node)
888 elif is_act_window(node):
889 self.process_act_window(node)
890 elif is_report(node):
891 self.process_report(node)
892 elif is_workflow(node):
893 if isinstance(node, types.DictionaryType):
894 self.process_workflow(node)
896 self.process_workflow({node: []})
897 elif is_function(node):
898 if isinstance(node, types.DictionaryType):
899 self.process_function(node)
901 self.process_function({node: []})
905 raise YamlImportException("Can not process YAML block: %s" % node)
907 def _log_node(self, node, is_preceded_by_comment):
909 is_preceded_by_comment = True
911 elif not is_preceded_by_comment:
912 if isinstance(node, types.DictionaryType):
913 msg = "Creating %s\n with %s"
914 args = node.items()[0]
915 self._log(msg, *args)
919 is_preceded_by_comment = False
920 return is_preceded_by_comment
922 def yaml_import(cr, module, yamlfile, kind, idref=None, mode='init', noupdate=False, report=None):
925 loglevel = logging.TEST if kind == 'test' else logging.DEBUG
926 yaml_string = yamlfile.read()
927 yaml_interpreter = YamlInterpreter(cr, module, idref, mode, filename=yamlfile.name, report=report, noupdate=noupdate, loglevel=loglevel)
928 yaml_interpreter.process(yaml_string)
930 # keeps convention of convert.py
931 convert_yaml_import = yaml_import
933 def threaded_yaml_import(db_name, module_name, file_name, delay=0):
939 cr = sql_db.db_connect(db_name).cursor()
940 fp = misc.file_open(file_name)
941 convert_yaml_import(cr, module_name, fp, {}, 'update', True)
945 threading.Thread(target=f).start()
948 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: