[IMP] tools.safe_eval_qweb: methods intended to provide more restricted alternatives...
[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.sql_db as sql_db
10 import openerp.workflow
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 = openerp.registry(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         return self.pool[model_name]
133
134     def validate_xml_id(self, xml_id):
135         id = xml_id
136         if '.' in xml_id:
137             module, id = xml_id.split('.', 1)
138             assert '.' not in id, "The ID reference '%s' must contains maximum one dot.\n" \
139                                   "It is used to refer to other modules ID, in the form: module.record_id" \
140                                   % (xml_id,)
141             if module != self.module:
142                 module_count = self.pool['ir.module.module'].search_count(self.cr, self.uid, \
143                         ['&', ('name', '=', module), ('state', 'in', ['installed'])])
144                 assert module_count == 1, 'The ID "%s" refers to an uninstalled module.' % (xml_id,)
145         if len(id) > 64: # TODO where does 64 come from (DB is 128)? should be a constant or loaded form DB
146             _logger.error('id: %s is to long (max: 64)', id)
147
148     def get_id(self, xml_id):
149         if xml_id is False or xml_id is None:
150             return False
151         #if not xml_id:
152         #    raise YamlImportException("The xml_id should be a non empty string.")
153         elif isinstance(xml_id, types.IntType):
154             id = xml_id
155         elif xml_id in self.id_map:
156             id = self.id_map[xml_id]
157         else:
158             if '.' in xml_id:
159                 module, checked_xml_id = xml_id.split('.', 1)
160             else:
161                 module = self.module
162                 checked_xml_id = xml_id
163             try:
164                 _, id = self.pool['ir.model.data'].get_object_reference(self.cr, self.uid, module, checked_xml_id)
165                 self.id_map[xml_id] = id
166             except ValueError:
167                 raise ValueError("""%s not found when processing %s.
168     This Yaml file appears to depend on missing data. This often happens for
169     tests that belong to a module's test suite and depend on each other.""" % (checked_xml_id, self.filename))
170
171         return id
172
173     def get_context(self, node, eval_dict):
174         context = self.context.copy()
175         if node.context:
176             context.update(eval(node.context, eval_dict))
177         return context
178
179     def isnoupdate(self, node):
180         return self.noupdate or node.noupdate or False
181
182     def _get_first_result(self, results, default=False):
183         if len(results):
184             value = results[0]
185             if isinstance(value, types.TupleType):
186                 value = value[0]
187         else:
188             value = default
189         return value
190
191     def process_comment(self, node):
192         return node
193
194     def _log_assert_failure(self, msg, *args):
195         self.assertion_report.record_failure()
196         _logger.error(msg, *args)
197
198     def _get_assertion_id(self, assertion):
199         if assertion.id:
200             ids = [self.get_id(assertion.id)]
201         elif assertion.search:
202             q = eval(assertion.search, self.eval_context)
203             ids = self.pool[assertion.model].search(self.cr, self.uid, q, context=assertion.context)
204         else:
205             raise YamlImportException('Nothing to assert: you must give either an id or a search criteria.')
206         return ids
207
208     def process_assert(self, node):
209         if isinstance(node, dict):
210             assertion, expressions = node.items()[0]
211         else:
212             assertion, expressions = node, []
213
214         if self.isnoupdate(assertion) and self.mode != 'init':
215             _logger.warning('This assertion was not evaluated ("%s").', assertion.string)
216             return
217         model = self.get_model(assertion.model)
218         ids = self._get_assertion_id(assertion)
219         if assertion.count is not None and len(ids) != assertion.count:
220             msg = 'assertion "%s" failed!\n'   \
221                   ' Incorrect search count:\n' \
222                   ' expected count: %d\n'      \
223                   ' obtained count: %d\n'
224             args = (assertion.string, assertion.count, len(ids))
225             self._log_assert_failure(msg, *args)
226         else:
227             context = self.get_context(assertion, self.eval_context)
228             for id in ids:
229                 record = model.browse(self.cr, self.uid, id, context)
230                 for test in expressions:
231                     try:
232                         success = unsafe_eval(test, self.eval_context, RecordDictWrapper(record))
233                     except Exception, e:
234                         _logger.debug('Exception during evaluation of !assert block in yaml_file %s.', self.filename, exc_info=True)
235                         raise YamlImportAbortion(e)
236                     if not success:
237                         msg = 'Assertion "%s" FAILED\ntest: %s\n'
238                         args = (assertion.string, test)
239                         for aop in ('==', '!=', '<>', 'in', 'not in', '>=', '<=', '>', '<'):
240                             if aop in test:
241                                 left, right = test.split(aop,1)
242                                 lmsg = ''
243                                 rmsg = ''
244                                 try:
245                                     lmsg = unsafe_eval(left, self.eval_context, RecordDictWrapper(record))
246                                 except Exception, e:
247                                     lmsg = '<exc>'
248
249                                 try:
250                                     rmsg = unsafe_eval(right, self.eval_context, RecordDictWrapper(record))
251                                 except Exception, e:
252                                     rmsg = '<exc>'
253
254                                 msg += 'values: ! %s %s %s'
255                                 args += ( lmsg, aop, rmsg )
256                                 break
257
258                         self._log_assert_failure(msg, *args)
259                         return
260             else: # all tests were successful for this assertion tag (no break)
261                 self.assertion_report.record_success()
262
263     def _coerce_bool(self, value, default=False):
264         if isinstance(value, types.BooleanType):
265             b = value
266         if isinstance(value, types.StringTypes):
267             b = value.strip().lower() not in ('0', 'false', 'off', 'no')
268         elif isinstance(value, types.IntType):
269             b = bool(value)
270         else:
271             b = default
272         return b
273
274     def create_osv_memory_record(self, record, fields):
275         model = self.get_model(record.model)
276         context = self.get_context(record, self.eval_context)
277         record_dict = self._create_record(model, fields)
278         id_new = model.create(self.cr, self.uid, record_dict, context=context)
279         self.id_map[record.id] = int(id_new)
280         return record_dict
281
282     def process_record(self, node):
283         record, fields = node.items()[0]
284         model = self.get_model(record.model)
285
286         view_id = record.view
287         if view_id and (view_id is not True) and isinstance(view_id, basestring):
288             module = self.module
289             if '.' in view_id:
290                 module, view_id = view_id.split('.',1)
291             view_id = self.pool['ir.model.data'].get_object_reference(self.cr, SUPERUSER_ID, module, view_id)[1]
292
293         if model.is_transient():
294             record_dict=self.create_osv_memory_record(record, fields)
295         else:
296             self.validate_xml_id(record.id)
297             try:
298                 self.pool['ir.model.data']._get_id(self.cr, SUPERUSER_ID, self.module, record.id)
299                 default = False
300             except ValueError:
301                 default = True
302
303             if self.isnoupdate(record) and self.mode != 'init':
304                 id = self.pool['ir.model.data']._update_dummy(self.cr, SUPERUSER_ID, record.model, self.module, record.id)
305                 # check if the resource already existed at the last update
306                 if id:
307                     self.id_map[record] = int(id)
308                     return None
309                 else:
310                     if not self._coerce_bool(record.forcecreate):
311                         return None
312
313
314             #context = self.get_context(record, self.eval_context)
315             #TOFIX: record.context like {'withoutemployee':True} should pass from self.eval_context. example: test_project.yml in project module
316             context = record.context
317             view_info = False
318             if view_id:
319                 varg = view_id
320                 if view_id is True: varg = False
321                 view_info = model.fields_view_get(self.cr, SUPERUSER_ID, varg, 'form', context)
322
323             record_dict = self._create_record(model, fields, view_info, default=default)
324             _logger.debug("RECORD_DICT %s" % record_dict)
325             id = self.pool['ir.model.data']._update(self.cr, SUPERUSER_ID, record.model, \
326                     self.module, record_dict, record.id, noupdate=self.isnoupdate(record), mode=self.mode, context=context)
327             self.id_map[record.id] = int(id)
328             if config.get('import_partial'):
329                 self.cr.commit()
330
331     def _create_record(self, model, fields, view_info=False, parent={}, default=True):
332         """This function processes the !record tag in yalm files. It simulates the record creation through an xml
333             view (either specified on the !record tag or the default one for this object), including the calls to
334             on_change() functions, and sending only values for fields that aren't set as readonly.
335             :param model: model instance
336             :param fields: dictonary mapping the field names and their values
337             :param view_info: result of fields_view_get() called on the object
338             :param parent: dictionary containing the values already computed for the parent, in case of one2many fields
339             :param default: if True, the default values must be processed too or not
340             :return: dictionary mapping the field names and their values, ready to use when calling the create() function
341             :rtype: dict
342         """
343         def _get_right_one2many_view(fg, field_name, view_type):
344             one2many_view = fg[field_name]['views'].get(view_type)
345             # if the view is not defined inline, we call fields_view_get()
346             if not one2many_view:
347                 one2many_view = self.pool[fg[field_name]['relation']].fields_view_get(self.cr, SUPERUSER_ID, False, view_type, self.context)
348             return one2many_view
349
350         def process_val(key, val):
351             if fg[key]['type'] == 'many2one':
352                 if type(val) in (tuple,list):
353                     val = val[0]
354             elif fg[key]['type'] == 'one2many':
355                 if val and isinstance(val, (list,tuple)) and isinstance(val[0], dict):
356                     # we want to return only the fields that aren't readonly
357                     # For that, we need to first get the right tree view to consider for the field `key´
358                     one2many_tree_view = _get_right_one2many_view(fg, key, 'tree')
359                     arch = etree.fromstring(one2many_tree_view['arch'].encode('utf-8'))
360                     for rec in val:
361                         # make a copy for the iteration, as we will alter `rec´
362                         rec_copy = rec.copy()
363                         for field_key in rec_copy:
364                             # if field is missing in view or has a readonly modifier, drop it
365                             field_elem = arch.xpath("//field[@name='%s']" % field_key)
366                             if field_elem and (field_elem[0].get('modifiers', '{}').find('"readonly": true') >= 0):
367                                 # TODO: currently we only support if readonly is True in the modifiers. Some improvement may be done in 
368                                 # order to support also modifiers that look like {"readonly": [["state", "not in", ["draft", "confirm"]]]}
369                                 del rec[field_key]
370                     # now that unwanted values have been removed from val, we can encapsulate it in a tuple as returned value
371                     val = map(lambda x: (0,0,x), val)
372             elif fg[key]['type'] == 'many2many':
373                 if val and isinstance(val,(list,tuple)) and isinstance(val[0], (int,long)):
374                     val = [(6,0,val)]
375
376             # we want to return only the fields that aren't readonly
377             if el.get('modifiers', '{}').find('"readonly": true') >= 0:
378                 # TODO: currently we only support if readonly is True in the modifiers. Some improvement may be done in 
379                 # order to support also modifiers that look like {"readonly": [["state", "not in", ["draft", "confirm"]]]}
380                 return False
381
382             return val
383
384         if view_info:
385             arch = etree.fromstring(view_info['arch'].decode('utf-8'))
386             view = arch if len(arch) else False
387         else:
388             view = False
389         fields = fields or {}
390         if view is not False:
391             fg = view_info['fields']
392             # gather the default values on the object. (Can't use `fields´ as parameter instead of {} because we may
393             # have references like `base.main_company´ in the yaml file and it's not compatible with the function)
394             defaults = default and model._add_missing_default_values(self.cr, SUPERUSER_ID, {}, context=self.context) or {}
395
396             # copy the default values in record_dict, only if they are in the view (because that's what the client does)
397             # the other default values will be added later on by the create().
398             record_dict = dict([(key, val) for key, val in defaults.items() if key in fg])
399
400             # Process all on_change calls
401             nodes = [view]
402             while nodes:
403                 el = nodes.pop(0)
404                 if el.tag=='field':
405                     field_name = el.attrib['name']
406                     assert field_name in fg, "The field '%s' is defined in the form view but not on the object '%s'!" % (field_name, model._name)
407                     if field_name in fields:
408                         one2many_form_view = None
409                         if (view is not False) and (fg[field_name]['type']=='one2many'):
410                             # for one2many fields, we want to eval them using the inline form view defined on the parent
411                             one2many_form_view = _get_right_one2many_view(fg, field_name, 'form')
412
413                         field_value = self._eval_field(model, field_name, fields[field_name], one2many_form_view or view_info, parent=record_dict, default=default)
414
415                         #call process_val to not update record_dict if values were given for readonly fields
416                         val = process_val(field_name, field_value)
417                         if val:
418                             record_dict[field_name] = val
419                         #if (field_name in defaults) and defaults[field_name] == field_value:
420                         #    print '*** You can remove these lines:', field_name, field_value
421
422                     #if field_name has a default value or a value is given in the yaml file, we must call its on_change()
423                     elif field_name not in defaults:
424                         continue
425
426                     if not el.attrib.get('on_change', False):
427                         continue
428                     match = re.match("([a-z_1-9A-Z]+)\((.*)\)", el.attrib['on_change'])
429                     assert match, "Unable to parse the on_change '%s'!" % (el.attrib['on_change'], )
430
431                     # creating the context
432                     class parent2(object):
433                         def __init__(self, d):
434                             self.d = d
435                         def __getattr__(self, name):
436                             return self.d.get(name, False)
437
438                     ctx = record_dict.copy()
439                     ctx['context'] = self.context
440                     ctx['uid'] = SUPERUSER_ID
441                     ctx['parent'] = parent2(parent)
442                     for a in fg:
443                         if a not in ctx:
444                             ctx[a] = process_val(a, defaults.get(a, False))
445
446                     # Evaluation args
447                     args = map(lambda x: eval(x, ctx), match.group(2).split(','))
448                     result = getattr(model, match.group(1))(self.cr, SUPERUSER_ID, [], *args)
449                     for key, val in (result or {}).get('value', {}).items():
450                         assert key in fg, "The returning field '%s' from your on_change call '%s' does not exist either on the object '%s', either in the view '%s' used for the creation" % (key, match.group(1), model._name, view_info['name'])
451                         record_dict[key] = process_val(key, val)
452                         #if (key in fields) and record_dict[key] == process_val(key, val):
453                         #    print '*** You can remove these lines:', key, val
454                 else:
455                     nodes = list(el) + nodes
456         else:
457             record_dict = {}
458
459         for field_name, expression in fields.items():
460             if field_name in record_dict:
461                 continue
462             field_value = self._eval_field(model, field_name, expression, default=False)
463             record_dict[field_name] = field_value
464         return record_dict
465
466     def process_ref(self, node, column=None):
467         assert node.search or node.id, '!ref node should have a `search` attribute or `id` attribute'
468         if node.search:
469             if node.model:
470                 model_name = node.model
471             elif column:
472                 model_name = column._obj
473             else:
474                 raise YamlImportException('You need to give a model for the search, or a column to infer it.')
475             model = self.get_model(model_name)
476             q = eval(node.search, self.eval_context)
477             ids = model.search(self.cr, self.uid, q)
478             if node.use:
479                 instances = model.browse(self.cr, self.uid, ids)
480                 value = [inst[node.use] for inst in instances]
481             else:
482                 value = ids
483         elif node.id:
484             value = self.get_id(node.id)
485         else:
486             value = None
487         return value
488
489     def process_eval(self, node):
490         return eval(node.expression, self.eval_context)
491
492     def _eval_field(self, model, field_name, expression, view_info=False, parent={}, default=True):
493         # TODO this should be refactored as something like model.get_field() in bin/osv
494         if field_name in model._columns:
495             column = model._columns[field_name]
496         elif field_name in model._inherit_fields:
497             column = model._inherit_fields[field_name][2]
498         else:
499             raise KeyError("Object '%s' does not contain field '%s'" % (model, field_name))
500         if is_ref(expression):
501             elements = self.process_ref(expression, column)
502             if column._type in ("many2many", "one2many"):
503                 value = [(6, 0, elements)]
504             else: # many2one
505                 if isinstance(elements, (list,tuple)):
506                     value = self._get_first_result(elements)
507                 else:
508                     value = elements
509         elif column._type == "many2one":
510             value = self.get_id(expression)
511         elif column._type == "one2many":
512             other_model = self.get_model(column._obj)
513             value = [(0, 0, self._create_record(other_model, fields, view_info, parent, default=default)) for fields in expression]
514         elif column._type == "many2many":
515             ids = [self.get_id(xml_id) for xml_id in expression]
516             value = [(6, 0, ids)]
517         elif column._type == "date" and is_string(expression):
518             # enforce ISO format for string date values, to be locale-agnostic during tests
519             time.strptime(expression, misc.DEFAULT_SERVER_DATE_FORMAT)
520             value = expression
521         elif column._type == "datetime" and is_string(expression):
522             # enforce ISO format for string datetime values, to be locale-agnostic during tests
523             time.strptime(expression, misc.DEFAULT_SERVER_DATETIME_FORMAT)
524             value = expression
525         else: # scalar field
526             if is_eval(expression):
527                 value = self.process_eval(expression)
528             else:
529                 value = expression
530             # raise YamlImportException('Unsupported column "%s" or value %s:%s' % (field_name, type(expression), expression))
531         return value
532
533     def process_context(self, node):
534         self.context = node.__dict__
535         if node.uid:
536             self.uid = self.get_id(node.uid)
537         if node.noupdate:
538             self.noupdate = node.noupdate
539
540     def process_python(self, node):
541         python, statements = node.items()[0]
542         model = self.get_model(python.model)
543         statements = statements.replace("\r\n", "\n")
544         code_context = {
545             'model': model,
546             'cr': self.cr,
547             'uid': self.uid,
548             'log': self._log,
549             'context': self.context,
550             'openerp': openerp,
551         }
552         code_context.update({'self': model}) # remove me when no !python block test uses 'self' anymore
553         try:
554             code_obj = compile(statements, self.filename, 'exec')
555             unsafe_eval(code_obj, {'ref': self.get_id}, code_context)
556         except AssertionError, e:
557             self._log_assert_failure('AssertionError in Python code %s: %s', python.name, e)
558             return
559         except Exception, e:
560             _logger.debug('Exception during evaluation of !python block in yaml_file %s.', self.filename, exc_info=True)
561             raise
562         else:
563             self.assertion_report.record_success()
564
565     def process_workflow(self, node):
566         workflow, values = node.items()[0]
567         if self.isnoupdate(workflow) and self.mode != 'init':
568             return
569         if workflow.ref:
570             id = self.get_id(workflow.ref)
571         else:
572             if not values:
573                 raise YamlImportException('You must define a child node if you do not give a ref.')
574             if not len(values) == 1:
575                 raise YamlImportException('Only one child node is accepted (%d given).' % len(values))
576             value = values[0]
577             if not 'model' in value and (not 'eval' in value or not 'search' in value):
578                 raise YamlImportException('You must provide a "model" and an "eval" or "search" to evaluate.')
579             value_model = self.get_model(value['model'])
580             local_context = {'obj': lambda x: value_model.browse(self.cr, self.uid, x, context=self.context)}
581             local_context.update(self.id_map)
582             id = eval(value['eval'], self.eval_context, local_context)
583
584         if workflow.uid is not None:
585             uid = workflow.uid
586         else:
587             uid = self.uid
588         self.cr.execute('select distinct signal from wkf_transition')
589         signals=[x['signal'] for x in self.cr.dictfetchall()]
590         if workflow.action not in signals:
591             raise YamlImportException('Incorrect action %s. No such action defined' % workflow.action)
592         openerp.workflow.trg_validate(uid, workflow.model, id, workflow.action, self.cr)
593
594     def _eval_params(self, model, params):
595         args = []
596         for i, param in enumerate(params):
597             if isinstance(param, types.ListType):
598                 value = self._eval_params(model, param)
599             elif is_ref(param):
600                 value = self.process_ref(param)
601             elif is_eval(param):
602                 value = self.process_eval(param)
603             elif isinstance(param, types.DictionaryType): # supports XML syntax
604                 param_model = self.get_model(param.get('model', model))
605                 if 'search' in param:
606                     q = eval(param['search'], self.eval_context)
607                     ids = param_model.search(self.cr, self.uid, q)
608                     value = self._get_first_result(ids)
609                 elif 'eval' in param:
610                     local_context = {'obj': lambda x: param_model.browse(self.cr, self.uid, x, self.context)}
611                     local_context.update(self.id_map)
612                     value = eval(param['eval'], self.eval_context, local_context)
613                 else:
614                     raise YamlImportException('You must provide either a !ref or at least a "eval" or a "search" to function parameter #%d.' % i)
615             else:
616                 value = param # scalar value
617             args.append(value)
618         return args
619
620     def process_function(self, node):
621         function, params = node.items()[0]
622         if self.isnoupdate(function) and self.mode != 'init':
623             return
624         model = self.get_model(function.model)
625         if function.eval:
626             args = self.process_eval(function.eval)
627         else:
628             args = self._eval_params(function.model, params)
629         method = function.name
630         getattr(model, method)(self.cr, self.uid, *args)
631
632     def _set_group_values(self, node, values):
633         if node.groups:
634             group_names = node.groups.split(',')
635             groups_value = []
636             for group in group_names:
637                 if group.startswith('-'):
638                     group_id = self.get_id(group[1:])
639                     groups_value.append((3, group_id))
640                 else:
641                     group_id = self.get_id(group)
642                     groups_value.append((4, group_id))
643             values['groups_id'] = groups_value
644
645     def process_menuitem(self, node):
646         self.validate_xml_id(node.id)
647
648         if not node.parent:
649             parent_id = False
650             self.cr.execute('select id from ir_ui_menu where parent_id is null and name=%s', (node.name,))
651             res = self.cr.fetchone()
652             values = {'parent_id': parent_id, 'name': node.name}
653         else:
654             parent_id = self.get_id(node.parent)
655             values = {'parent_id': parent_id}
656             if node.name:
657                 values['name'] = node.name
658             try:
659                 res = [ self.get_id(node.id) ]
660             except: # which exception ?
661                 res = None
662
663         if node.action:
664             action_type = node.type or 'act_window'
665             icons = {
666                 "act_window": 'STOCK_NEW',
667                 "report.xml": 'STOCK_PASTE',
668                 "wizard": 'STOCK_EXECUTE',
669                 "url": 'STOCK_JUMP_TO',
670             }
671             values['icon'] = icons.get(action_type, 'STOCK_NEW')
672             if action_type == 'act_window':
673                 action_id = self.get_id(node.action)
674                 self.cr.execute('select view_type,view_mode,name,view_id,target from ir_act_window where id=%s', (action_id,))
675                 ir_act_window_result = self.cr.fetchone()
676                 assert ir_act_window_result, "No window action defined for this id %s !\n" \
677                         "Verify that this is a window action or add a type argument." % (node.action,)
678                 action_type, action_mode, action_name, view_id, target = ir_act_window_result
679                 if view_id:
680                     self.cr.execute('SELECT type FROM ir_ui_view WHERE id=%s', (view_id,))
681                     # TODO guess why action_mode is ir_act_window.view_mode above and ir_ui_view.type here
682                     action_mode = self.cr.fetchone()
683                 self.cr.execute('SELECT view_mode FROM ir_act_window_view WHERE act_window_id=%s ORDER BY sequence LIMIT 1', (action_id,))
684                 if self.cr.rowcount:
685                     action_mode = self.cr.fetchone()
686                 if action_type == 'tree':
687                     values['icon'] = 'STOCK_INDENT'
688                 elif action_mode and action_mode.startswith('tree'):
689                     values['icon'] = 'STOCK_JUSTIFY_FILL'
690                 elif action_mode and action_mode.startswith('graph'):
691                     values['icon'] = 'terp-graph'
692                 elif action_mode and action_mode.startswith('calendar'):
693                     values['icon'] = 'terp-calendar'
694                 if target == 'new':
695                     values['icon'] = 'STOCK_EXECUTE'
696                 if not values.get('name', False):
697                     values['name'] = action_name
698             elif action_type == 'wizard':
699                 action_id = self.get_id(node.action)
700                 self.cr.execute('select name from ir_act_wizard where id=%s', (action_id,))
701                 ir_act_wizard_result = self.cr.fetchone()
702                 if (not values.get('name', False)) and ir_act_wizard_result:
703                     values['name'] = ir_act_wizard_result[0]
704             else:
705                 raise YamlImportException("Unsupported type '%s' in menuitem tag." % action_type)
706         if node.sequence:
707             values['sequence'] = node.sequence
708         if node.icon:
709             values['icon'] = node.icon
710
711         self._set_group_values(node, values)
712
713         pid = self.pool['ir.model.data']._update(self.cr, SUPERUSER_ID, \
714                 'ir.ui.menu', self.module, values, node.id, mode=self.mode, \
715                 noupdate=self.isnoupdate(node), res_id=res and res[0] or False)
716
717         if node.id and parent_id:
718             self.id_map[node.id] = int(parent_id)
719
720         if node.action and pid:
721             action_type = node.type or 'act_window'
722             action_id = self.get_id(node.action)
723             action = "ir.actions.%s,%d" % (action_type, action_id)
724             self.pool['ir.model.data'].ir_set(self.cr, SUPERUSER_ID, 'action', \
725                     'tree_but_open', 'Menuitem', [('ir.ui.menu', int(parent_id))], action, True, True, xml_id=node.id)
726
727     def process_act_window(self, node):
728         assert getattr(node, 'id'), "Attribute %s of act_window is empty !" % ('id',)
729         assert getattr(node, 'name'), "Attribute %s of act_window is empty !" % ('name',)
730         assert getattr(node, 'res_model'), "Attribute %s of act_window is empty !" % ('res_model',)
731         self.validate_xml_id(node.id)
732         view_id = False
733         if node.view:
734             view_id = self.get_id(node.view)
735         if not node.context:
736             node.context={}
737         context = eval(str(node.context), self.eval_context)
738         values = {
739             'name': node.name,
740             'type': node.type or 'ir.actions.act_window',
741             'view_id': view_id,
742             'domain': node.domain,
743             'context': context,
744             'res_model': node.res_model,
745             'src_model': node.src_model,
746             'view_type': node.view_type or 'form',
747             'view_mode': node.view_mode or 'tree,form',
748             'usage': node.usage,
749             'limit': node.limit,
750             'auto_refresh': node.auto_refresh,
751             'multi': getattr(node, 'multi', False),
752         }
753
754         self._set_group_values(node, values)
755
756         if node.target:
757             values['target'] = node.target
758         id = self.pool['ir.model.data']._update(self.cr, SUPERUSER_ID, \
759                 'ir.actions.act_window', self.module, values, node.id, mode=self.mode)
760         self.id_map[node.id] = int(id)
761
762         if node.src_model:
763             keyword = 'client_action_relate'
764             value = 'ir.actions.act_window,%s' % id
765             replace = node.replace or True
766             self.pool['ir.model.data'].ir_set(self.cr, SUPERUSER_ID, 'action', keyword, \
767                     node.id, [node.src_model], value, replace=replace, noupdate=self.isnoupdate(node), isobject=True, xml_id=node.id)
768         # TODO add remove ir.model.data
769
770     def process_delete(self, node):
771         assert getattr(node, 'model'), "Attribute %s of delete tag is empty !" % ('model',)
772         if node.model in self.pool:
773             if node.search:
774                 ids = self.pool[node.model].search(self.cr, self.uid, eval(node.search, self.eval_context))
775             else:
776                 ids = [self.get_id(node.id)]
777             if len(ids):
778                 self.pool[node.model].unlink(self.cr, self.uid, ids)
779         else:
780             self._log("Record not deleted.")
781
782     def process_url(self, node):
783         self.validate_xml_id(node.id)
784
785         res = {'name': node.name, 'url': node.url, 'target': node.target}
786
787         id = self.pool['ir.model.data']._update(self.cr, SUPERUSER_ID, \
788                 "ir.actions.act_url", self.module, res, node.id, mode=self.mode)
789         self.id_map[node.id] = int(id)
790         # ir_set
791         if (not node.menu or eval(node.menu)) and id:
792             keyword = node.keyword or 'client_action_multi'
793             value = 'ir.actions.act_url,%s' % id
794             replace = node.replace or True
795             self.pool['ir.model.data'].ir_set(self.cr, SUPERUSER_ID, 'action', \
796                     keyword, node.url, ["ir.actions.act_url"], value, replace=replace, \
797                     noupdate=self.isnoupdate(node), isobject=True, xml_id=node.id)
798
799     def process_ir_set(self, node):
800         if not self.mode == 'init':
801             return False
802         _, fields = node.items()[0]
803         res = {}
804         for fieldname, expression in fields.items():
805             if is_eval(expression):
806                 value = eval(expression.expression, self.eval_context)
807             else:
808                 value = expression
809             res[fieldname] = value
810         self.pool['ir.model.data'].ir_set(self.cr, SUPERUSER_ID, res['key'], res['key2'], \
811                 res['name'], res['models'], res['value'], replace=res.get('replace',True), \
812                 isobject=res.get('isobject', False), meta=res.get('meta',None))
813
814     def process_report(self, node):
815         values = {}
816         for dest, f in (('name','string'), ('model','model'), ('report_name','name')):
817             values[dest] = getattr(node, f)
818             assert values[dest], "Attribute %s of report is empty !" % (f,)
819         for field,dest in (('rml','report_rml'),('file','report_rml'),('xml','report_xml'),('xsl','report_xsl'),('attachment','attachment'),('attachment_use','attachment_use')):
820             if getattr(node, field):
821                 values[dest] = getattr(node, field)
822         if node.auto:
823             values['auto'] = eval(node.auto)
824         if node.sxw:
825             sxw_file = misc.file_open(node.sxw)
826             try:
827                 sxw_content = sxw_file.read()
828                 values['report_sxw_content'] = sxw_content
829             finally:
830                 sxw_file.close()
831         if node.header:
832             values['header'] = eval(node.header)
833         values['multi'] = node.multi and eval(node.multi)
834         xml_id = node.id
835         self.validate_xml_id(xml_id)
836
837         self._set_group_values(node, values)
838
839         id = self.pool['ir.model.data']._update(self.cr, SUPERUSER_ID, "ir.actions.report.xml", \
840                 self.module, values, xml_id, noupdate=self.isnoupdate(node), mode=self.mode)
841         self.id_map[xml_id] = int(id)
842
843         if not node.menu or eval(node.menu):
844             keyword = node.keyword or 'client_print_multi'
845             value = 'ir.actions.report.xml,%s' % id
846             replace = node.replace or True
847             self.pool['ir.model.data'].ir_set(self.cr, SUPERUSER_ID, 'action', \
848                     keyword, values['name'], [values['model']], value, replace=replace, isobject=True, xml_id=xml_id)
849
850     def process_none(self):
851         """
852         Empty node or commented node should not pass silently.
853         """
854         self._log_assert_failure("You have an empty block in your tests.")
855
856
857     def process(self, yaml_string):
858         """
859         Processes a Yaml string. Custom tags are interpreted by 'process_' instance methods.
860         """
861         yaml_tag.add_constructors()
862
863         is_preceded_by_comment = False
864         for node in yaml.load(yaml_string):
865             is_preceded_by_comment = self._log_node(node, is_preceded_by_comment)
866             try:
867                 self._process_node(node)
868             except Exception, e:
869                 _logger.exception(e)
870                 raise
871
872     def _process_node(self, node):
873         if is_comment(node):
874             self.process_comment(node)
875         elif is_assert(node):
876             self.process_assert(node)
877         elif is_record(node):
878             self.process_record(node)
879         elif is_python(node):
880             self.process_python(node)
881         elif is_menuitem(node):
882             self.process_menuitem(node)
883         elif is_delete(node):
884             self.process_delete(node)
885         elif is_url(node):
886             self.process_url(node)
887         elif is_context(node):
888             self.process_context(node)
889         elif is_ir_set(node):
890             self.process_ir_set(node)
891         elif is_act_window(node):
892             self.process_act_window(node)
893         elif is_report(node):
894             self.process_report(node)
895         elif is_workflow(node):
896             if isinstance(node, types.DictionaryType):
897                 self.process_workflow(node)
898             else:
899                 self.process_workflow({node: []})
900         elif is_function(node):
901             if isinstance(node, types.DictionaryType):
902                 self.process_function(node)
903             else:
904                 self.process_function({node: []})
905         elif node is None:
906             self.process_none()
907         else:
908             raise YamlImportException("Can not process YAML block: %s" % node)
909
910     def _log_node(self, node, is_preceded_by_comment):
911         if is_comment(node):
912             is_preceded_by_comment = True
913             self._log(node)
914         elif not is_preceded_by_comment:
915             if isinstance(node, types.DictionaryType):
916                 msg = "Creating %s\n with %s"
917                 args = node.items()[0]
918                 self._log(msg, *args)
919             else:
920                 self._log(node)
921         else:
922             is_preceded_by_comment = False
923         return is_preceded_by_comment
924
925 def yaml_import(cr, module, yamlfile, kind, idref=None, mode='init', noupdate=False, report=None):
926     if idref is None:
927         idref = {}
928     loglevel = logging.INFO if kind == 'test' else logging.DEBUG
929     yaml_string = yamlfile.read()
930     yaml_interpreter = YamlInterpreter(cr, module, idref, mode, filename=yamlfile.name, report=report, noupdate=noupdate, loglevel=loglevel)
931     yaml_interpreter.process(yaml_string)
932
933 # keeps convention of convert.py
934 convert_yaml_import = yaml_import
935
936 def threaded_yaml_import(db_name, module_name, file_name, delay=0):
937     def f():
938         time.sleep(delay)
939         cr = None
940         fp = None
941         try:
942             cr = sql_db.db_connect(db_name).cursor()
943             fp = misc.file_open(file_name)
944             convert_yaml_import(cr, module_name, fp, {}, 'update', True)
945         finally:
946             if cr: cr.close()
947             if fp: fp.close()
948     threading.Thread(target=f).start()
949
950
951 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: