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