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