[IMP] tools: mail: improved append_content_to_html, including plaintext2html. Updated...
[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 is False:
358                     val = []
359                 if len(val) and type(val[0]) == dict:
360                     #we want to return only the fields that aren't readonly
361                     #For that, we need to first get the right tree view to consider for the field `key´
362                     one2many_tree_view = _get_right_one2many_view(fg, key, 'tree')
363                     for rec in val:
364                         #make a copy for the iteration, as we will alterate the size of `rec´ dictionary
365                         rec_copy = rec.copy()
366                         for field_key in rec_copy:
367                             #seek in the view for the field `field_key´ and removing it from `key´ values, as this column is readonly in the tree view
368                             subfield_obj = etree.fromstring(one2many_tree_view['arch'].encode('utf-8')).xpath("//field[@name='%s']"%(field_key))
369                             if subfield_obj and (subfield_obj[0].get('modifiers', '{}').find('"readonly": true') >= 0):
370                                 #TODO: currently we only support if readonly is True in the modifiers. Some improvement may be done in 
371                                 #order to support also modifiers that look like {"readonly": [["state", "not in", ["draft", "confirm"]]]}
372                                 del(rec[field_key])
373
374                     #now that unwanted values have been removed from val, we can encapsulate it in a tuple as returned value
375                     val = map(lambda x: (0,0,x), val)
376
377             #we want to return only the fields that aren't readonly
378             if el.get('modifiers', '{}').find('"readonly": true') >= 0:
379                 #TODO: currently we only support if readonly is True in the modifiers. Some improvement may be done in 
380                 #order to support also modifiers that look like {"readonly": [["state", "not in", ["draft", "confirm"]]]}
381                 return False
382             return val
383
384         if view_info:
385             arch = etree.fromstring(view_info['arch'].encode('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 = { 'model': model, 'cr': self.cr, 'uid': self.uid, 'log': self._log, 'context': self.context }
545         code_context.update({'self': model}) # remove me when no !python block test uses 'self' anymore
546         try:
547             code_obj = compile(statements, self.filename, 'exec')
548             unsafe_eval(code_obj, {'ref': self.get_id}, code_context)
549         except AssertionError, e:
550             self._log_assert_failure('AssertionError in Python code %s: %s', python.name, e)
551             return
552         except Exception, e:
553             _logger.debug('Exception during evaluation of !python block in yaml_file %s.', self.filename, exc_info=True)
554             raise
555         else:
556             self.assertion_report.record_success()
557
558     def process_workflow(self, node):
559         workflow, values = node.items()[0]
560         if self.isnoupdate(workflow) and self.mode != 'init':
561             return
562         if workflow.ref:
563             id = self.get_id(workflow.ref)
564         else:
565             if not values:
566                 raise YamlImportException('You must define a child node if you do not give a ref.')
567             if not len(values) == 1:
568                 raise YamlImportException('Only one child node is accepted (%d given).' % len(values))
569             value = values[0]
570             if not 'model' in value and (not 'eval' in value or not 'search' in value):
571                 raise YamlImportException('You must provide a "model" and an "eval" or "search" to evaluate.')
572             value_model = self.get_model(value['model'])
573             local_context = {'obj': lambda x: value_model.browse(self.cr, self.uid, x, context=self.context)}
574             local_context.update(self.id_map)
575             id = eval(value['eval'], self.eval_context, local_context)
576
577         if workflow.uid is not None:
578             uid = workflow.uid
579         else:
580             uid = self.uid
581         self.cr.execute('select distinct signal from wkf_transition')
582         signals=[x['signal'] for x in self.cr.dictfetchall()]
583         if workflow.action not in signals:
584             raise YamlImportException('Incorrect action %s. No such action defined' % workflow.action)
585         import openerp.netsvc as netsvc
586         wf_service = netsvc.LocalService("workflow")
587         wf_service.trg_validate(uid, workflow.model, id, workflow.action, self.cr)
588
589     def _eval_params(self, model, params):
590         args = []
591         for i, param in enumerate(params):
592             if isinstance(param, types.ListType):
593                 value = self._eval_params(model, param)
594             elif is_ref(param):
595                 value = self.process_ref(param)
596             elif is_eval(param):
597                 value = self.process_eval(param)
598             elif isinstance(param, types.DictionaryType): # supports XML syntax
599                 param_model = self.get_model(param.get('model', model))
600                 if 'search' in param:
601                     q = eval(param['search'], self.eval_context)
602                     ids = param_model.search(self.cr, self.uid, q)
603                     value = self._get_first_result(ids)
604                 elif 'eval' in param:
605                     local_context = {'obj': lambda x: param_model.browse(self.cr, self.uid, x, self.context)}
606                     local_context.update(self.id_map)
607                     value = eval(param['eval'], self.eval_context, local_context)
608                 else:
609                     raise YamlImportException('You must provide either a !ref or at least a "eval" or a "search" to function parameter #%d.' % i)
610             else:
611                 value = param # scalar value
612             args.append(value)
613         return args
614
615     def process_function(self, node):
616         function, params = node.items()[0]
617         if self.isnoupdate(function) and self.mode != 'init':
618             return
619         model = self.get_model(function.model)
620         if function.eval:
621             args = self.process_eval(function.eval)
622         else:
623             args = self._eval_params(function.model, params)
624         method = function.name
625         getattr(model, method)(self.cr, self.uid, *args)
626
627     def _set_group_values(self, node, values):
628         if node.groups:
629             group_names = node.groups.split(',')
630             groups_value = []
631             for group in group_names:
632                 if group.startswith('-'):
633                     group_id = self.get_id(group[1:])
634                     groups_value.append((3, group_id))
635                 else:
636                     group_id = self.get_id(group)
637                     groups_value.append((4, group_id))
638             values['groups_id'] = groups_value
639
640     def process_menuitem(self, node):
641         self.validate_xml_id(node.id)
642
643         if not node.parent:
644             parent_id = False
645             self.cr.execute('select id from ir_ui_menu where parent_id is null and name=%s', (node.name,))
646             res = self.cr.fetchone()
647             values = {'parent_id': parent_id, 'name': node.name}
648         else:
649             parent_id = self.get_id(node.parent)
650             values = {'parent_id': parent_id}
651             if node.name:
652                 values['name'] = node.name
653             try:
654                 res = [ self.get_id(node.id) ]
655             except: # which exception ?
656                 res = None
657
658         if node.action:
659             action_type = node.type or 'act_window'
660             icons = {
661                 "act_window": 'STOCK_NEW',
662                 "report.xml": 'STOCK_PASTE',
663                 "wizard": 'STOCK_EXECUTE',
664                 "url": 'STOCK_JUMP_TO',
665             }
666             values['icon'] = icons.get(action_type, 'STOCK_NEW')
667             if action_type == 'act_window':
668                 action_id = self.get_id(node.action)
669                 self.cr.execute('select view_type,view_mode,name,view_id,target from ir_act_window where id=%s', (action_id,))
670                 ir_act_window_result = self.cr.fetchone()
671                 assert ir_act_window_result, "No window action defined for this id %s !\n" \
672                         "Verify that this is a window action or add a type argument." % (node.action,)
673                 action_type, action_mode, action_name, view_id, target = ir_act_window_result
674                 if view_id:
675                     self.cr.execute('SELECT type FROM ir_ui_view WHERE id=%s', (view_id,))
676                     # TODO guess why action_mode is ir_act_window.view_mode above and ir_ui_view.type here
677                     action_mode = self.cr.fetchone()
678                 self.cr.execute('SELECT view_mode FROM ir_act_window_view WHERE act_window_id=%s ORDER BY sequence LIMIT 1', (action_id,))
679                 if self.cr.rowcount:
680                     action_mode = self.cr.fetchone()
681                 if action_type == 'tree':
682                     values['icon'] = 'STOCK_INDENT'
683                 elif action_mode and action_mode.startswith('tree'):
684                     values['icon'] = 'STOCK_JUSTIFY_FILL'
685                 elif action_mode and action_mode.startswith('graph'):
686                     values['icon'] = 'terp-graph'
687                 elif action_mode and action_mode.startswith('calendar'):
688                     values['icon'] = 'terp-calendar'
689                 if target == 'new':
690                     values['icon'] = 'STOCK_EXECUTE'
691                 if not values.get('name', False):
692                     values['name'] = action_name
693             elif action_type == 'wizard':
694                 action_id = self.get_id(node.action)
695                 self.cr.execute('select name from ir_act_wizard where id=%s', (action_id,))
696                 ir_act_wizard_result = self.cr.fetchone()
697                 if (not values.get('name', False)) and ir_act_wizard_result:
698                     values['name'] = ir_act_wizard_result[0]
699             else:
700                 raise YamlImportException("Unsupported type '%s' in menuitem tag." % action_type)
701         if node.sequence:
702             values['sequence'] = node.sequence
703         if node.icon:
704             values['icon'] = node.icon
705
706         self._set_group_values(node, values)
707
708         pid = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, \
709                 'ir.ui.menu', self.module, values, node.id, mode=self.mode, \
710                 noupdate=self.isnoupdate(node), res_id=res and res[0] or False)
711
712         if node.id and parent_id:
713             self.id_map[node.id] = int(parent_id)
714
715         if node.action and pid:
716             action_type = node.type or 'act_window'
717             action_id = self.get_id(node.action)
718             action = "ir.actions.%s,%d" % (action_type, action_id)
719             self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, 'action', \
720                     'tree_but_open', 'Menuitem', [('ir.ui.menu', int(parent_id))], action, True, True, xml_id=node.id)
721
722     def process_act_window(self, node):
723         assert getattr(node, 'id'), "Attribute %s of act_window is empty !" % ('id',)
724         assert getattr(node, 'name'), "Attribute %s of act_window is empty !" % ('name',)
725         assert getattr(node, 'res_model'), "Attribute %s of act_window is empty !" % ('res_model',)
726         self.validate_xml_id(node.id)
727         view_id = False
728         if node.view:
729             view_id = self.get_id(node.view)
730         if not node.context:
731             node.context={}
732         context = eval(str(node.context), self.eval_context)
733         values = {
734             'name': node.name,
735             'type': node.type or 'ir.actions.act_window',
736             'view_id': view_id,
737             'domain': node.domain,
738             'context': context,
739             'res_model': node.res_model,
740             'src_model': node.src_model,
741             'view_type': node.view_type or 'form',
742             'view_mode': node.view_mode or 'tree,form',
743             'usage': node.usage,
744             'limit': node.limit,
745             'auto_refresh': node.auto_refresh,
746             'multi': getattr(node, 'multi', False),
747         }
748
749         self._set_group_values(node, values)
750
751         if node.target:
752             values['target'] = node.target
753         id = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, \
754                 'ir.actions.act_window', self.module, values, node.id, mode=self.mode)
755         self.id_map[node.id] = int(id)
756
757         if node.src_model:
758             keyword = 'client_action_relate'
759             value = 'ir.actions.act_window,%s' % id
760             replace = node.replace or True
761             self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, 'action', keyword, \
762                     node.id, [node.src_model], value, replace=replace, noupdate=self.isnoupdate(node), isobject=True, xml_id=node.id)
763         # TODO add remove ir.model.data
764
765     def process_delete(self, node):
766         assert getattr(node, 'model'), "Attribute %s of delete tag is empty !" % ('model',)
767         if self.pool.get(node.model):
768             if node.search:
769                 ids = self.pool.get(node.model).search(self.cr, self.uid, eval(node.search, self.eval_context))
770             else:
771                 ids = [self.get_id(node.id)]
772             if len(ids):
773                 self.pool.get(node.model).unlink(self.cr, self.uid, ids)
774         else:
775             self._log("Record not deleted.")
776
777     def process_url(self, node):
778         self.validate_xml_id(node.id)
779
780         res = {'name': node.name, 'url': node.url, 'target': node.target}
781
782         id = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, \
783                 "ir.actions.act_url", self.module, res, node.id, mode=self.mode)
784         self.id_map[node.id] = int(id)
785         # ir_set
786         if (not node.menu or eval(node.menu)) and id:
787             keyword = node.keyword or 'client_action_multi'
788             value = 'ir.actions.act_url,%s' % id
789             replace = node.replace or True
790             self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, 'action', \
791                     keyword, node.url, ["ir.actions.act_url"], value, replace=replace, \
792                     noupdate=self.isnoupdate(node), isobject=True, xml_id=node.id)
793
794     def process_ir_set(self, node):
795         if not self.mode == 'init':
796             return False
797         _, fields = node.items()[0]
798         res = {}
799         for fieldname, expression in fields.items():
800             if is_eval(expression):
801                 value = eval(expression.expression, self.eval_context)
802             else:
803                 value = expression
804             res[fieldname] = value
805         self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, res['key'], res['key2'], \
806                 res['name'], res['models'], res['value'], replace=res.get('replace',True), \
807                 isobject=res.get('isobject', False), meta=res.get('meta',None))
808
809     def process_report(self, node):
810         values = {}
811         for dest, f in (('name','string'), ('model','model'), ('report_name','name')):
812             values[dest] = getattr(node, f)
813             assert values[dest], "Attribute %s of report is empty !" % (f,)
814         for field,dest in (('rml','report_rml'),('file','report_rml'),('xml','report_xml'),('xsl','report_xsl'),('attachment','attachment'),('attachment_use','attachment_use')):
815             if getattr(node, field):
816                 values[dest] = getattr(node, field)
817         if node.auto:
818             values['auto'] = eval(node.auto)
819         if node.sxw:
820             sxw_file = misc.file_open(node.sxw)
821             try:
822                 sxw_content = sxw_file.read()
823                 values['report_sxw_content'] = sxw_content
824             finally:
825                 sxw_file.close()
826         if node.header:
827             values['header'] = eval(node.header)
828         values['multi'] = node.multi and eval(node.multi)
829         xml_id = node.id
830         self.validate_xml_id(xml_id)
831
832         self._set_group_values(node, values)
833
834         id = self.pool.get('ir.model.data')._update(self.cr, SUPERUSER_ID, "ir.actions.report.xml", \
835                 self.module, values, xml_id, noupdate=self.isnoupdate(node), mode=self.mode)
836         self.id_map[xml_id] = int(id)
837
838         if not node.menu or eval(node.menu):
839             keyword = node.keyword or 'client_print_multi'
840             value = 'ir.actions.report.xml,%s' % id
841             replace = node.replace or True
842             self.pool.get('ir.model.data').ir_set(self.cr, SUPERUSER_ID, 'action', \
843                     keyword, values['name'], [values['model']], value, replace=replace, isobject=True, xml_id=xml_id)
844
845     def process_none(self):
846         """
847         Empty node or commented node should not pass silently.
848         """
849         self._log_assert_failure("You have an empty block in your tests.")
850
851
852     def process(self, yaml_string):
853         """
854         Processes a Yaml string. Custom tags are interpreted by 'process_' instance methods.
855         """
856         yaml_tag.add_constructors()
857
858         is_preceded_by_comment = False
859         for node in yaml.load(yaml_string):
860             is_preceded_by_comment = self._log_node(node, is_preceded_by_comment)
861             try:
862                 self._process_node(node)
863             except Exception, e:
864                 _logger.exception(e)
865                 raise
866
867     def _process_node(self, node):
868         if is_comment(node):
869             self.process_comment(node)
870         elif is_assert(node):
871             self.process_assert(node)
872         elif is_record(node):
873             self.process_record(node)
874         elif is_python(node):
875             self.process_python(node)
876         elif is_menuitem(node):
877             self.process_menuitem(node)
878         elif is_delete(node):
879             self.process_delete(node)
880         elif is_url(node):
881             self.process_url(node)
882         elif is_context(node):
883             self.process_context(node)
884         elif is_ir_set(node):
885             self.process_ir_set(node)
886         elif is_act_window(node):
887             self.process_act_window(node)
888         elif is_report(node):
889             self.process_report(node)
890         elif is_workflow(node):
891             if isinstance(node, types.DictionaryType):
892                 self.process_workflow(node)
893             else:
894                 self.process_workflow({node: []})
895         elif is_function(node):
896             if isinstance(node, types.DictionaryType):
897                 self.process_function(node)
898             else:
899                 self.process_function({node: []})
900         elif node is None:
901             self.process_none()
902         else:
903             raise YamlImportException("Can not process YAML block: %s" % node)
904
905     def _log_node(self, node, is_preceded_by_comment):
906         if is_comment(node):
907             is_preceded_by_comment = True
908             self._log(node)
909         elif not is_preceded_by_comment:
910             if isinstance(node, types.DictionaryType):
911                 msg = "Creating %s\n with %s"
912                 args = node.items()[0]
913                 self._log(msg, *args)
914             else:
915                 self._log(node)
916         else:
917             is_preceded_by_comment = False
918         return is_preceded_by_comment
919
920 def yaml_import(cr, module, yamlfile, kind, idref=None, mode='init', noupdate=False, report=None):
921     if idref is None:
922         idref = {}
923     loglevel = logging.TEST if kind == 'test' else logging.DEBUG
924     yaml_string = yamlfile.read()
925     yaml_interpreter = YamlInterpreter(cr, module, idref, mode, filename=yamlfile.name, report=report, noupdate=noupdate, loglevel=loglevel)
926     yaml_interpreter.process(yaml_string)
927
928 # keeps convention of convert.py
929 convert_yaml_import = yaml_import
930
931 def threaded_yaml_import(db_name, module_name, file_name, delay=0):
932     def f():
933         time.sleep(delay)
934         cr = None
935         fp = None
936         try:
937             cr = sql_db.db_connect(db_name).cursor()
938             fp = misc.file_open(file_name)
939             convert_yaml_import(cr, module_name, fp, {}, 'update', True)
940         finally:
941             if cr: cr.close()
942             if fp: fp.close()
943     threading.Thread(target=f).start()
944
945
946 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: