1 # -*- coding: utf-8 -*-
5 import time # used to eval time.strftime expressions
6 from datetime import datetime, timedelta
9 from copy import deepcopy
10 import openerp.pooler as pooler
11 import openerp.sql_db as sql_db
13 from config import config
17 from lxml import etree
18 from openerp import SUPERUSER_ID
20 # YAML import needs both safe and unsafe eval, but let's
23 from safe_eval import safe_eval as eval
25 import assertion_report
27 _logger = logging.getLogger(__name__)
29 class YamlImportException(Exception):
32 class YamlImportAbortion(Exception):
35 def _is_yaml_mapping(node, tag_constructor):
36 value = isinstance(node, types.DictionaryType) \
37 and len(node.keys()) == 1 \
38 and isinstance(node.keys()[0], tag_constructor)
42 return isinstance(node, types.StringTypes)
45 return isinstance(node, yaml_tag.Assert) \
46 or _is_yaml_mapping(node, yaml_tag.Assert)
49 return _is_yaml_mapping(node, yaml_tag.Record)
52 return _is_yaml_mapping(node, yaml_tag.Python)
54 def is_menuitem(node):
55 return isinstance(node, yaml_tag.Menuitem) \
56 or _is_yaml_mapping(node, yaml_tag.Menuitem)
58 def is_function(node):
59 return isinstance(node, yaml_tag.Function) \
60 or _is_yaml_mapping(node, yaml_tag.Function)
63 return isinstance(node, yaml_tag.Report)
65 def is_workflow(node):
66 return isinstance(node, yaml_tag.Workflow)
68 def is_act_window(node):
69 return isinstance(node, yaml_tag.ActWindow)
72 return isinstance(node, yaml_tag.Delete)
75 return isinstance(node, yaml_tag.Context)
78 return isinstance(node, yaml_tag.Url)
81 return isinstance(node, yaml_tag.Eval)
84 return isinstance(node, yaml_tag.Ref) \
85 or _is_yaml_mapping(node, yaml_tag.Ref)
88 return _is_yaml_mapping(node, yaml_tag.IrSet)
91 return isinstance(node, basestring)
93 class RecordDictWrapper(dict):
95 Used to pass a record as locals in eval:
96 records do not strictly behave like dict, so we force them to.
98 def __init__(self, record):
100 def __getitem__(self, key):
101 if key in self.record:
102 return self.record[key]
103 return dict.__getitem__(self, key)
105 class YamlInterpreter(object):
106 def __init__(self, cr, module, id_map, mode, filename, report=None, noupdate=False, loglevel=logging.DEBUG):
111 self.filename = filename
113 report = assertion_report.assertion_report()
114 self.assertion_report = report
115 self.noupdate = noupdate
116 self.loglevel = loglevel
117 self.pool = pooler.get_pool(cr.dbname)
119 self.context = {} # opererp context
120 self.eval_context = {'ref': self._ref(),
121 '_ref': self._ref(), # added '_ref' so that record['ref'] is possible
123 'datetime': datetime,
124 'timedelta': timedelta}
126 def _log(self, *args, **kwargs):
127 _logger.log(self.loglevel, *args, **kwargs)
130 return lambda xml_id: self.get_id(xml_id)
132 def get_model(self, model_name):
133 model = self.pool.get(model_name)
134 assert model, "The model %s does not exist." % (model_name,)
137 def validate_xml_id(self, xml_id):
140 module, id = xml_id.split('.', 1)
141 assert '.' not in id, "The ID reference '%s' must contains maximum one dot.\n" \
142 "It is used to refer to other modules ID, in the form: module.record_id" \
144 if module != self.module:
145 module_count = self.pool.get('ir.module.module').search_count(self.cr, self.uid, \
146 ['&', ('name', '=', module), ('state', 'in', ['installed'])])
147 assert module_count == 1, 'The ID "%s" refers to an uninstalled module.' % (xml_id,)
148 if len(id) > 64: # TODO where does 64 come from (DB is 128)? should be a constant or loaded form DB
149 _logger.error('id: %s is to long (max: 64)', id)
151 def get_id(self, xml_id):
152 if xml_id is False or xml_id is None:
155 # raise YamlImportException("The xml_id should be a non empty string.")
156 elif isinstance(xml_id, types.IntType):
158 elif xml_id in self.id_map:
159 id = self.id_map[xml_id]
162 module, checked_xml_id = xml_id.split('.', 1)
165 checked_xml_id = xml_id
167 _, id = self.pool.get('ir.model.data').get_object_reference(self.cr, self.uid, module, checked_xml_id)
168 self.id_map[xml_id] = id
170 raise ValueError("""%s not found when processing %s.
171 This Yaml file appears to depend on missing data. This often happens for
172 tests that belong to a module's test suite and depend on each other.""" % (checked_xml_id, self.filename))
176 def get_context(self, node, eval_dict):
177 context = self.context.copy()
179 context.update(eval(node.context, eval_dict))
182 def isnoupdate(self, node):
183 return self.noupdate or node.noupdate or False
185 def _get_first_result(self, results, default=False):
188 if isinstance(value, types.TupleType):
194 def process_comment(self, node):
197 def _log_assert_failure(self, msg, *args):
198 from openerp.modules import module # cannot be made before (loop)
199 basepath = module.get_module_path(self.module)
200 self.assertion_report.record_failure(
201 details=dict(module=self.module,
202 testfile=os.path.relpath(self.filename, basepath),
204 msg_args=deepcopy(args)))
205 _logger.error(msg, *args)
207 def _get_assertion_id(self, assertion):
209 ids = [self.get_id(assertion.id)]
210 elif assertion.search:
211 q = eval(assertion.search, self.eval_context)
212 ids = self.pool.get(assertion.model).search(self.cr, self.uid, q, context=assertion.context)
214 raise YamlImportException('Nothing to assert: you must give either an id or a search criteria.')
217 def process_assert(self, node):
218 if isinstance(node, dict):
219 assertion, expressions = node.items()[0]
221 assertion, expressions = node, []
223 if self.isnoupdate(assertion) and self.mode != 'init':
224 _logger.warning('This assertion was not evaluated ("%s").', assertion.string)
226 model = self.get_model(assertion.model)
227 ids = self._get_assertion_id(assertion)
228 if assertion.count is not None and len(ids) != assertion.count:
229 msg = 'assertion "%s" failed!\n' \
230 ' Incorrect search count:\n' \
231 ' expected count: %d\n' \
232 ' obtained count: %d\n'
233 args = (assertion.string, assertion.count, len(ids))
234 self._log_assert_failure(msg, *args)
236 context = self.get_context(assertion, self.eval_context)
238 record = model.browse(self.cr, self.uid, id, context)
239 for test in expressions:
241 success = unsafe_eval(test, self.eval_context, RecordDictWrapper(record))
243 _logger.debug('Exception during evaluation of !assert block in yaml_file %s.', self.filename, exc_info=True)
244 raise YamlImportAbortion(e)
246 msg = 'Assertion "%s" FAILED\ntest: %s\n'
247 args = (assertion.string, test)
248 for aop in ('==', '!=', '<>', 'in', 'not in', '>=', '<=', '>', '<'):
250 left, right = test.split(aop,1)
254 lmsg = unsafe_eval(left, self.eval_context, RecordDictWrapper(record))
259 rmsg = unsafe_eval(right, self.eval_context, RecordDictWrapper(record))
263 msg += 'values: ! %s %s %s'
264 args += ( lmsg, aop, rmsg )
267 self._log_assert_failure(msg, *args)
269 else: # all tests were successful for this assertion tag (no break)
270 self.assertion_report.record_success()
272 def _coerce_bool(self, value, default=False):
273 if isinstance(value, types.BooleanType):
275 if isinstance(value, types.StringTypes):
276 b = value.strip().lower() not in ('0', 'false', 'off', 'no')
277 elif isinstance(value, types.IntType):
283 def create_osv_memory_record(self, record, fields):
284 model = self.get_model(record.model)
285 context = self.get_context(record, self.eval_context)
286 record_dict = self._create_record(model, fields)
287 id_new = model.create(self.cr, self.uid, record_dict, context=context)
288 self.id_map[record.id] = int(id_new)
291 def process_record(self, node):
292 import openerp.osv as osv
293 record, fields = node.items()[0]
294 model = self.get_model(record.model)
296 view_id = record.view
297 if view_id and (view_id is not True) and isinstance(view_id, basestring):
300 module, view_id = view_id.split('.',1)
301 view_id = self.pool.get('ir.model.data').get_object_reference(self.cr, SUPERUSER_ID, module, view_id)[1]
303 if model.is_transient():
304 record_dict=self.create_osv_memory_record(record, fields)
306 self.validate_xml_id(record.id)
308 self.pool.get('ir.model.data')._get_id(self.cr, SUPERUSER_ID, self.module, record.id)
313 if self.isnoupdate(record) and self.mode != 'init':
314 id = self.pool.get('ir.model.data')._update_dummy(self.cr, SUPERUSER_ID, record.model, self.module, record.id)
315 # check if the resource already existed at the last update
317 self.id_map[record] = int(id)
320 if not self._coerce_bool(record.forcecreate):
324 #context = self.get_context(record, self.eval_context)
325 #TOFIX: record.context like {'withoutemployee':True} should pass from self.eval_context. example: test_project.yml in project module
326 context = record.context
330 if view_id is True: varg = False
331 view_info = model.fields_view_get(self.cr, SUPERUSER_ID, varg, 'form', context)
333 record_dict = self._create_record(model, fields, view_info, default=default)
334 _logger.debug("RECORD_DICT %s" % record_dict)
335 id = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, record.model, \
336 self.module, record_dict, record.id, noupdate=self.isnoupdate(record), mode=self.mode, context=context)
337 self.id_map[record.id] = int(id)
338 if config.get('import_partial'):
341 def _create_record(self, model, fields, view_info=False, parent={}, default=True):
342 """This function processes the !record tag in yalm files. It simulates the record creation through an xml
343 view (either specified on the !record tag or the default one for this object), including the calls to
344 on_change() functions, and sending only values for fields that aren't set as readonly.
345 :param model: model instance
346 :param fields: dictonary mapping the field names and their values
347 :param view_info: result of fields_view_get() called on the object
348 :param parent: dictionary containing the values already computed for the parent, in case of one2many fields
349 :param default: if True, the default values must be processed too or not
350 :return: dictionary mapping the field names and their values, ready to use when calling the create() function
353 def _get_right_one2many_view(fg, field_name, view_type):
354 one2many_view = fg[field_name]['views'].get(view_type)
355 # if the view is not defined inline, we call fields_view_get()
356 if not one2many_view:
357 one2many_view = self.pool.get(fg[field_name]['relation']).fields_view_get(self.cr, SUPERUSER_ID, False, view_type, self.context)
360 def process_val(key, val):
361 if fg[key]['type'] == 'many2one':
362 if type(val) in (tuple,list):
364 elif fg[key]['type'] == 'one2many':
365 if val and isinstance(val, (list,tuple)) and isinstance(val[0], dict):
366 # we want to return only the fields that aren't readonly
367 # For that, we need to first get the right tree view to consider for the field `key´
368 one2many_tree_view = _get_right_one2many_view(fg, key, 'tree')
369 arch = etree.fromstring(one2many_tree_view['arch'].encode('utf-8'))
371 # make a copy for the iteration, as we will alter `rec´
372 rec_copy = rec.copy()
373 for field_key in rec_copy:
374 # if field is missing in view or has a readonly modifier, drop it
375 field_elem = arch.xpath("//field[@name='%s']" % field_key)
376 if field_elem and (field_elem[0].get('modifiers', '{}').find('"readonly": true') >= 0):
377 # TODO: currently we only support if readonly is True in the modifiers. Some improvement may be done in
378 # order to support also modifiers that look like {"readonly": [["state", "not in", ["draft", "confirm"]]]}
380 # now that unwanted values have been removed from val, we can encapsulate it in a tuple as returned value
381 val = map(lambda x: (0,0,x), val)
382 elif fg[key]['type'] == 'many2many':
383 if val and isinstance(val,(list,tuple)) and isinstance(val[0], (int,long)):
386 # we want to return only the fields that aren't readonly
387 if el.get('modifiers', '{}').find('"readonly": true') >= 0:
388 # TODO: currently we only support if readonly is True in the modifiers. Some improvement may be done in
389 # order to support also modifiers that look like {"readonly": [["state", "not in", ["draft", "confirm"]]]}
395 arch = etree.fromstring(view_info['arch'].decode('utf-8'))
396 view = arch if len(arch) else False
399 fields = fields or {}
400 if view is not False:
401 fg = view_info['fields']
402 # gather the default values on the object. (Can't use `fields´ as parameter instead of {} because we may
403 # have references like `base.main_company´ in the yaml file and it's not compatible with the function)
404 defaults = default and model._add_missing_default_values(self.cr, SUPERUSER_ID, {}, context=self.context) or {}
406 # copy the default values in record_dict, only if they are in the view (because that's what the client does)
407 # the other default values will be added later on by the create().
408 record_dict = dict([(key, val) for key, val in defaults.items() if key in fg])
410 # Process all on_change calls
415 field_name = el.attrib['name']
416 assert field_name in fg, "The field '%s' is defined in the form view but not on the object '%s'!" % (field_name, model._name)
417 if field_name in fields:
418 one2many_form_view = None
419 if (view is not False) and (fg[field_name]['type']=='one2many'):
420 # for one2many fields, we want to eval them using the inline form view defined on the parent
421 one2many_form_view = _get_right_one2many_view(fg, field_name, 'form')
423 field_value = self._eval_field(model, field_name, fields[field_name], one2many_form_view or view_info, parent=record_dict, default=default)
425 #call process_val to not update record_dict if values were given for readonly fields
426 val = process_val(field_name, field_value)
428 record_dict[field_name] = val
429 #if (field_name in defaults) and defaults[field_name] == field_value:
430 # print '*** You can remove these lines:', field_name, field_value
432 #if field_name has a default value or a value is given in the yaml file, we must call its on_change()
433 elif field_name not in defaults:
436 if not el.attrib.get('on_change', False):
438 match = re.match("([a-z_1-9A-Z]+)\((.*)\)", el.attrib['on_change'])
439 assert match, "Unable to parse the on_change '%s'!" % (el.attrib['on_change'], )
441 # creating the context
442 class parent2(object):
443 def __init__(self, d):
445 def __getattr__(self, name):
446 return self.d.get(name, False)
448 ctx = record_dict.copy()
449 ctx['context'] = self.context
450 ctx['uid'] = SUPERUSER_ID
451 ctx['parent'] = parent2(parent)
454 ctx[a] = process_val(a, defaults.get(a, False))
457 args = map(lambda x: eval(x, ctx), match.group(2).split(','))
458 result = getattr(model, match.group(1))(self.cr, SUPERUSER_ID, [], *args)
459 for key, val in (result or {}).get('value', {}).items():
461 record_dict[key] = process_val(key, val)
463 _logger.debug("The returning field '%s' from your on_change call '%s'"
464 " does not exist either on the object '%s', either in"
466 key, match.group(1), model._name, view_info['name'])
468 nodes = list(el) + nodes
472 for field_name, expression in fields.items():
473 if field_name in record_dict:
475 field_value = self._eval_field(model, field_name, expression, default=False)
476 record_dict[field_name] = field_value
479 def process_ref(self, node, column=None):
480 assert node.search or node.id, '!ref node should have a `search` attribute or `id` attribute'
483 model_name = node.model
485 model_name = column._obj
487 raise YamlImportException('You need to give a model for the search, or a column to infer it.')
488 model = self.get_model(model_name)
489 q = eval(node.search, self.eval_context)
490 ids = model.search(self.cr, self.uid, q)
492 instances = model.browse(self.cr, self.uid, ids)
493 value = [inst[node.use] for inst in instances]
497 value = self.get_id(node.id)
502 def process_eval(self, node):
503 return eval(node.expression, self.eval_context)
505 def _eval_field(self, model, field_name, expression, view_info=False, parent={}, default=True):
506 # TODO this should be refactored as something like model.get_field() in bin/osv
507 if field_name in model._columns:
508 column = model._columns[field_name]
509 elif field_name in model._inherit_fields:
510 column = model._inherit_fields[field_name][2]
512 raise KeyError("Object '%s' does not contain field '%s'" % (model, field_name))
513 if is_ref(expression):
514 elements = self.process_ref(expression, column)
515 if column._type in ("many2many", "one2many"):
516 value = [(6, 0, elements)]
518 if isinstance(elements, (list,tuple)):
519 value = self._get_first_result(elements)
522 elif column._type == "many2one":
523 value = self.get_id(expression)
524 elif column._type == "one2many":
525 other_model = self.get_model(column._obj)
526 value = [(0, 0, self._create_record(other_model, fields, view_info, parent, default=default)) for fields in expression]
527 elif column._type == "many2many":
528 ids = [self.get_id(xml_id) for xml_id in expression]
529 value = [(6, 0, ids)]
530 elif column._type == "date" and is_string(expression):
531 # enforce ISO format for string date values, to be locale-agnostic during tests
532 time.strptime(expression, misc.DEFAULT_SERVER_DATE_FORMAT)
534 elif column._type == "datetime" and is_string(expression):
535 # enforce ISO format for string datetime values, to be locale-agnostic during tests
536 time.strptime(expression, misc.DEFAULT_SERVER_DATETIME_FORMAT)
539 if is_eval(expression):
540 value = self.process_eval(expression)
543 # raise YamlImportException('Unsupported column "%s" or value %s:%s' % (field_name, type(expression), expression))
546 def process_context(self, node):
547 self.context = node.__dict__
549 self.uid = self.get_id(node.uid)
551 self.noupdate = node.noupdate
553 def process_python(self, node):
554 python, statements = node.items()[0]
555 model = self.get_model(python.model)
556 statements = statements.replace("\r\n", "\n")
557 code_context = { 'model': model, 'cr': self.cr, 'uid': self.uid, 'log': self._log, 'context': self.context }
558 code_context.update({'self': model}) # remove me when no !python block test uses 'self' anymore
560 code_obj = compile(statements, self.filename, 'exec')
561 unsafe_eval(code_obj, {'ref': self.get_id}, code_context)
562 except AssertionError, e:
563 self._log_assert_failure('AssertionError in Python code %s: %s', python.name, e)
566 _logger.debug('Exception during evaluation of !python block in yaml_file %s.', self.filename, exc_info=True)
569 self.assertion_report.record_success()
571 def process_workflow(self, node):
572 workflow, values = node.items()[0]
573 if self.isnoupdate(workflow) and self.mode != 'init':
576 id = self.get_id(workflow.ref)
579 raise YamlImportException('You must define a child node if you do not give a ref.')
580 if not len(values) == 1:
581 raise YamlImportException('Only one child node is accepted (%d given).' % len(values))
583 if not 'model' in value and (not 'eval' in value or not 'search' in value):
584 raise YamlImportException('You must provide a "model" and an "eval" or "search" to evaluate.')
585 value_model = self.get_model(value['model'])
586 local_context = {'obj': lambda x: value_model.browse(self.cr, self.uid, x, context=self.context)}
587 local_context.update(self.id_map)
588 id = eval(value['eval'], self.eval_context, local_context)
590 if workflow.uid is not None:
594 self.cr.execute('select distinct signal from wkf_transition')
595 signals=[x['signal'] for x in self.cr.dictfetchall()]
596 if workflow.action not in signals:
597 raise YamlImportException('Incorrect action %s. No such action defined' % workflow.action)
598 import openerp.netsvc as netsvc
599 wf_service = netsvc.LocalService("workflow")
600 wf_service.trg_validate(uid, workflow.model, id, workflow.action, self.cr)
602 def _eval_params(self, model, params):
604 for i, param in enumerate(params):
605 if isinstance(param, types.ListType):
606 value = self._eval_params(model, param)
608 value = self.process_ref(param)
610 value = self.process_eval(param)
611 elif isinstance(param, types.DictionaryType): # supports XML syntax
612 param_model = self.get_model(param.get('model', model))
613 if 'search' in param:
614 q = eval(param['search'], self.eval_context)
615 ids = param_model.search(self.cr, self.uid, q)
616 value = self._get_first_result(ids)
617 elif 'eval' in param:
618 local_context = {'obj': lambda x: param_model.browse(self.cr, self.uid, x, self.context)}
619 local_context.update(self.id_map)
620 value = eval(param['eval'], self.eval_context, local_context)
622 raise YamlImportException('You must provide either a !ref or at least a "eval" or a "search" to function parameter #%d.' % i)
624 value = param # scalar value
628 def process_function(self, node):
629 function, params = node.items()[0]
630 if self.isnoupdate(function) and self.mode != 'init':
632 model = self.get_model(function.model)
634 args = self.process_eval(function.eval)
636 args = self._eval_params(function.model, params)
637 method = function.name
638 getattr(model, method)(self.cr, self.uid, *args)
640 def _set_group_values(self, node, values):
642 group_names = node.groups.split(',')
644 for group in group_names:
645 if group.startswith('-'):
646 group_id = self.get_id(group[1:])
647 groups_value.append((3, group_id))
649 group_id = self.get_id(group)
650 groups_value.append((4, group_id))
651 values['groups_id'] = groups_value
653 def process_menuitem(self, node):
654 self.validate_xml_id(node.id)
658 self.cr.execute('select id from ir_ui_menu where parent_id is null and name=%s', (node.name,))
659 res = self.cr.fetchone()
660 values = {'parent_id': parent_id, 'name': node.name}
662 parent_id = self.get_id(node.parent)
663 values = {'parent_id': parent_id}
665 values['name'] = node.name
667 res = [ self.get_id(node.id) ]
668 except: # which exception ?
672 action_type = node.type or 'act_window'
674 "act_window": 'STOCK_NEW',
675 "report.xml": 'STOCK_PASTE',
676 "wizard": 'STOCK_EXECUTE',
677 "url": 'STOCK_JUMP_TO',
679 values['icon'] = icons.get(action_type, 'STOCK_NEW')
680 if action_type == 'act_window':
681 action_id = self.get_id(node.action)
682 self.cr.execute('select view_type,view_mode,name,view_id,target from ir_act_window where id=%s', (action_id,))
683 ir_act_window_result = self.cr.fetchone()
684 assert ir_act_window_result, "No window action defined for this id %s !\n" \
685 "Verify that this is a window action or add a type argument." % (node.action,)
686 action_type, action_mode, action_name, view_id, target = ir_act_window_result
688 self.cr.execute('SELECT type FROM ir_ui_view WHERE id=%s', (view_id,))
689 # TODO guess why action_mode is ir_act_window.view_mode above and ir_ui_view.type here
690 action_mode = self.cr.fetchone()
691 self.cr.execute('SELECT view_mode FROM ir_act_window_view WHERE act_window_id=%s ORDER BY sequence LIMIT 1', (action_id,))
693 action_mode = self.cr.fetchone()
694 if action_type == 'tree':
695 values['icon'] = 'STOCK_INDENT'
696 elif action_mode and action_mode.startswith('tree'):
697 values['icon'] = 'STOCK_JUSTIFY_FILL'
698 elif action_mode and action_mode.startswith('graph'):
699 values['icon'] = 'terp-graph'
700 elif action_mode and action_mode.startswith('calendar'):
701 values['icon'] = 'terp-calendar'
703 values['icon'] = 'STOCK_EXECUTE'
704 if not values.get('name', False):
705 values['name'] = action_name
706 elif action_type == 'wizard':
707 action_id = self.get_id(node.action)
708 self.cr.execute('select name from ir_act_wizard where id=%s', (action_id,))
709 ir_act_wizard_result = self.cr.fetchone()
710 if (not values.get('name', False)) and ir_act_wizard_result:
711 values['name'] = ir_act_wizard_result[0]
713 raise YamlImportException("Unsupported type '%s' in menuitem tag." % action_type)
715 values['sequence'] = node.sequence
717 values['icon'] = node.icon
719 self._set_group_values(node, values)
721 pid = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, \
722 'ir.ui.menu', self.module, values, node.id, mode=self.mode, \
723 noupdate=self.isnoupdate(node), res_id=res and res[0] or False)
725 if node.id and parent_id:
726 self.id_map[node.id] = int(parent_id)
728 if node.action and pid:
729 action_type = node.type or 'act_window'
730 action_id = self.get_id(node.action)
731 action = "ir.actions.%s,%d" % (action_type, action_id)
732 self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, 'action', \
733 'tree_but_open', 'Menuitem', [('ir.ui.menu', int(parent_id))], action, True, True, xml_id=node.id)
735 def process_act_window(self, node):
736 assert getattr(node, 'id'), "Attribute %s of act_window is empty !" % ('id',)
737 assert getattr(node, 'name'), "Attribute %s of act_window is empty !" % ('name',)
738 assert getattr(node, 'res_model'), "Attribute %s of act_window is empty !" % ('res_model',)
739 self.validate_xml_id(node.id)
742 view_id = self.get_id(node.view)
745 context = eval(str(node.context), self.eval_context)
748 'type': node.type or 'ir.actions.act_window',
750 'domain': node.domain,
752 'res_model': node.res_model,
753 'src_model': node.src_model,
754 'view_type': node.view_type or 'form',
755 'view_mode': node.view_mode or 'tree,form',
758 'auto_refresh': node.auto_refresh,
759 'multi': getattr(node, 'multi', False),
762 self._set_group_values(node, values)
765 values['target'] = node.target
766 id = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, \
767 'ir.actions.act_window', self.module, values, node.id, mode=self.mode)
768 self.id_map[node.id] = int(id)
771 keyword = 'client_action_relate'
772 value = 'ir.actions.act_window,%s' % id
773 replace = node.replace or True
774 self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, 'action', keyword, \
775 node.id, [node.src_model], value, replace=replace, noupdate=self.isnoupdate(node), isobject=True, xml_id=node.id)
776 # TODO add remove ir.model.data
778 def process_delete(self, node):
779 assert getattr(node, 'model'), "Attribute %s of delete tag is empty !" % ('model',)
780 if self.pool.get(node.model):
782 ids = self.pool.get(node.model).search(self.cr, self.uid, eval(node.search, self.eval_context))
784 ids = [self.get_id(node.id)]
786 self.pool.get(node.model).unlink(self.cr, self.uid, ids)
788 self._log("Record not deleted.")
790 def process_url(self, node):
791 self.validate_xml_id(node.id)
793 res = {'name': node.name, 'url': node.url, 'target': node.target}
795 id = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, \
796 "ir.actions.act_url", self.module, res, node.id, mode=self.mode)
797 self.id_map[node.id] = int(id)
799 if (not node.menu or eval(node.menu)) and id:
800 keyword = node.keyword or 'client_action_multi'
801 value = 'ir.actions.act_url,%s' % id
802 replace = node.replace or True
803 self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, 'action', \
804 keyword, node.url, ["ir.actions.act_url"], value, replace=replace, \
805 noupdate=self.isnoupdate(node), isobject=True, xml_id=node.id)
807 def process_ir_set(self, node):
808 if not self.mode == 'init':
810 _, fields = node.items()[0]
812 for fieldname, expression in fields.items():
813 if is_eval(expression):
814 value = eval(expression.expression, self.eval_context)
817 res[fieldname] = value
818 self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, res['key'], res['key2'], \
819 res['name'], res['models'], res['value'], replace=res.get('replace',True), \
820 isobject=res.get('isobject', False), meta=res.get('meta',None))
822 def process_report(self, node):
824 for dest, f in (('name','string'), ('model','model'), ('report_name','name')):
825 values[dest] = getattr(node, f)
826 assert values[dest], "Attribute %s of report is empty !" % (f,)
827 for field,dest in (('rml','report_rml'),('file','report_rml'),('xml','report_xml'),('xsl','report_xsl'),('attachment','attachment'),('attachment_use','attachment_use')):
828 if getattr(node, field):
829 values[dest] = getattr(node, field)
831 values['auto'] = eval(node.auto)
833 sxw_file = misc.file_open(node.sxw)
835 sxw_content = sxw_file.read()
836 values['report_sxw_content'] = sxw_content
840 values['header'] = eval(node.header)
841 values['multi'] = node.multi and eval(node.multi)
843 self.validate_xml_id(xml_id)
845 self._set_group_values(node, values)
847 id = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, "ir.actions.report.xml", \
848 self.module, values, xml_id, noupdate=self.isnoupdate(node), mode=self.mode)
849 self.id_map[xml_id] = int(id)
851 if not node.menu or eval(node.menu):
852 keyword = node.keyword or 'client_print_multi'
853 value = 'ir.actions.report.xml,%s' % id
854 replace = node.replace or True
855 self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, 'action', \
856 keyword, values['name'], [values['model']], value, replace=replace, isobject=True, xml_id=xml_id)
858 def process_none(self):
860 Empty node or commented node should not pass silently.
862 self._log_assert_failure("You have an empty block in your tests.")
865 def process(self, yaml_string):
867 Processes a Yaml string. Custom tags are interpreted by 'process_' instance methods.
869 yaml_tag.add_constructors()
871 is_preceded_by_comment = False
872 for node in yaml.load(yaml_string):
873 is_preceded_by_comment = self._log_node(node, is_preceded_by_comment)
875 self._process_node(node)
880 def _process_node(self, node):
882 self.process_comment(node)
883 elif is_assert(node):
884 self.process_assert(node)
885 elif is_record(node):
886 self.process_record(node)
887 elif is_python(node):
888 self.process_python(node)
889 elif is_menuitem(node):
890 self.process_menuitem(node)
891 elif is_delete(node):
892 self.process_delete(node)
894 self.process_url(node)
895 elif is_context(node):
896 self.process_context(node)
897 elif is_ir_set(node):
898 self.process_ir_set(node)
899 elif is_act_window(node):
900 self.process_act_window(node)
901 elif is_report(node):
902 self.process_report(node)
903 elif is_workflow(node):
904 if isinstance(node, types.DictionaryType):
905 self.process_workflow(node)
907 self.process_workflow({node: []})
908 elif is_function(node):
909 if isinstance(node, types.DictionaryType):
910 self.process_function(node)
912 self.process_function({node: []})
916 raise YamlImportException("Can not process YAML block: %s" % node)
918 def _log_node(self, node, is_preceded_by_comment):
920 is_preceded_by_comment = True
922 elif not is_preceded_by_comment:
923 if isinstance(node, types.DictionaryType):
924 msg = "Creating %s\n with %s"
925 args = node.items()[0]
926 self._log(msg, *args)
930 is_preceded_by_comment = False
931 return is_preceded_by_comment
933 def yaml_import(cr, module, yamlfile, kind, idref=None, mode='init', noupdate=False, report=None):
936 loglevel = logging.TEST if kind == 'test' else logging.DEBUG
937 yaml_string = yamlfile.read()
938 yaml_interpreter = YamlInterpreter(cr, module, idref, mode, filename=yamlfile.name, report=report, noupdate=noupdate, loglevel=loglevel)
939 yaml_interpreter.process(yaml_string)
941 # keeps convention of convert.py
942 convert_yaml_import = yaml_import
944 def threaded_yaml_import(db_name, module_name, file_name, delay=0):
950 cr = sql_db.db_connect(db_name).cursor()
951 fp = misc.file_open(file_name)
952 convert_yaml_import(cr, module_name, fp, {}, 'update', True)
956 threading.Thread(target=f).start()
959 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: