[REF] yaml_import: removed nested import openerp.
[odoo/odoo.git] / openerp / tools / yaml_import.py
1 # -*- coding: utf-8 -*-
2 import threading
3 import types
4 import time # used to eval time.strftime expressions
5 from datetime import datetime, timedelta
6 import logging
7
8 import openerp
9 import openerp.pooler as pooler
10 import openerp.sql_db as sql_db
11 import misc
12 from config import config
13 import yaml_tag
14 import yaml
15 import re
16 from lxml import etree
17 from openerp import SUPERUSER_ID
18
19 # YAML import needs both safe and unsafe eval, but let's
20 # default to /safe/.
21 unsafe_eval = eval
22 from safe_eval import safe_eval as eval
23
24 import assertion_report
25
26 _logger = logging.getLogger(__name__)
27
28 class YamlImportException(Exception):
29     pass
30
31 class YamlImportAbortion(Exception):
32     pass
33
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)
38     return value
39
40 def is_comment(node):
41     return isinstance(node, types.StringTypes)
42
43 def is_assert(node):
44     return isinstance(node, yaml_tag.Assert) \
45         or _is_yaml_mapping(node, yaml_tag.Assert)
46
47 def is_record(node):
48     return _is_yaml_mapping(node, yaml_tag.Record)
49
50 def is_python(node):
51     return _is_yaml_mapping(node, yaml_tag.Python)
52
53 def is_menuitem(node):
54     return isinstance(node, yaml_tag.Menuitem) \
55         or _is_yaml_mapping(node, yaml_tag.Menuitem)
56
57 def is_function(node):
58     return isinstance(node, yaml_tag.Function) \
59         or _is_yaml_mapping(node, yaml_tag.Function)
60
61 def is_report(node):
62     return isinstance(node, yaml_tag.Report)
63
64 def is_workflow(node):
65     return isinstance(node, yaml_tag.Workflow)
66
67 def is_act_window(node):
68     return isinstance(node, yaml_tag.ActWindow)
69
70 def is_delete(node):
71     return isinstance(node, yaml_tag.Delete)
72
73 def is_context(node):
74     return isinstance(node, yaml_tag.Context)
75
76 def is_url(node):
77     return isinstance(node, yaml_tag.Url)
78
79 def is_eval(node):
80     return isinstance(node, yaml_tag.Eval)
81
82 def is_ref(node):
83     return isinstance(node, yaml_tag.Ref) \
84         or _is_yaml_mapping(node, yaml_tag.Ref)
85
86 def is_ir_set(node):
87     return _is_yaml_mapping(node, yaml_tag.IrSet)
88
89 def is_string(node):
90     return isinstance(node, basestring)
91
92 class RecordDictWrapper(dict):
93     """
94     Used to pass a record as locals in eval:
95     records do not strictly behave like dict, so we force them to.
96     """
97     def __init__(self, record):
98         self.record = record
99     def __getitem__(self, key):
100         if key in self.record:
101             return self.record[key]
102         return dict.__getitem__(self, key)
103
104 class YamlInterpreter(object):
105     def __init__(self, cr, module, id_map, mode, filename, report=None, noupdate=False, loglevel=logging.DEBUG):
106         self.cr = cr
107         self.module = module
108         self.id_map = id_map
109         self.mode = mode
110         self.filename = filename
111         if report is None:
112             report = assertion_report.assertion_report()
113         self.assertion_report = report
114         self.noupdate = noupdate
115         self.loglevel = loglevel
116         self.pool = pooler.get_pool(cr.dbname)
117         self.uid = 1
118         self.context = {} # opererp context
119         self.eval_context = {'ref': self._ref(),
120                              '_ref': self._ref(), # added '_ref' so that record['ref'] is possible
121                              'time': time,
122                              'datetime': datetime,
123                              'timedelta': timedelta}
124
125     def _log(self, *args, **kwargs):
126         _logger.log(self.loglevel, *args, **kwargs)
127
128     def _ref(self):
129         return lambda xml_id: self.get_id(xml_id)
130
131     def get_model(self, model_name):
132         model = self.pool.get(model_name)
133         assert model, "The model %s does not exist." % (model_name,)
134         return model
135
136     def validate_xml_id(self, xml_id):
137         id = xml_id
138         if '.' in xml_id:
139             module, id = xml_id.split('.', 1)
140             assert '.' not in id, "The ID reference '%s' must contains maximum one dot.\n" \
141                                   "It is used to refer to other modules ID, in the form: module.record_id" \
142                                   % (xml_id,)
143             if module != self.module:
144                 module_count = self.pool.get('ir.module.module').search_count(self.cr, self.uid, \
145                         ['&', ('name', '=', module), ('state', 'in', ['installed'])])
146                 assert module_count == 1, 'The ID "%s" refers to an uninstalled module.' % (xml_id,)
147         if len(id) > 64: # TODO where does 64 come from (DB is 128)? should be a constant or loaded form DB
148             _logger.error('id: %s is to long (max: 64)', id)
149
150     def get_id(self, xml_id):
151         if xml_id is False or xml_id is None:
152             return False
153         #if not xml_id:
154         #    raise YamlImportException("The xml_id should be a non empty string.")
155         elif isinstance(xml_id, types.IntType):
156             id = xml_id
157         elif xml_id in self.id_map:
158             id = self.id_map[xml_id]
159         else:
160             if '.' in xml_id:
161                 module, checked_xml_id = xml_id.split('.', 1)
162             else:
163                 module = self.module
164                 checked_xml_id = xml_id
165             try:
166                 _, id = self.pool.get('ir.model.data').get_object_reference(self.cr, self.uid, module, checked_xml_id)
167                 self.id_map[xml_id] = id
168             except ValueError:
169                 raise ValueError("""%s not found when processing %s.
170     This Yaml file appears to depend on missing data. This often happens for
171     tests that belong to a module's test suite and depend on each other.""" % (checked_xml_id, self.filename))
172
173         return id
174
175     def get_context(self, node, eval_dict):
176         context = self.context.copy()
177         if node.context:
178             context.update(eval(node.context, eval_dict))
179         return context
180
181     def isnoupdate(self, node):
182         return self.noupdate or node.noupdate or False
183
184     def _get_first_result(self, results, default=False):
185         if len(results):
186             value = results[0]
187             if isinstance(value, types.TupleType):
188                 value = value[0]
189         else:
190             value = default
191         return value
192
193     def process_comment(self, node):
194         return node
195
196     def _log_assert_failure(self, msg, *args):
197         self.assertion_report.record_failure()
198         _logger.error(msg, *args)
199
200     def _get_assertion_id(self, assertion):
201         if assertion.id:
202             ids = [self.get_id(assertion.id)]
203         elif assertion.search:
204             q = eval(assertion.search, self.eval_context)
205             ids = self.pool.get(assertion.model).search(self.cr, self.uid, q, context=assertion.context)
206         else:
207             raise YamlImportException('Nothing to assert: you must give either an id or a search criteria.')
208         return ids
209
210     def process_assert(self, node):
211         if isinstance(node, dict):
212             assertion, expressions = node.items()[0]
213         else:
214             assertion, expressions = node, []
215
216         if self.isnoupdate(assertion) and self.mode != 'init':
217             _logger.warning('This assertion was not evaluated ("%s").', assertion.string)
218             return
219         model = self.get_model(assertion.model)
220         ids = self._get_assertion_id(assertion)
221         if assertion.count is not None and len(ids) != assertion.count:
222             msg = 'assertion "%s" failed!\n'   \
223                   ' Incorrect search count:\n' \
224                   ' expected count: %d\n'      \
225                   ' obtained count: %d\n'
226             args = (assertion.string, assertion.count, len(ids))
227             self._log_assert_failure(msg, *args)
228         else:
229             context = self.get_context(assertion, self.eval_context)
230             for id in ids:
231                 record = model.browse(self.cr, self.uid, id, context)
232                 for test in expressions:
233                     try:
234                         success = unsafe_eval(test, self.eval_context, RecordDictWrapper(record))
235                     except Exception, e:
236                         _logger.debug('Exception during evaluation of !assert block in yaml_file %s.', self.filename, exc_info=True)
237                         raise YamlImportAbortion(e)
238                     if not success:
239                         msg = 'Assertion "%s" FAILED\ntest: %s\n'
240                         args = (assertion.string, test)
241                         for aop in ('==', '!=', '<>', 'in', 'not in', '>=', '<=', '>', '<'):
242                             if aop in test:
243                                 left, right = test.split(aop,1)
244                                 lmsg = ''
245                                 rmsg = ''
246                                 try:
247                                     lmsg = unsafe_eval(left, self.eval_context, RecordDictWrapper(record))
248                                 except Exception, e:
249                                     lmsg = '<exc>'
250
251                                 try:
252                                     rmsg = unsafe_eval(right, self.eval_context, RecordDictWrapper(record))
253                                 except Exception, e:
254                                     rmsg = '<exc>'
255
256                                 msg += 'values: ! %s %s %s'
257                                 args += ( lmsg, aop, rmsg )
258                                 break
259
260                         self._log_assert_failure(msg, *args)
261                         return
262             else: # all tests were successful for this assertion tag (no break)
263                 self.assertion_report.record_success()
264
265     def _coerce_bool(self, value, default=False):
266         if isinstance(value, types.BooleanType):
267             b = value
268         if isinstance(value, types.StringTypes):
269             b = value.strip().lower() not in ('0', 'false', 'off', 'no')
270         elif isinstance(value, types.IntType):
271             b = bool(value)
272         else:
273             b = default
274         return b
275
276     def create_osv_memory_record(self, record, fields):
277         model = self.get_model(record.model)
278         context = self.get_context(record, self.eval_context)
279         record_dict = self._create_record(model, fields)
280         id_new = model.create(self.cr, self.uid, record_dict, context=context)
281         self.id_map[record.id] = int(id_new)
282         return record_dict
283
284     def process_record(self, node):
285         record, fields = node.items()[0]
286         model = self.get_model(record.model)
287
288         view_id = record.view
289         if view_id and (view_id is not True) and isinstance(view_id, basestring):
290             module = self.module
291             if '.' in view_id:
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]
294
295         if model.is_transient():
296             record_dict=self.create_osv_memory_record(record, fields)
297         else:
298             self.validate_xml_id(record.id)
299             try:
300                 self.pool.get('ir.model.data')._get_id(self.cr, SUPERUSER_ID, self.module, record.id)
301                 default = False
302             except ValueError:
303                 default = True
304
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
308                 if id:
309                     self.id_map[record] = int(id)
310                     return None
311                 else:
312                     if not self._coerce_bool(record.forcecreate):
313                         return None
314
315
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
319             view_info = False
320             if view_id:
321                 varg = view_id
322                 if view_id is True: varg = False
323                 view_info = model.fields_view_get(self.cr, SUPERUSER_ID, varg, 'form', context)
324
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'):
331                 self.cr.commit()
332
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
343             :rtype: dict
344         """
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)
350             return one2many_view
351
352         def process_val(key, val):
353             if fg[key]['type'] == 'many2one':
354                 if type(val) in (tuple,list):
355                     val = val[0]
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'))
362                     for rec in val:
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"]]]}
371                                 del rec[field_key]
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)):
376                     val = [(6,0,val)]
377
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"]]]}
382                 return False
383
384             return val
385
386         if view_info:
387             arch = etree.fromstring(view_info['arch'].decode('utf-8'))
388             view = arch if len(arch) else False
389         else:
390             view = 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 {}
397
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])
401
402             # Process all on_change calls
403             nodes = [view]
404             while nodes:
405                 el = nodes.pop(0)
406                 if el.tag=='field':
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')
414
415                         field_value = self._eval_field(model, field_name, fields[field_name], one2many_form_view or view_info, parent=record_dict, default=default)
416
417                         #call process_val to not update record_dict if values were given for readonly fields
418                         val = process_val(field_name, field_value)
419                         if val:
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
423
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:
426                         continue
427
428                     if not el.attrib.get('on_change', False):
429                         continue
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'], )
432
433                     # creating the context
434                     class parent2(object):
435                         def __init__(self, d):
436                             self.d = d
437                         def __getattr__(self, name):
438                             return self.d.get(name, False)
439
440                     ctx = record_dict.copy()
441                     ctx['context'] = self.context
442                     ctx['uid'] = SUPERUSER_ID
443                     ctx['parent'] = parent2(parent)
444                     for a in fg:
445                         if a not in ctx:
446                             ctx[a] = process_val(a, defaults.get(a, False))
447
448                     # Evaluation args
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
456                 else:
457                     nodes = list(el) + nodes
458         else:
459             record_dict = {}
460
461         for field_name, expression in fields.items():
462             if field_name in record_dict:
463                 continue
464             field_value = self._eval_field(model, field_name, expression, default=False)
465             record_dict[field_name] = field_value
466         return record_dict
467
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'
470         if node.search:
471             if node.model:
472                 model_name = node.model
473             elif column:
474                 model_name = column._obj
475             else:
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)
480             if node.use:
481                 instances = model.browse(self.cr, self.uid, ids)
482                 value = [inst[node.use] for inst in instances]
483             else:
484                 value = ids
485         elif node.id:
486             value = self.get_id(node.id)
487         else:
488             value = None
489         return value
490
491     def process_eval(self, node):
492         return eval(node.expression, self.eval_context)
493
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]
500         else:
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)]
506             else: # many2one
507                 if isinstance(elements, (list,tuple)):
508                     value = self._get_first_result(elements)
509                 else:
510                     value = 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)
522             value = expression
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)
526             value = expression
527         else: # scalar field
528             if is_eval(expression):
529                 value = self.process_eval(expression)
530             else:
531                 value = expression
532             # raise YamlImportException('Unsupported column "%s" or value %s:%s' % (field_name, type(expression), expression))
533         return value
534
535     def process_context(self, node):
536         self.context = node.__dict__
537         if node.uid:
538             self.uid = self.get_id(node.uid)
539         if node.noupdate:
540             self.noupdate = node.noupdate
541
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 = {
547             'model': model,
548             'cr': self.cr,
549             'uid': self.uid,
550             'log': self._log,
551             'context': self.context,
552             'openerp': openerp,
553         }
554         code_context.update({'self': model}) # remove me when no !python block test uses 'self' anymore
555         try:
556             code_obj = compile(statements, self.filename, 'exec')
557             unsafe_eval(code_obj, {'ref': self.get_id}, code_context)
558         except AssertionError, e:
559             self._log_assert_failure('AssertionError in Python code %s: %s', python.name, e)
560             return
561         except Exception, e:
562             _logger.debug('Exception during evaluation of !python block in yaml_file %s.', self.filename, exc_info=True)
563             raise
564         else:
565             self.assertion_report.record_success()
566
567     def process_workflow(self, node):
568         workflow, values = node.items()[0]
569         if self.isnoupdate(workflow) and self.mode != 'init':
570             return
571         if workflow.ref:
572             id = self.get_id(workflow.ref)
573         else:
574             if not values:
575                 raise YamlImportException('You must define a child node if you do not give a ref.')
576             if not len(values) == 1:
577                 raise YamlImportException('Only one child node is accepted (%d given).' % len(values))
578             value = values[0]
579             if not 'model' in value and (not 'eval' in value or not 'search' in value):
580                 raise YamlImportException('You must provide a "model" and an "eval" or "search" to evaluate.')
581             value_model = self.get_model(value['model'])
582             local_context = {'obj': lambda x: value_model.browse(self.cr, self.uid, x, context=self.context)}
583             local_context.update(self.id_map)
584             id = eval(value['eval'], self.eval_context, local_context)
585
586         if workflow.uid is not None:
587             uid = workflow.uid
588         else:
589             uid = self.uid
590         self.cr.execute('select distinct signal from wkf_transition')
591         signals=[x['signal'] for x in self.cr.dictfetchall()]
592         if workflow.action not in signals:
593             raise YamlImportException('Incorrect action %s. No such action defined' % workflow.action)
594         import openerp.netsvc as netsvc
595         wf_service = netsvc.LocalService("workflow")
596         wf_service.trg_validate(uid, workflow.model, id, workflow.action, self.cr)
597
598     def _eval_params(self, model, params):
599         args = []
600         for i, param in enumerate(params):
601             if isinstance(param, types.ListType):
602                 value = self._eval_params(model, param)
603             elif is_ref(param):
604                 value = self.process_ref(param)
605             elif is_eval(param):
606                 value = self.process_eval(param)
607             elif isinstance(param, types.DictionaryType): # supports XML syntax
608                 param_model = self.get_model(param.get('model', model))
609                 if 'search' in param:
610                     q = eval(param['search'], self.eval_context)
611                     ids = param_model.search(self.cr, self.uid, q)
612                     value = self._get_first_result(ids)
613                 elif 'eval' in param:
614                     local_context = {'obj': lambda x: param_model.browse(self.cr, self.uid, x, self.context)}
615                     local_context.update(self.id_map)
616                     value = eval(param['eval'], self.eval_context, local_context)
617                 else:
618                     raise YamlImportException('You must provide either a !ref or at least a "eval" or a "search" to function parameter #%d.' % i)
619             else:
620                 value = param # scalar value
621             args.append(value)
622         return args
623
624     def process_function(self, node):
625         function, params = node.items()[0]
626         if self.isnoupdate(function) and self.mode != 'init':
627             return
628         model = self.get_model(function.model)
629         if function.eval:
630             args = self.process_eval(function.eval)
631         else:
632             args = self._eval_params(function.model, params)
633         method = function.name
634         getattr(model, method)(self.cr, self.uid, *args)
635
636     def _set_group_values(self, node, values):
637         if node.groups:
638             group_names = node.groups.split(',')
639             groups_value = []
640             for group in group_names:
641                 if group.startswith('-'):
642                     group_id = self.get_id(group[1:])
643                     groups_value.append((3, group_id))
644                 else:
645                     group_id = self.get_id(group)
646                     groups_value.append((4, group_id))
647             values['groups_id'] = groups_value
648
649     def process_menuitem(self, node):
650         self.validate_xml_id(node.id)
651
652         if not node.parent:
653             parent_id = False
654             self.cr.execute('select id from ir_ui_menu where parent_id is null and name=%s', (node.name,))
655             res = self.cr.fetchone()
656             values = {'parent_id': parent_id, 'name': node.name}
657         else:
658             parent_id = self.get_id(node.parent)
659             values = {'parent_id': parent_id}
660             if node.name:
661                 values['name'] = node.name
662             try:
663                 res = [ self.get_id(node.id) ]
664             except: # which exception ?
665                 res = None
666
667         if node.action:
668             action_type = node.type or 'act_window'
669             icons = {
670                 "act_window": 'STOCK_NEW',
671                 "report.xml": 'STOCK_PASTE',
672                 "wizard": 'STOCK_EXECUTE',
673                 "url": 'STOCK_JUMP_TO',
674             }
675             values['icon'] = icons.get(action_type, 'STOCK_NEW')
676             if action_type == 'act_window':
677                 action_id = self.get_id(node.action)
678                 self.cr.execute('select view_type,view_mode,name,view_id,target from ir_act_window where id=%s', (action_id,))
679                 ir_act_window_result = self.cr.fetchone()
680                 assert ir_act_window_result, "No window action defined for this id %s !\n" \
681                         "Verify that this is a window action or add a type argument." % (node.action,)
682                 action_type, action_mode, action_name, view_id, target = ir_act_window_result
683                 if view_id:
684                     self.cr.execute('SELECT type FROM ir_ui_view WHERE id=%s', (view_id,))
685                     # TODO guess why action_mode is ir_act_window.view_mode above and ir_ui_view.type here
686                     action_mode = self.cr.fetchone()
687                 self.cr.execute('SELECT view_mode FROM ir_act_window_view WHERE act_window_id=%s ORDER BY sequence LIMIT 1', (action_id,))
688                 if self.cr.rowcount:
689                     action_mode = self.cr.fetchone()
690                 if action_type == 'tree':
691                     values['icon'] = 'STOCK_INDENT'
692                 elif action_mode and action_mode.startswith('tree'):
693                     values['icon'] = 'STOCK_JUSTIFY_FILL'
694                 elif action_mode and action_mode.startswith('graph'):
695                     values['icon'] = 'terp-graph'
696                 elif action_mode and action_mode.startswith('calendar'):
697                     values['icon'] = 'terp-calendar'
698                 if target == 'new':
699                     values['icon'] = 'STOCK_EXECUTE'
700                 if not values.get('name', False):
701                     values['name'] = action_name
702             elif action_type == 'wizard':
703                 action_id = self.get_id(node.action)
704                 self.cr.execute('select name from ir_act_wizard where id=%s', (action_id,))
705                 ir_act_wizard_result = self.cr.fetchone()
706                 if (not values.get('name', False)) and ir_act_wizard_result:
707                     values['name'] = ir_act_wizard_result[0]
708             else:
709                 raise YamlImportException("Unsupported type '%s' in menuitem tag." % action_type)
710         if node.sequence:
711             values['sequence'] = node.sequence
712         if node.icon:
713             values['icon'] = node.icon
714
715         self._set_group_values(node, values)
716
717         pid = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, \
718                 'ir.ui.menu', self.module, values, node.id, mode=self.mode, \
719                 noupdate=self.isnoupdate(node), res_id=res and res[0] or False)
720
721         if node.id and parent_id:
722             self.id_map[node.id] = int(parent_id)
723
724         if node.action and pid:
725             action_type = node.type or 'act_window'
726             action_id = self.get_id(node.action)
727             action = "ir.actions.%s,%d" % (action_type, action_id)
728             self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, 'action', \
729                     'tree_but_open', 'Menuitem', [('ir.ui.menu', int(parent_id))], action, True, True, xml_id=node.id)
730
731     def process_act_window(self, node):
732         assert getattr(node, 'id'), "Attribute %s of act_window is empty !" % ('id',)
733         assert getattr(node, 'name'), "Attribute %s of act_window is empty !" % ('name',)
734         assert getattr(node, 'res_model'), "Attribute %s of act_window is empty !" % ('res_model',)
735         self.validate_xml_id(node.id)
736         view_id = False
737         if node.view:
738             view_id = self.get_id(node.view)
739         if not node.context:
740             node.context={}
741         context = eval(str(node.context), self.eval_context)
742         values = {
743             'name': node.name,
744             'type': node.type or 'ir.actions.act_window',
745             'view_id': view_id,
746             'domain': node.domain,
747             'context': context,
748             'res_model': node.res_model,
749             'src_model': node.src_model,
750             'view_type': node.view_type or 'form',
751             'view_mode': node.view_mode or 'tree,form',
752             'usage': node.usage,
753             'limit': node.limit,
754             'auto_refresh': node.auto_refresh,
755             'multi': getattr(node, 'multi', False),
756         }
757
758         self._set_group_values(node, values)
759
760         if node.target:
761             values['target'] = node.target
762         id = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, \
763                 'ir.actions.act_window', self.module, values, node.id, mode=self.mode)
764         self.id_map[node.id] = int(id)
765
766         if node.src_model:
767             keyword = 'client_action_relate'
768             value = 'ir.actions.act_window,%s' % id
769             replace = node.replace or True
770             self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, 'action', keyword, \
771                     node.id, [node.src_model], value, replace=replace, noupdate=self.isnoupdate(node), isobject=True, xml_id=node.id)
772         # TODO add remove ir.model.data
773
774     def process_delete(self, node):
775         assert getattr(node, 'model'), "Attribute %s of delete tag is empty !" % ('model',)
776         if self.pool.get(node.model):
777             if node.search:
778                 ids = self.pool.get(node.model).search(self.cr, self.uid, eval(node.search, self.eval_context))
779             else:
780                 ids = [self.get_id(node.id)]
781             if len(ids):
782                 self.pool.get(node.model).unlink(self.cr, self.uid, ids)
783         else:
784             self._log("Record not deleted.")
785
786     def process_url(self, node):
787         self.validate_xml_id(node.id)
788
789         res = {'name': node.name, 'url': node.url, 'target': node.target}
790
791         id = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, \
792                 "ir.actions.act_url", self.module, res, node.id, mode=self.mode)
793         self.id_map[node.id] = int(id)
794         # ir_set
795         if (not node.menu or eval(node.menu)) and id:
796             keyword = node.keyword or 'client_action_multi'
797             value = 'ir.actions.act_url,%s' % id
798             replace = node.replace or True
799             self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, 'action', \
800                     keyword, node.url, ["ir.actions.act_url"], value, replace=replace, \
801                     noupdate=self.isnoupdate(node), isobject=True, xml_id=node.id)
802
803     def process_ir_set(self, node):
804         if not self.mode == 'init':
805             return False
806         _, fields = node.items()[0]
807         res = {}
808         for fieldname, expression in fields.items():
809             if is_eval(expression):
810                 value = eval(expression.expression, self.eval_context)
811             else:
812                 value = expression
813             res[fieldname] = value
814         self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, res['key'], res['key2'], \
815                 res['name'], res['models'], res['value'], replace=res.get('replace',True), \
816                 isobject=res.get('isobject', False), meta=res.get('meta',None))
817
818     def process_report(self, node):
819         values = {}
820         for dest, f in (('name','string'), ('model','model'), ('report_name','name')):
821             values[dest] = getattr(node, f)
822             assert values[dest], "Attribute %s of report is empty !" % (f,)
823         for field,dest in (('rml','report_rml'),('file','report_rml'),('xml','report_xml'),('xsl','report_xsl'),('attachment','attachment'),('attachment_use','attachment_use')):
824             if getattr(node, field):
825                 values[dest] = getattr(node, field)
826         if node.auto:
827             values['auto'] = eval(node.auto)
828         if node.sxw:
829             sxw_file = misc.file_open(node.sxw)
830             try:
831                 sxw_content = sxw_file.read()
832                 values['report_sxw_content'] = sxw_content
833             finally:
834                 sxw_file.close()
835         if node.header:
836             values['header'] = eval(node.header)
837         values['multi'] = node.multi and eval(node.multi)
838         xml_id = node.id
839         self.validate_xml_id(xml_id)
840
841         self._set_group_values(node, values)
842
843         id = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, "ir.actions.report.xml", \
844                 self.module, values, xml_id, noupdate=self.isnoupdate(node), mode=self.mode)
845         self.id_map[xml_id] = int(id)
846
847         if not node.menu or eval(node.menu):
848             keyword = node.keyword or 'client_print_multi'
849             value = 'ir.actions.report.xml,%s' % id
850             replace = node.replace or True
851             self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, 'action', \
852                     keyword, values['name'], [values['model']], value, replace=replace, isobject=True, xml_id=xml_id)
853
854     def process_none(self):
855         """
856         Empty node or commented node should not pass silently.
857         """
858         self._log_assert_failure("You have an empty block in your tests.")
859
860
861     def process(self, yaml_string):
862         """
863         Processes a Yaml string. Custom tags are interpreted by 'process_' instance methods.
864         """
865         yaml_tag.add_constructors()
866
867         is_preceded_by_comment = False
868         for node in yaml.load(yaml_string):
869             is_preceded_by_comment = self._log_node(node, is_preceded_by_comment)
870             try:
871                 self._process_node(node)
872             except Exception, e:
873                 _logger.exception(e)
874                 raise
875
876     def _process_node(self, node):
877         if is_comment(node):
878             self.process_comment(node)
879         elif is_assert(node):
880             self.process_assert(node)
881         elif is_record(node):
882             self.process_record(node)
883         elif is_python(node):
884             self.process_python(node)
885         elif is_menuitem(node):
886             self.process_menuitem(node)
887         elif is_delete(node):
888             self.process_delete(node)
889         elif is_url(node):
890             self.process_url(node)
891         elif is_context(node):
892             self.process_context(node)
893         elif is_ir_set(node):
894             self.process_ir_set(node)
895         elif is_act_window(node):
896             self.process_act_window(node)
897         elif is_report(node):
898             self.process_report(node)
899         elif is_workflow(node):
900             if isinstance(node, types.DictionaryType):
901                 self.process_workflow(node)
902             else:
903                 self.process_workflow({node: []})
904         elif is_function(node):
905             if isinstance(node, types.DictionaryType):
906                 self.process_function(node)
907             else:
908                 self.process_function({node: []})
909         elif node is None:
910             self.process_none()
911         else:
912             raise YamlImportException("Can not process YAML block: %s" % node)
913
914     def _log_node(self, node, is_preceded_by_comment):
915         if is_comment(node):
916             is_preceded_by_comment = True
917             self._log(node)
918         elif not is_preceded_by_comment:
919             if isinstance(node, types.DictionaryType):
920                 msg = "Creating %s\n with %s"
921                 args = node.items()[0]
922                 self._log(msg, *args)
923             else:
924                 self._log(node)
925         else:
926             is_preceded_by_comment = False
927         return is_preceded_by_comment
928
929 def yaml_import(cr, module, yamlfile, kind, idref=None, mode='init', noupdate=False, report=None):
930     if idref is None:
931         idref = {}
932     loglevel = logging.TEST if kind == 'test' else logging.DEBUG
933     yaml_string = yamlfile.read()
934     yaml_interpreter = YamlInterpreter(cr, module, idref, mode, filename=yamlfile.name, report=report, noupdate=noupdate, loglevel=loglevel)
935     yaml_interpreter.process(yaml_string)
936
937 # keeps convention of convert.py
938 convert_yaml_import = yaml_import
939
940 def threaded_yaml_import(db_name, module_name, file_name, delay=0):
941     def f():
942         time.sleep(delay)
943         cr = None
944         fp = None
945         try:
946             cr = sql_db.db_connect(db_name).cursor()
947             fp = misc.file_open(file_name)
948             convert_yaml_import(cr, module_name, fp, {}, 'update', True)
949         finally:
950             if cr: cr.close()
951             if fp: fp.close()
952     threading.Thread(target=f).start()
953
954
955 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: