"""
user_groups = frozenset(self.pool.get('res.users').browse(cr, 1, uid, context).groups_id)
+
+ check_view_ids = context and context.get('check_view_ids') or (0,)
+ conditions = [['inherit_id', '=', view_id], ['model', '=', model]]
if self.pool._init:
- # Module init currently in progress, only consider views from modules whose code was already loaded
- check_view_ids = context and context.get('check_view_ids') or (0,)
- query = """SELECT v.id FROM ir_ui_view v LEFT JOIN ir_model_data md ON (md.model = 'ir.ui.view' AND md.res_id = v.id)
- WHERE v.inherit_id=%s AND v.model=%s AND (md.module in %s OR v.id in %s)
- ORDER BY priority"""
- query_params = (view_id, model, tuple(self.pool._init_modules), tuple(check_view_ids))
- else:
- # Modules fully loaded, consider all views
- query = """SELECT v.id FROM ir_ui_view v
- WHERE v.inherit_id=%s AND v.model=%s
- ORDER BY priority"""
- query_params = (view_id, model)
- cr.execute(query, query_params)
- view_ids = [v[0] for v in cr.fetchall()]
- # filter views based on user groups
+ # Module init currently in progress, only consider views from
+ # modules whose code is already loaded
+ conditions.extend([
- ['model_ids.model', '=', 'ir.ui.view'],
+ '|',
+ ['model_ids.module', 'in', tuple(self.pool._init_modules)],
+ ['id', 'in', check_view_ids],
+ ])
+ view_ids = self.search(cr, uid, conditions, context=context)
+
return [(view.arch, view.id)
for view in self.browse(cr, 1, view_ids, context)
if not (view.groups_id and user_groups.isdisjoint(view.groups_id))]
- def write(self, cr, uid, ids, vals, context=None):
- if not isinstance(ids, (list, tuple)):
- ids = [ids]
+ def raise_view_error(self, cr, uid, message, view_id, context=None):
+ view = self.browse(cr, uid, [view_id], context)[0]
+ message = "Inherit error: %s view_id: %s, xml_id: %s, model: %s, parent_view: %s" % (message, view_id, view.xml_id, view.model, view.inherit_id)
+ raise AttributeError(message)
- # drop the corresponding view customizations (used for dashboards for example), otherwise
- # not all users would see the updated views
- custom_view_ids = self.pool.get('ir.ui.view.custom').search(cr, uid, [('ref_id','in',ids)])
- if custom_view_ids:
- self.pool.get('ir.ui.view.custom').unlink(cr, uid, custom_view_ids)
+ def locate_node(self, arch, spec):
+ """ Locate a node in a source (parent) architecture.
- return super(view, self).write(cr, uid, ids, vals, context)
+ Given a complete source (parent) architecture (i.e. the field
+ `arch` in a view), and a 'spec' node (a node in an inheriting
+ view that specifies the location in the source view of what
+ should be changed), return (if it exists) the node in the
+ source view matching the specification.
+
+ :param arch: a parent architecture to modify
+ :param spec: a modifying node in an inheriting view
+ :return: a node in the source matching the spec
+ """
+ if spec.tag == 'xpath':
+ nodes = arch.xpath(spec.get('expr'))
+ return nodes[0] if nodes else None
+ elif spec.tag == 'field':
+ # Only compare the field name: a field can be only once in a given view
+ # at a given level (and for multilevel expressions, we should use xpath
+ # inheritance spec anyway).
+ for node in arch.iter('field'):
+ if node.get('name') == spec.get('name'):
+ return node
+ return None
+
+ for node in arch.iter(spec.tag):
+ if isinstance(node, SKIPPED_ELEMENT_TYPES):
+ continue
+ if all(node.get(attr) == spec.get(attr) for attr in spec.attrib
+ if attr not in ('position','version')):
+ # Version spec should match parent's root element's version
+ if spec.get('version') and spec.get('version') != arch.get('version'):
+ return None
+ return node
+ return None
+
+ def inherit_branding(self, specs_tree, view_id, source_id):
+ for node in specs_tree.iterchildren(tag=etree.Element):
+ xpath = node.getroottree().getpath(node)
+ if node.tag == 'data' or node.tag == 'xpath':
+ self.inherit_branding(node, view_id, source_id)
+ else:
+ node.set('data-oe-id', str(view_id))
+ node.set('data-oe-source-id', str(source_id))
+ node.set('data-oe-xpath', xpath)
+ node.set('data-oe-model', 'ir.ui.view')
+ node.set('data-oe-field', 'arch')
+
+ return specs_tree
+
+ def apply_inheritance_specs(self, cr, uid, source, specs_tree, inherit_id, context=None):
+ """ Apply an inheriting view (a descendant of the base view)
+
+ Apply to a source architecture all the spec nodes (i.e. nodes
+ describing where and what changes to apply to some parent
+ architecture) given by an inheriting view.
+
+ :param Element source: a parent architecture to modify
+ :param Elepect specs_tree: a modifying architecture in an inheriting view
+ :param inherit_id: the database id of specs_arch
+ :return: a modified source where the specs are applied
+ :rtype: Element
+ """
+ # Queue of specification nodes (i.e. nodes describing where and
+ # changes to apply to some parent architecture).
+ specs = [specs_tree]
+
+ while len(specs):
+ spec = specs.pop(0)
+ if isinstance(spec, SKIPPED_ELEMENT_TYPES):
+ continue
+ if spec.tag == 'data':
+ specs += [ c for c in specs_tree ]
+ continue
+ node = self.locate_node(source, spec)
+ if node is not None:
+ pos = spec.get('position', 'inside')
+ if pos == 'replace':
+ if node.getparent() is None:
+ source = copy.deepcopy(spec[0])
+ else:
+ for child in spec:
+ node.addprevious(child)
+ node.getparent().remove(node)
+ elif pos == 'attributes':
+ for child in spec.getiterator('attribute'):
+ attribute = (child.get('name'), child.text and child.text.encode('utf8') or None)
+ if attribute[1]:
+ node.set(attribute[0], attribute[1])
+ elif attribute[0] in node.attrib:
+ del node.attrib[attribute[0]]
+ else:
+ sib = node.getnext()
+ for child in spec:
+ if pos == 'inside':
+ node.append(child)
+ elif pos == 'after':
+ if sib is None:
+ node.addnext(child)
+ node = child
+ else:
+ sib.addprevious(child)
+ elif pos == 'before':
+ node.addprevious(child)
+ else:
+ self.raise_view_error(cr, uid, "Invalid position value: '%s'" % pos, inherit_id, context=context)
+ else:
+ attrs = ''.join([
+ ' %s="%s"' % (attr, spec.get(attr))
+ for attr in spec.attrib
+ if attr != 'position'
+ ])
+ tag = "<%s%s>" % (spec.tag, attrs)
+ self.raise_view_error(cr, uid, "Element '%s' not found in parent view " % tag, inherit_id, context=context)
+
+ return source
+
+ def apply_view_inheritance(self, cr, uid, source, source_id, model, context=None):
+ """ Apply all the (directly and indirectly) inheriting views.
+
+ :param source: a parent architecture to modify (with parent modifications already applied)
+ :param source_id: the database view_id of the parent view
+ :param model: the original model for which we create a view (not
+ necessarily the same as the source's model); only the inheriting
+ views with that specific model will be applied.
+ :return: a modified source where all the modifying architecture are applied
+ """
+ if context is None: context = {}
+ sql_inherit = self.pool.get('ir.ui.view').get_inheriting_views_arch(cr, uid, source_id, model, context=context)
+ for (specs, view_id) in sql_inherit:
+ specs_tree = etree.fromstring(specs.encode('utf-8'))
+ if context.get('inherit_branding'):
+ self.inherit_branding(specs_tree, view_id, source_id)
+ source = self.apply_inheritance_specs(cr, uid, source, specs_tree, view_id, context=context)
+ source = self.apply_view_inheritance(cr, uid, source, view_id, model, context=context)
+ return source
+
+ def read_combined(self, cr, uid, view_id, fields=None, context=None):
+ """
+ Utility function to get a view combined with its inherited views.
+
+ * Gets the top of the view tree if a sub-view is requested
+ * Applies all inherited archs on the root view
+ * Returns the view with all requested fields
+ .. note:: ``arch`` is always added to the fields list even if not
+ requested (similar to ``id``)
+ """
+ if context is None: context = {}
+
+ # if view_id is not a root view, climb back to the top.
- v = self.browse(cr, uid, view_id, context=context)
++ base = v = self.browse(cr, uid, view_id, context=context)
+ while v.inherit_id:
+ v = v.inherit_id
+ root_id = v.id
+
+ # arch and model fields are always returned
+ if fields:
+ fields = list(set(fields) | set(['arch', 'model']))
+
+ # read the view arch
+ [view] = self.read(cr, uid, [root_id], fields=fields, context=context)
+ arch_tree = etree.fromstring(view['arch'].encode('utf-8'))
+
+ if context.get('inherit_branding'):
+ arch_tree.attrib.update({
+ 'data-oe-model': 'ir.ui.view',
+ 'data-oe-id': str(root_id),
+ 'data-oe-field': 'arch',
+ })
+
+ # and apply inheritance
- arch = self.apply_view_inheritance(cr, uid, arch_tree, root_id, v.model, context=context)
++ arch = self.apply_view_inheritance(
++ cr, uid, arch_tree, root_id, base.model, context=context)
+
+ return dict(view, arch=etree.tostring(arch, encoding='utf-8'))
+
+ # postprocessing: groups, modifiers, ...
+
+ def postprocess(self, cr, user, model, node, view_id, in_tree_view, model_fields, context=None):
+ """Return the description of the fields in the node.
+
+ In a normal call to this method, node is a complete view architecture
+ but it is actually possible to give some sub-node (this is used so
+ that the method can call itself recursively).
+
+ Originally, the field descriptions are drawn from the node itself.
+ But there is now some code calling fields_get() in order to merge some
+ of those information in the architecture.
+
+ """
+ if context is None:
+ context = {}
+ result = False
+ fields = {}
+ children = True
+
+ modifiers = {}
+ Model = self.pool.get(model)
+
+ def encode(s):
+ if isinstance(s, unicode):
+ return s.encode('utf8')
+ return s
+
+ def check_group(node):
+ """Apply group restrictions, may be set at view level or model level::
+ * at view level this means the element should be made invisible to
+ people who are not members
+ * at model level (exclusively for fields, obviously), this means
+ the field should be completely removed from the view, as it is
+ completely unavailable for non-members
+
+ :return: True if field should be included in the result of fields_view_get
+ """
+ if Model and node.tag == 'field' and node.get('name') in Model._all_columns:
+ column = Model._all_columns[node.get('name')].column
+ if column.groups and not self.user_has_groups(
+ cr, user, groups=column.groups, context=context):
+ node.getparent().remove(node)
+ fields.pop(node.get('name'), None)
+ # no point processing view-level ``groups`` anymore, return
+ return False
+ if node.get('groups'):
+ can_see = self.user_has_groups(
+ cr, user, groups=node.get('groups'), context=context)
+ if not can_see:
+ node.set('invisible', '1')
+ modifiers['invisible'] = True
+ if 'attrs' in node.attrib:
+ del(node.attrib['attrs']) #avoid making field visible later
+ del(node.attrib['groups'])
+ return True
+
+ if node.tag in ('field', 'node', 'arrow'):
+ if node.get('object'):
+ attrs = {}
+ views = {}
+ xml = "<form>"
+ for f in node:
+ if f.tag == 'field':
+ xml += etree.tostring(f, encoding="utf-8")
+ xml += "</form>"
+ new_xml = etree.fromstring(encode(xml))
+ ctx = context.copy()
+ ctx['base_model_name'] = model
+ xarch, xfields = self.postprocess_and_fields(cr, user, node.get('object'), new_xml, view_id, ctx)
+ views['form'] = {
+ 'arch': xarch,
+ 'fields': xfields
+ }
+ attrs = {'views': views}
+ fields = xfields
+ if node.get('name'):
+ attrs = {}
+ try:
+ if node.get('name') in Model._columns:
+ column = Model._columns[node.get('name')]
+ else:
+ column = Model._inherit_fields[node.get('name')][2]
+ except Exception:
+ column = False
+
+ if column:
+ relation = self.pool[column._obj] if column._obj else None
+
+ children = False
+ views = {}
+ for f in node:
+ if f.tag in ('form', 'tree', 'graph', 'kanban'):
+ node.remove(f)
+ ctx = context.copy()
+ ctx['base_model_name'] = Model
+ xarch, xfields = self.postprocess_and_fields(cr, user, column._obj or None, f, view_id, ctx)
+ views[str(f.tag)] = {
+ 'arch': xarch,
+ 'fields': xfields
+ }
+ attrs = {'views': views}
+ if node.get('widget') and node.get('widget') == 'selection':
+ # Prepare the cached selection list for the client. This needs to be
+ # done even when the field is invisible to the current user, because
+ # other events could need to change its value to any of the selectable ones
+ # (such as on_change events, refreshes, etc.)
+
+ # If domain and context are strings, we keep them for client-side, otherwise
+ # we evaluate them server-side to consider them when generating the list of
+ # possible values
+ # TODO: find a way to remove this hack, by allow dynamic domains
+ dom = []
+ if column._domain and not isinstance(column._domain, basestring):
+ dom = list(column._domain)
+ dom += eval(node.get('domain', '[]'), {'uid': user, 'time': time})
+ search_context = dict(context)
+ if column._context and not isinstance(column._context, basestring):
+ search_context.update(column._context)
+ attrs['selection'] = relation._name_search(cr, user, '', dom, context=search_context, limit=None, name_get_uid=1)
+ if (node.get('required') and not int(node.get('required'))) or not column.required:
+ attrs['selection'].append((False, ''))
+ fields[node.get('name')] = attrs
+
+ field = model_fields.get(node.get('name'))
+ if field:
+ orm.transfer_field_to_modifiers(field, modifiers)
+
+
+ elif node.tag in ('form', 'tree'):
+ result = Model.view_header_get(cr, user, False, node.tag, context)
+ if result:
+ node.set('string', result)
+ in_tree_view = node.tag == 'tree'
+
+ elif node.tag == 'calendar':
+ for additional_field in ('date_start', 'date_delay', 'date_stop', 'color'):
+ if node.get(additional_field):
+ fields[node.get(additional_field)] = {}
+
+ if not check_group(node):
+ # node must be removed, no need to proceed further with its children
+ return fields
+
+ # The view architeture overrides the python model.
+ # Get the attrs before they are (possibly) deleted by check_group below
+ orm.transfer_node_to_modifiers(node, modifiers, context, in_tree_view)
+
+ # TODO remove attrs couterpart in modifiers when invisible is true ?
+
+ # translate view
+ if 'lang' in context:
+ Translations = self.pool['ir.translation']
+ if node.text and node.text.strip():
+ trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.text.strip())
+ if trans:
+ node.text = node.text.replace(node.text.strip(), trans)
+ if node.tail and node.tail.strip():
+ trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.tail.strip())
+ if trans:
+ node.tail = node.tail.replace(node.tail.strip(), trans)
+
+ if node.get('string') and not result:
+ trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.get('string'))
+ if trans == node.get('string') and ('base_model_name' in context):
+ # If translation is same as source, perhaps we'd have more luck with the alternative model name
+ # (in case we are in a mixed situation, such as an inherited view where parent_view.model != model
+ trans = Translations._get_source(cr, user, context['base_model_name'], 'view', context['lang'], node.get('string'))
+ if trans:
+ node.set('string', trans)
+
+ for attr_name in ('confirm', 'sum', 'avg', 'help', 'placeholder'):
+ attr_value = node.get(attr_name)
+ if attr_value:
+ trans = Translations._get_source(cr, user, model, 'view', context['lang'], attr_value)
+ if trans:
+ node.set(attr_name, trans)
+
+ for f in node:
+ if children or (node.tag == 'field' and f.tag in ('filter','separator')):
+ fields.update(self.postprocess(cr, user, model, f, view_id, in_tree_view, model_fields, context))
+
+ orm.transfer_modifiers_to_node(modifiers, node)
+ return fields
+
+ def _disable_workflow_buttons(self, cr, user, model, node):
+ """ Set the buttons in node to readonly if the user can't activate them. """
+ if model is None or user == 1:
+ # admin user can always activate workflow buttons
+ return node
+
+ # TODO handle the case of more than one workflow for a model or multiple
+ # transitions with different groups and same signal
+ usersobj = self.pool.get('res.users')
+ buttons = (n for n in node.getiterator('button') if n.get('type') != 'object')
+ for button in buttons:
+ user_groups = usersobj.read(cr, user, [user], ['groups_id'])[0]['groups_id']
+ cr.execute("""SELECT DISTINCT t.group_id
+ FROM wkf
+ INNER JOIN wkf_activity a ON a.wkf_id = wkf.id
+ INNER JOIN wkf_transition t ON (t.act_to = a.id)
+ WHERE wkf.osv = %s
+ AND t.signal = %s
+ AND t.group_id is NOT NULL
+ """, (model, button.get('name')))
+ group_ids = [x[0] for x in cr.fetchall() if x[0]]
+ can_click = not group_ids or bool(set(user_groups).intersection(group_ids))
+ button.set('readonly', str(int(not can_click)))
+ return node
+
+ def postprocess_and_fields(self, cr, user, model, node, view_id, context=None):
+ """ Return an architecture and a description of all the fields.
+
+ The field description combines the result of fields_get() and
+ postprocess().
+
+ :param node: the architecture as as an etree
+ :return: a tuple (arch, fields) where arch is the given node as a
+ string and fields is the description of all the fields.
+
+ """
+ fields = {}
+ Model = self.pool.get(model)
+
+ if node.tag == 'diagram':
+ if node.getchildren()[0].tag == 'node':
+ node_model = self.pool[node.getchildren()[0].get('object')]
+ node_fields = node_model.fields_get(cr, user, None, context)
+ fields.update(node_fields)
+ if not node.get("create") and not node_model.check_access_rights(cr, user, 'create', raise_exception=False):
+ node.set("create", 'false')
+ if node.getchildren()[1].tag == 'arrow':
+ arrow_fields = self.pool[node.getchildren()[1].get('object')].fields_get(cr, user, None, context)
+ fields.update(arrow_fields)
+ elif Model:
+ fields = Model.fields_get(cr, user, None, context)
+
+ fields_def = self.postprocess(cr, user, model, node, view_id, False, fields, context=context)
+ node = self._disable_workflow_buttons(cr, user, model, node)
+ if node.tag in ('kanban', 'tree', 'form', 'gantt'):
+ for action, operation in (('create', 'create'), ('delete', 'unlink'), ('edit', 'write')):
+ if not node.get(action) and not Model.check_access_rights(cr, user, operation, raise_exception=False):
+ node.set(action, 'false')
+ arch = etree.tostring(node, encoding="utf-8").replace('\t', '')
+ for k in fields.keys():
+ if k not in fields_def:
+ del fields[k]
+ for field in fields_def:
+ if field == 'id':
+ # sometime, the view may contain the (invisible) field 'id' needed for a domain (when 2 objects have cross references)
+ fields['id'] = {'readonly': True, 'type': 'integer', 'string': 'ID'}
+ elif field in fields:
+ fields[field].update(fields_def[field])
+ else:
+ cr.execute('select name, model from ir_ui_view where (id=%s or inherit_id=%s) and arch like %s', (view_id, view_id, '%%%s%%' % field))
+ res = cr.fetchall()[:]
+ model = res[0][1]
+ res.insert(0, ("Can't find field '%s' in the following view parts composing the view of object model '%s':" % (field, model), None))
+ msg = "\n * ".join([r[0] for r in res])
+ msg += "\n\nEither you wrongly customized this view, or some modules bringing those views are not compatible with your current data model"
+ _logger.error(msg)
+ raise orm.except_orm('View error', msg)
+ return arch, fields
+
+ # view used as templates
+
+ @tools.ormcache_context(accepted_keys=('lang','inherit_branding', 'editable', 'translatable'))
+ def read_template(self, cr, uid, id_, context=None):
+ try:
+ id_ = int(id_)
+ except ValueError:
+ if '.' not in id_:
+ raise ValueError('Invalid id: %r' % (id_,))
+ IMD = self.pool['ir.model.data']
+ m, _, n = id_.partition('.')
+ _, id_ = IMD.get_object_reference(cr, uid, m, n)
+
+ arch = self.read_combined(cr, uid, id_, fields=['arch'], context=context)['arch']
+ arch_tree = etree.fromstring(arch)
+
+ if 'lang' in context:
+ arch_tree = self.translate_qweb(cr, uid, id_, arch_tree, context['lang'], context)
+ self.distribute_branding(arch_tree)
+ root = etree.Element('tpl')
+ root.append(arch_tree)
+ arch = etree.tostring(root, encoding='utf-8', xml_declaration=True)
+ return arch
+
+ def distribute_branding(self, e, branding=None, parent_xpath='',
+ index_map=misc.ConstantMapping(1)):
+ if e.get('t-ignore') or e.tag == 'head':
+ # TODO: find a better name and check if we have a string to boolean helper
+ return
+
+ node_path = e.get('data-oe-xpath')
+ if node_path is None:
+ node_path = "%s/%s[%d]" % (parent_xpath, e.tag, index_map[e.tag])
+ if branding and not (e.get('data-oe-model') or e.get('t-field')):
+ e.attrib.update(branding)
+ e.set('data-oe-xpath', node_path)
+ if not e.get('data-oe-model'): return
+
+ # if a branded element contains branded elements distribute own
+ # branding to children unless it's t-raw, then just remove branding
+ # on current element
+ if e.tag == 't' or 't-raw' in e.attrib or \
+ any(self.is_node_branded(child) for child in e.iterdescendants()):
+ distributed_branding = dict(
+ (attribute, e.attrib.pop(attribute))
+ for attribute in MOVABLE_BRANDING
+ if e.get(attribute))
+
+ if 't-raw' not in e.attrib:
+ # TODO: collections.Counter if remove p2.6 compat
+ # running index by tag type, for XPath query generation
+ indexes = collections.defaultdict(lambda: 0)
+ for child in e.iterchildren(tag=etree.Element):
+ indexes[child.tag] += 1
+ self.distribute_branding(child, distributed_branding,
+ parent_xpath=node_path,
+ index_map=indexes)
+
+ def is_node_branded(self, node):
+ """ Finds out whether a node is branded or qweb-active (bears a
+ @data-oe-model or a @t-* *which is not t-field* as t-field does not
+ section out views)
+
+ :param node: an etree-compatible element to test
+ :type node: etree._Element
+ :rtype: boolean
+ """
+ return any(
+ (attr == 'data-oe-model' or (attr != 't-field' and attr.startswith('t-')))
+ for attr in node.attrib
+ )
+
+ def translate_qweb(self, cr, uid, id_, arch, lang, context=None):
+ # TODO: this should be moved in a place before inheritance is applied
+ # but process() is only called on fields_view_get()
+ Translations = self.pool['ir.translation']
+ h = HTMLParser.HTMLParser()
+ def get_trans(text):
+ if not text or not text.strip():
+ return None
+ text = h.unescape(text.strip())
+ if len(text) < 2 or (text.startswith('<!') and text.endswith('>')):
+ return None
+ # if text == 'Our Events':
+ # from pudb import set_trace;set_trace() ############################## Breakpoint ##############################
+ return Translations._get_source(cr, uid, 'website', 'view', lang, text, id_)
+
+ if arch.tag not in ['script']:
+ text = get_trans(arch.text)
+ if text:
+ arch.text = arch.text.replace(arch.text.strip(), text)
+ tail = get_trans(arch.tail)
+ if tail:
+ arch.tail = arch.tail.replace(arch.tail.strip(), tail)
+
+ for attr_name in ('title', 'alt', 'placeholder'):
+ attr = get_trans(arch.get(attr_name))
+ if attr:
+ arch.set(attr_name, attr)
+ for node in arch.iterchildren("*"):
+ self.translate_qweb(cr, uid, id_, node, lang, context)
+ return arch
+
+ def render(self, cr, uid, id_or_xml_id, values=None, engine='ir.qweb', context=None):
+ if not context:
+ context = {}
+
+ def loader(name):
+ return self.read_template(cr, uid, name, context=context)
+
+ return self.pool[engine].render(
+ cr, uid, id_or_xml_id, values,
+ loader=loader, undefined_handler=lambda key, v: None,
+ context=context)
+
+ # maybe used to print the workflow ?
def graph_get(self, cr, uid, id, model, node_obj, conn_obj, src_node, des_node, label, scale, context=None):
nodes=[]
+# -*- encoding: utf-8 -*-
from functools import partial
+ import unittest2
+
-import lxml.etree
+from lxml import etree as ET
from lxml.builder import E
- import unittest2
-import openerp.tests.common as common
-from openerp.osv.orm import except_orm
+from openerp.tests import common
- from openerp.osv.orm import except_orm
from openerp.tools import mute_logger
-class test_views(common.TransactionCase):
+Field = E.field
- @mute_logger('openerp.osv.orm', 'openerp.addons.base.ir.ir_ui_view')
- def test_whatever(self):
- Views = self.registry('ir.ui.view')
- self.assertTrue(Views.pool._init)
-
- error_msg = "The model name does not exist or the view architecture cannot be rendered"
- # test arch check is call for views without xmlid during registry initialization
- with self.assertRaisesRegexp(except_orm, error_msg):
- Views.create(self.cr, self.uid, {
- 'name': 'Test View #1',
- 'model': 'ir.ui.view',
- 'arch': """<?xml version="1.0"?>
- <tree>
- <field name="test_1"/>
- </tree>
- """,
- })
+class TestNodeLocator(common.BaseCase):
+ """
+ The node locator returns None when it can not find a node, and the first
+ match when it finds something (no jquery-style node sets)
+ """
+ def setUp(self):
+ super(TestNodeLocator, self).setUp()
+ self.Views = self.registry('ir.ui.view')
- # same for inherited views
- with self.assertRaisesRegexp(except_orm, error_msg):
- # Views.pudb = True
- Views.create(self.cr, self.uid, {
- 'name': 'Test View #2',
- 'model': 'ir.ui.view',
- 'inherit_id': self.browse_ref('base.view_view_tree').id,
- 'arch': """<?xml version="1.0"?>
- <xpath expr="//field[@name='name']" position="after">
- <field name="test_2"/>
- </xpath>
- """,
- })
+ def test_no_match_xpath(self):
+ """
+ xpath simply uses the provided @expr pattern to find a node
+ """
+ node = self.Views.locate_node(
+ E.root(E.foo(), E.bar(), E.baz()),
+ E.xpath(expr="//qux"))
+ self.assertIsNone(node)
+
+ def test_match_xpath(self):
+ bar = E.bar()
+ node = self.Views.locate_node(
+ E.root(E.foo(), bar, E.baz()),
+ E.xpath(expr="//bar"))
+ self.assertIs(node, bar)
+
+
+ def test_no_match_field(self):
+ """
+ A field spec will match by @name against all fields of the view
+ """
+ node = self.Views.locate_node(
+ E.root(E.foo(), E.bar(), E.baz()),
+ Field(name="qux"))
+ self.assertIsNone(node)
+
+ node = self.Views.locate_node(
+ E.root(Field(name="foo"), Field(name="bar"), Field(name="baz")),
+ Field(name="qux"))
+ self.assertIsNone(node)
+
+ def test_match_field(self):
+ bar = Field(name="bar")
+ node = self.Views.locate_node(
+ E.root(Field(name="foo"), bar, Field(name="baz")),
+ Field(name="bar"))
+ self.assertIs(node, bar)
+
+
+ def test_no_match_other(self):
+ """
+ Non-xpath non-fields are matched by node name first
+ """
+ node = self.Views.locate_node(
+ E.root(E.foo(), E.bar(), E.baz()),
+ E.qux())
+ self.assertIsNone(node)
+
+ def test_match_other(self):
+ bar = E.bar()
+ node = self.Views.locate_node(
+ E.root(E.foo(), bar, E.baz()),
+ E.bar())
+ self.assertIs(bar, node)
+
+ def test_attribute_mismatch(self):
+ """
+ Non-xpath non-field are filtered by matching attributes on spec and
+ matched nodes
+ """
+ node = self.Views.locate_node(
+ E.root(E.foo(attr='1'), E.bar(attr='2'), E.baz(attr='3')),
+ E.bar(attr='5'))
+ self.assertIsNone(node)
+
+ def test_attribute_filter(self):
+ match = E.bar(attr='2')
+ node = self.Views.locate_node(
+ E.root(E.bar(attr='1'), match, E.root(E.bar(attr='3'))),
+ E.bar(attr='2'))
+ self.assertIs(node, match)
+
+ def test_version_mismatch(self):
+ """
+ A @version on the spec will be matched against the view's version
+ """
+ node = self.Views.locate_node(
+ E.root(E.foo(attr='1'), version='4'),
+ E.foo(attr='1', version='3'))
+ self.assertIsNone(node)
+
+class TestViewInheritance(common.TransactionCase):
+ def arch_for(self, name, view_type='form', parent=None):
+ """ Generates a trivial view of the specified ``view_type``.
+
+ The generated view is empty but ``name`` is set as its root's ``@string``.
+
+ If ``parent`` is not falsy, generates an extension view (instead of
+ a root view) replacing the parent's ``@string`` by ``name``
+
+ :param str name: ``@string`` value for the view root
+ :param str view_type:
+ :param bool parent:
+ :return: generated arch
+ :rtype: str
+ """
+ if not parent:
+ element = E(view_type, string=name)
+ else:
+ element = E(view_type,
+ E.attribute(name, name='string'),
+ position='attributes'
+ )
+ return ET.tostring(element)
+
+ def makeView(self, name, parent=None, arch=None):
+ """ Generates a basic ir.ui.view with the provided name, parent and arch.
+
+ If no parent is provided, the view is top-level.
+
+ If no arch is provided, generates one by calling :meth:`~.arch_for`.
+
+ :param str name:
+ :param int parent: id of the parent view, if any
+ :param str arch:
+ :returns: the created view's id.
+ :rtype: int
+ """
+ view_id = self.View.create(self.cr, self.uid, {
+ 'model': self.model,
+ 'name': name,
+ 'arch': arch or self.arch_for(name, parent=parent),
+ 'inherit_id': parent,
+ })
+ self.ids[name] = view_id
+ return view_id
+
+ def setUp(self):
+ super(TestViewInheritance, self).setUp()
+
+ self.model = 'dummy'
+ self.View = self.registry('ir.ui.view')
+ self._init = self.View.pool._init
+ self.View.pool._init = False
+ self.ids = {}
+
+ a = self.makeView("A")
+ a1 = self.makeView("A1", a)
+ a11 = self.makeView("A11", a1)
+ self.makeView("A111", a11)
+ self.makeView("A12", a1)
+ a2 = self.makeView("A2", a)
+ self.makeView("A21", a2)
+ a22 = self.makeView("A22", a2)
+ self.makeView("A221", a22)
+
+ b = self.makeView('B', arch=self.arch_for("B", 'tree'))
+ self.makeView('B1', b, arch=self.arch_for("B1", 'tree', parent=b))
+ c = self.makeView('C', arch=self.arch_for("C", 'tree'))
+ self.View.write(self.cr, self.uid, c, {'priority': 1})
+
+ def tearDown(self):
+ self.View.pool._init = self._init
+ super(TestViewInheritance, self).tearDown()
+
+ def test_get_inheriting_views_arch(self):
+ self.assertEqual(self.View.get_inheriting_views_arch(
+ self.cr, self.uid, self.ids['A'], self.model), [
+ (self.arch_for('A1', parent=True), self.ids['A1']),
+ (self.arch_for('A2', parent=True), self.ids['A2']),
+ ])
+
+ self.assertEqual(self.View.get_inheriting_views_arch(
+ self.cr, self.uid, self.ids['A21'], self.model),
+ [])
+
+ self.assertEqual(self.View.get_inheriting_views_arch(
+ self.cr, self.uid, self.ids['A11'], self.model),
+ [(self.arch_for('A111', parent=True), self.ids['A111'])])
+
+ def test_default_view(self):
+ default = self.View.default_view(
+ self.cr, self.uid, model=self.model, view_type='form')
+ self.assertEqual(default, self.ids['A'])
+
+ default_tree = self.View.default_view(
+ self.cr, self.uid, model=self.model, view_type='tree')
+ self.assertEqual(default_tree, self.ids['C'])
+
+ def test_no_default_view(self):
+ self.assertFalse(
+ self.View.default_view(
+ self.cr, self.uid, model='does.not.exist', view_type='form'))
+
+ self.assertFalse(
+ self.View.default_view(
+ self.cr, self.uid, model=self.model, view_type='graph'))
+
+class TestApplyInheritanceSpecs(common.TransactionCase):
+ """ Applies a sequence of inheritance specification nodes to a base
+ architecture. IO state parameters (cr, uid, model, context) are used for
+ error reporting
+
+ The base architecture is altered in-place.
+ """
+ def setUp(self):
+ super(TestApplyInheritanceSpecs, self).setUp()
+ self.View = self.registry('ir.ui.view')
+ self.base_arch = E.form(
+ Field(name="target"),
+ string="Title")
+
+ def test_replace(self):
+ spec = Field(
+ Field(name="replacement"),
+ name="target", position="replace")
+
+ self.View.apply_inheritance_specs(self.cr, self.uid,
+ self.base_arch,
+ spec, None)
+
+ self.assertEqual(
+ ET.tostring(self.base_arch),
+ ET.tostring(E.form(Field(name="replacement"), string="Title")))
+
+ def test_delete(self):
+ spec = Field(name="target", position="replace")
+
+ self.View.apply_inheritance_specs(self.cr, self.uid,
+ self.base_arch,
+ spec, None)
+
+ self.assertEqual(
+ ET.tostring(self.base_arch),
+ ET.tostring(E.form(string="Title")))
+
+ def test_insert_after(self):
+ spec = Field(
+ Field(name="inserted"),
+ name="target", position="after")
+
+ self.View.apply_inheritance_specs(self.cr, self.uid,
+ self.base_arch,
+ spec, None)
+
+ self.assertEqual(
+ ET.tostring(self.base_arch),
+ ET.tostring(E.form(
+ Field(name="target"),
+ Field(name="inserted"),
+ string="Title"
+ )))
+
+ def test_insert_before(self):
+ spec = Field(
+ Field(name="inserted"),
+ name="target", position="before")
+
+ self.View.apply_inheritance_specs(self.cr, self.uid,
+ self.base_arch,
+ spec, None)
+
+ self.assertEqual(
+ ET.tostring(self.base_arch),
+ ET.tostring(E.form(
+ Field(name="inserted"),
+ Field(name="target"),
+ string="Title")))
+
+ def test_insert_inside(self):
+ default = Field(Field(name="inserted"), name="target")
+ spec = Field(Field(name="inserted 2"), name="target", position='inside')
+
+ self.View.apply_inheritance_specs(self.cr, self.uid,
+ self.base_arch,
+ default, None)
+ self.View.apply_inheritance_specs(self.cr, self.uid,
+ self.base_arch,
+ spec, None)
+
+ self.assertEqual(
+ ET.tostring(self.base_arch),
+ ET.tostring(E.form(
+ Field(
+ Field(name="inserted"),
+ Field(name="inserted 2"),
+ name="target"),
+ string="Title")))
+
+ def test_unpack_data(self):
+ spec = E.data(
+ Field(Field(name="inserted 0"), name="target"),
+ Field(Field(name="inserted 1"), name="target"),
+ Field(Field(name="inserted 2"), name="target"),
+ Field(Field(name="inserted 3"), name="target"),
+ )
+
+ self.View.apply_inheritance_specs(self.cr, self.uid,
+ self.base_arch,
+ spec, None)
+
+ self.assertEqual(
+ ET.tostring(self.base_arch),
+ ET.tostring(E.form(
+ Field(
+ Field(name="inserted 0"),
+ Field(name="inserted 1"),
+ Field(name="inserted 2"),
+ Field(name="inserted 3"),
+ name="target"),
+ string="Title")))
+
+ def test_invalid_position(self):
+ spec = Field(
+ Field(name="whoops"),
+ name="target", position="serious_series")
+
+ with self.assertRaises(AttributeError):
+ self.View.apply_inheritance_specs(self.cr, self.uid,
+ self.base_arch,
+ spec, None)
+
+ def test_incorrect_version(self):
+ # Version ignored on //field elements, so use something else
+ arch = E.form(E.element(foo="42"))
+ spec = E.element(
+ Field(name="placeholder"),
+ foo="42", version="7.0")
+
+ with self.assertRaises(AttributeError):
+ self.View.apply_inheritance_specs(self.cr, self.uid,
+ arch,
+ spec, None)
+
+ def test_target_not_found(self):
+ spec = Field(name="targut")
+
+ with self.assertRaises(AttributeError):
+ self.View.apply_inheritance_specs(self.cr, self.uid,
+ self.base_arch,
+ spec, None)
+
+class TestApplyInheritedArchs(common.TransactionCase):
+ """ Applies a sequence of modificator archs to a base view
+ """
+
+class TestViewCombined(common.TransactionCase):
+ """
+ Test fallback operations of View.read_combined:
+ * defaults mapping
+ * ?
+ """
+
+class TestNoModel(common.TransactionCase):
+ def test_create_view_nomodel(self):
+ View = self.registry('ir.ui.view')
+ view_id = View.create(self.cr, self.uid, {
+ 'name': 'dummy',
+ 'arch': '<form string=""/>',
+ 'inherit_id': False
+ })
+ fields = ['name', 'arch', 'type', 'priority', 'inherit_id', 'model']
+ [view] = View.read(self.cr, self.uid, [view_id], fields)
+ self.assertEqual(view, {
+ 'id': view_id,
+ 'name': 'dummy',
+ 'arch': '<form string=""/>',
+ 'type': 'form',
+ 'priority': 16,
+ 'inherit_id': False,
+ 'model': False,
+ })
+
+ arch = E.body(
+ E.div(
+ E.h1("Title"),
+ id="header"),
+ E.p("Welcome!"),
+ E.div(
+ E.hr(),
+ E.p("Copyright copyrighter", {'class': 'legalese'}),
+ id="footer"),
+ {'class': "index"},)
+ def test_fields_mess(self):
+ """
+ Try to call __view_look_dom_arch without a model provided, will need
+ to be altered once it's broken up into sane components
+ """
+ View = self.registry('ir.ui.view')
+
+ sarch, fields = View.postprocess_and_fields(
+ self.cr, self.uid, None, self.arch, None)
+
+ self.assertEqual(sarch, ET.tostring(self.arch, encoding='utf-8'))
+ self.assertEqual(fields, {})
+
+ def test_mess_translation(self):
+ """
+ Test if translations work correctly without a model
+ """
+ View = self.registry('ir.ui.view')
+ self.registry('res.lang').load_lang(self.cr, self.uid, 'fr_FR')
+ self.registry('ir.translation').create(self.cr, self.uid, {
+ 'name': '',
+ 'type': 'view',
+ 'lang': 'fr_FR',
+ 'src': 'Copyright copyrighter',
+ 'value': u"Copyrighter, tous droits réservés",
+ })
+ sarch, fields = View.postprocess_and_fields(
+ self.cr, self.uid, None, self.arch, None, {'lang': 'fr_FR'})
+ self.assertEqual(
+ sarch,
+ ET.tostring(self.arch, encoding='utf-8')
+ .replace('Copyright copyrighter',
+ 'Copyrighter, tous droits réservés'))
+ self.assertEqual(fields, {})
+
+
+class test_views(common.TransactionCase):
- def test_20_remove_unexisting_attribute(self):
+ def test_nonexistent_attribute_removal(self):
Views = self.registry('ir.ui.view')
Views.create(self.cr, self.uid, {
'name': 'Test View',
<field name="name"/>
</xpath>
""",
- })
+ )
self.assertTrue(validate()) # inherited view
+
- # validation of a bad inherited view
- self._insert_view(**{
- 'name': 'bad inherited view',
- 'model': model,
- 'priority': 2,
- 'inherit_id': vid,
- 'arch': """<?xml version="1.0"?>
- <xpath expr="//field[@name='url']" position="after">
- <field name="bad"/>
- </xpath>
- """,
- })
- with mute_logger('openerp.osv.orm', 'openerp.addons.base.ir.ir_ui_view'):
- self.assertFalse(validate()) # bad inherited view
-
+ def test_view_inheritance(self):
+ Views = self.registry('ir.ui.view')
+
+ v1 = Views.create(self.cr, self.uid, {
+ 'name': "bob",
+ 'model': 'ir.ui.view',
+ 'arch': """
+ <form string="Base title" version="7.0">
+ <separator string="separator" colspan="4"/>
+ <footer>
+ <button name="action_next" type="object" string="Next button"/>
+ or
+ <button string="Skip" special="cancel" />
+ </footer>
+ </form>
+ """
+ })
+ v2 = Views.create(self.cr, self.uid, {
+ 'name': "edmund",
+ 'model': 'ir.ui.view',
+ 'inherit_id': v1,
+ 'arch': """
+ <data>
+ <form position="attributes" version="7.0">
+ <attribute name="string">Replacement title</attribute>
+ </form>
+ <footer position="replace">
+ <footer>
+ <button name="action_next" type="object" string="New button"/>
+ </footer>
+ </footer>
+ <separator string="separator" position="replace">
+ <p>Replacement data</p>
+ </separator>
+ </data>
+ """
+ })
+ v3 = Views.create(self.cr, self.uid, {
+ 'name': 'jake',
+ 'model': 'ir.ui.view',
+ 'inherit_id': v1,
+ 'priority': 17,
+ 'arch': """
+ <footer position="attributes">
+ <attribute name="thing">bob</attribute>
+ </footer>
+ """
+ })
+
+ view = self.registry('ir.ui.view').fields_view_get(
+ self.cr, self.uid, v2, view_type='form', context={
+ # fucking what?
+ 'check_view_ids': [v2, v3]
+ })
+ self.assertEqual(view['type'], 'form')
+ self.assertEqual(
- lxml.etree.tostring(lxml.etree.fromstring(
++ ET.tostring(ET.fromstring(
+ view['arch'],
- parser=lxml.etree.XMLParser(remove_blank_text=True)
++ parser=ET.XMLParser(remove_blank_text=True)
+ )),
+ '<form string="Replacement title" version="7.0">'
+ '<p>Replacement data</p>'
+ '<footer thing="bob">'
+ '<button name="action_next" type="object" string="New button"/>'
+ '</footer>'
+ '</form>')
+
+ def test_view_inheritance_divergent_models(self):
+ Views = self.registry('ir.ui.view')
+
+ v1 = Views.create(self.cr, self.uid, {
+ 'name': "bob",
+ 'model': 'ir.ui.view.custom',
+ 'arch': """
+ <form string="Base title" version="7.0">
+ <separator string="separator" colspan="4"/>
+ <footer>
+ <button name="action_next" type="object" string="Next button"/>
+ or
+ <button string="Skip" special="cancel" />
+ </footer>
+ </form>
+ """
+ })
+ v2 = Views.create(self.cr, self.uid, {
+ 'name': "edmund",
+ 'model': 'ir.ui.view',
+ 'inherit_id': v1,
+ 'arch': """
+ <data>
+ <form position="attributes" version="7.0">
+ <attribute name="string">Replacement title</attribute>
+ </form>
+ <footer position="replace">
+ <footer>
+ <button name="action_next" type="object" string="New button"/>
+ </footer>
+ </footer>
+ <separator string="separator" position="replace">
+ <p>Replacement data</p>
+ </separator>
+ </data>
+ """
+ })
+ v3 = Views.create(self.cr, self.uid, {
+ 'name': 'jake',
+ 'model': 'ir.ui.menu',
+ 'inherit_id': v1,
+ 'priority': 17,
+ 'arch': """
+ <footer position="attributes">
+ <attribute name="thing">bob</attribute>
+ </footer>
+ """
+ })
+
+ view = self.registry('ir.ui.view').fields_view_get(
+ self.cr, self.uid, v2, view_type='form', context={
+ # fucking what?
+ 'check_view_ids': [v2, v3]
+ })
+ self.assertEqual(view['type'], 'form')
+ self.assertEqual(
- lxml.etree.tostring(lxml.etree.fromstring(
++ ET.tostring(ET.fromstring(
+ view['arch'],
- parser=lxml.etree.XMLParser(remove_blank_text=True)
++ parser=ET.XMLParser(remove_blank_text=True)
+ )),
+ '<form string="Replacement title" version="7.0">'
+ '<p>Replacement data</p>'
+ '<footer>'
+ '<button name="action_next" type="object" string="New button"/>'
+ '</footer>'
+ '</form>')
+
-if __name__ == '__main__':
- unittest2.main()