from lxml import etree
import openerp
-from openerp import tools
+from openerp import tools, api
from openerp.http import request
+from openerp.modules.module import get_resource_path, get_resource_from_path
from openerp.osv import fields, osv, orm
-from openerp.tools import graph, SKIPPED_ELEMENT_TYPES
+from openerp.tools import config, graph, SKIPPED_ELEMENT_TYPES, SKIPPED_ELEMENTS
+from openerp.tools.convert import _fix_multiple_roots
+from openerp.tools.parse_version import parse_version
from openerp.tools.safe_eval import safe_eval as eval
from openerp.tools.view_validation import valid_view
from openerp.tools import misc
_logger = logging.getLogger(__name__)
-MOVABLE_BRANDING = ['data-oe-model', 'data-oe-id', 'data-oe-field', 'data-oe-xpath']
-
-def keep_query(*args, **kw):
- if not args and not kw:
- args = ('*',)
- params = kw.copy()
- query_params = frozenset(werkzeug.url_decode(request.httprequest.query_string).keys())
- for keep_param in args:
- for param in fnmatch.filter(query_params, keep_param):
- if param not in params and param in request.params:
- params[param] = request.params[param]
+MOVABLE_BRANDING = ['data-oe-model', 'data-oe-id', 'data-oe-field', 'data-oe-xpath', 'data-oe-source-id']
+
+def keep_query(*keep_params, **additional_params):
+ """
+ Generate a query string keeping the current request querystring's parameters specified
+ in ``keep_params`` and also adds the parameters specified in ``additional_params``.
+
+ Multiple values query string params will be merged into a single one with comma seperated
+ values.
+
+ The ``keep_params`` arguments can use wildcards too, eg:
+
+ keep_query('search', 'shop_*', page=4)
+ """
+ if not keep_params and not additional_params:
+ keep_params = ('*',)
+ params = additional_params.copy()
+ qs_keys = request.httprequest.args.keys()
+ for keep_param in keep_params:
+ for param in fnmatch.filter(qs_keys, keep_param):
+ if param not in additional_params and param in qs_keys:
+ params[param] = ','.join(request.httprequest.args.getlist(param))
return werkzeug.urls.url_encode(params)
class view_custom(osv.osv):
if not cr.fetchone():
cr.execute('CREATE INDEX ir_ui_view_custom_user_id_ref_id ON ir_ui_view_custom (user_id, ref_id)')
+def _hasclass(context, *cls):
+ """ Checks if the context node has all the classes passed as arguments
+ """
+ node_classes = set(context.context_node.attrib.get('class', '').split())
+
+ return node_classes.issuperset(cls)
+
+def get_view_arch_from_file(filename, xmlid):
+ doc = etree.parse(filename)
+ node = None
+ for n in doc.xpath('//*[@id="%s"] | //*[@id="%s"]' % (xmlid, xmlid.split('.')[1])):
+ if n.tag in ('template', 'record'):
+ node = n
+ break
+ if node is not None:
+ if node.tag == 'record':
+ field = node.find('field[@name="arch"]')
+ _fix_multiple_roots(field)
+ inner = ''.join([etree.tostring(child) for child in field.iterchildren()])
+ return field.text + inner
+ elif node.tag == 'template':
+ # The following dom operations has been copied from convert.py's _tag_template()
+ if not node.get('inherit_id'):
+ node.set('t-name', xmlid)
+ node.tag = 't'
+ else:
+ node.tag = 'data'
+ node.attrib.pop('id', None)
+ return etree.tostring(node)
+ _logger.warning("Could not find view arch definition in file '%s' for xmlid '%s'" % (filename, xmlid))
+ return None
+
+xpath_utils = etree.FunctionNamespace(None)
+xpath_utils['hasclass'] = _hasclass
+
class view(osv.osv):
_name = 'ir.ui.view'
data_ids = IMD.search_read(cr, uid, [('id', 'in', ids), ('model', '=', 'ir.ui.view')], ['res_id'], context=context)
return map(itemgetter('res_id'), data_ids)
+ def _arch_get(self, cr, uid, ids, name, arg, context=None):
+ result = {}
+ for view in self.browse(cr, uid, ids, context=context):
+ arch_fs = None
+ if config['dev_mode'] and view.arch_fs and view.xml_id:
+ # It is safe to split on / herebelow because arch_fs is explicitely stored with '/'
+ fullpath = get_resource_path(*view.arch_fs.split('/'))
+ arch_fs = get_view_arch_from_file(fullpath, view.xml_id)
+ result[view.id] = arch_fs or view.arch_db
+ return result
+
+ def _arch_set(self, cr, uid, ids, field_name, field_value, args, context=None):
+ if not isinstance(ids, list):
+ ids = [ids]
+ if field_value:
+ for view in self.browse(cr, uid, ids, context=context):
+ data = dict(arch_db=field_value)
+ key = 'install_mode_data'
+ if context and key in context:
+ imd = context[key]
+ if self._model._name == imd['model'] and (not view.xml_id or view.xml_id == imd['xml_id']):
+ # we store the relative path to the resource instead of the absolute path
+ data['arch_fs'] = '/'.join(get_resource_from_path(imd['xml_file'])[0:2])
+ self.write(cr, uid, ids, data, context=context)
+
+ return True
+
_columns = {
'name': fields.char('View Name', required=True),
'model': fields.char('Object', select=True),
+ 'key': fields.char(string='Key'),
'priority': fields.integer('Sequence', required=True),
'type': fields.selection([
('tree','Tree'),
('form','Form'),
('graph', 'Graph'),
+ ('pivot', 'Pivot'),
('calendar', 'Calendar'),
('diagram','Diagram'),
('gantt', 'Gantt'),
('kanban', 'Kanban'),
('search','Search'),
('qweb', 'QWeb')], string='View Type'),
- 'arch': fields.text('View Architecture', required=True),
- 'inherit_id': fields.many2one('ir.ui.view', 'Inherited View', ondelete='cascade', select=True),
+ 'arch': fields.function(_arch_get, fnct_inv=_arch_set, string='View Architecture', type="text", nodrop=True),
+ 'arch_db': fields.text('Arch Blob'),
+ 'arch_fs': fields.char('Arch Filename'),
+ 'inherit_id': fields.many2one('ir.ui.view', 'Inherited View', ondelete='restrict', select=True),
'inherit_children_ids': fields.one2many('ir.ui.view','inherit_id', 'Inherit Views'),
'field_parent': fields.char('Child Field'),
'model_data_id': fields.function(_get_model_data, type='many2one', relation='ir.model.data', string="Model Data",
'model_ids': fields.one2many('ir.model.data', 'res_id', domain=[('model','=','ir.ui.view')], auto_join=True),
'create_date': fields.datetime('Create Date', readonly=True),
'write_date': fields.datetime('Last Modification Date', readonly=True),
+
+ 'mode': fields.selection(
+ [('primary', "Base view"), ('extension', "Extension View")],
+ string="View inheritance mode", required=True,
+ help="""Only applies if this view inherits from an other one (inherit_id is not False/Null).
+
+* if extension (default), if this view is requested the closest primary view
+ is looked up (via inherit_id), then all views inheriting from it with this
+ view's model are applied
+* if primary, the closest primary view is fully resolved (even if it uses a
+ different model than this one), then this view's inheritance specs
+ (<xpath/>) are applied, and the result is used as if it were this view's
+ actual arch.
+"""),
+ 'active': fields.boolean("Active",
+ help="""If this view is inherited,
+* if True, the view always extends its parent
+* if False, the view currently does not extend its parent but can be enabled
+ """),
}
_defaults = {
+ 'mode': 'primary',
+ 'active': True,
'priority': 16,
}
_order = "priority,name"
view_docs = view_docs[0]
validator = self._relaxng()
for view_arch in view_docs:
- if (view_arch.get('version') < '7.0') and validator and not validator.validate(view_arch):
+ version = view_arch.get('version', '7.0')
+ if parse_version(version) < parse_version('7.0') and validator and not validator.validate(view_arch):
for error in validator.error_log:
_logger.error(tools.ustr(error))
return False
return False
return True
+ _sql_constraints = [
+ ('inheritance_mode',
+ "CHECK (mode != 'extension' OR inherit_id IS NOT NULL)",
+ "Invalid inheritance mode: if the mode is 'extension', the view must"
+ " extend an other view"),
+ ]
_constraints = [
- (_check_xml, 'Invalid view definition', ['arch'])
+ (_check_xml, 'Invalid view definition', ['arch']),
]
def _auto_init(self, cr, context=None):
if not cr.fetchone():
cr.execute('CREATE INDEX ir_ui_view_model_type_inherit_id ON ir_ui_view (model, inherit_id)')
+ def _compute_defaults(self, cr, uid, values, context=None):
+ if 'inherit_id' in values:
+ values.setdefault(
+ 'mode', 'extension' if values['inherit_id'] else 'primary')
+ return values
+
def create(self, cr, uid, values, context=None):
- if 'type' not in values:
+ if not values.get('type'):
if values.get('inherit_id'):
values['type'] = self.browse(cr, uid, values['inherit_id'], context).type
else:
values['type'] = etree.fromstring(values['arch']).tag
if not values.get('name'):
- values['name'] = "%s %s" % (values['model'], values['type'])
+ values['name'] = "%s %s" % (values.get('model'), values['type'])
- self.read_template.clear_cache(self)
- return super(view, self).create(cr, uid, values, context)
+ self.clear_cache()
+ return super(view, self).create(
+ cr, uid,
+ self._compute_defaults(cr, uid, values, context=context),
+ context=context)
def write(self, cr, uid, ids, vals, context=None):
if not isinstance(ids, (list, tuple)):
if context is None:
context = {}
+ # If view is modified we remove the arch_fs information thus activating the arch_db
+ # version. An `init` of the view will restore the arch_fs for the --dev mode
+ if 'arch' in vals and 'install_mode_data' not in context:
+ vals['arch_fs'] = False
+
# 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)
- self.read_template.clear_cache(self)
- ret = super(view, self).write(cr, uid, ids, vals, context)
+ self.clear_cache()
+ ret = super(view, self).write(
+ cr, uid, ids,
+ self._compute_defaults(cr, uid, vals, context=context),
+ context)
return ret
- def copy(self, cr, uid, id, default=None, context=None):
- if not default:
- default = {}
- default.update({
- 'model_ids': [],
- })
- return super(view, self).copy(cr, uid, id, default, context=context)
+ def toggle(self, cr, uid, ids, context=None):
+ """ Switches between enabled and disabled statuses
+ """
+ for view in self.browse(cr, uid, ids, context=dict(context or {}, active_test=False)):
+ view.write({'active': not view.active})
# default view selection
def default_view(self, cr, uid, model, view_type, context=None):
""" Fetches the default view for the provided (model, view_type) pair:
- view with no parent (inherit_id=Fase) with the lowest priority.
+ primary view with the lowest priority.
:param str model:
:param int view_type:
domain = [
['model', '=', model],
['type', '=', view_type],
- ['inherit_id', '=', False],
+ ['mode', '=', 'primary'],
]
- ids = self.search(cr, uid, domain, limit=1, order='priority', context=context)
+ ids = self.search(cr, uid, domain, limit=1, context=context)
if not ids:
return False
return ids[0]
user = self.pool['res.users'].browse(cr, 1, uid, context=context)
user_groups = frozenset(user.groups_id or ())
- check_view_ids = context and context.get('check_view_ids') or (0,)
- conditions = [['inherit_id', '=', view_id], ['model', '=', model]]
+ conditions = [
+ ['inherit_id', '=', view_id],
+ ['model', '=', model],
+ ['mode', '=', 'extension'],
+ ['active', '=', True],
+ ]
if self.pool._init:
# Module init currently in progress, only consider views from
# modules whose code is already loaded
conditions.extend([
'|',
['model_ids.module', 'in', tuple(self.pool._init_modules)],
- ['id', 'in', check_view_ids],
+ ['id', 'in', context and context.get('check_view_ids') or (0,)],
])
view_ids = self.search(cr, uid, conditions, context=context)
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)
+ attribute = (child.get('name'), child.text or None)
if attribute[1]:
node.set(attribute[0], attribute[1])
elif attribute[0] in node.attrib:
if context is None: context = {}
if root_id is None:
root_id = source_id
- sql_inherit = self.pool.get('ir.ui.view').get_inheriting_views_arch(cr, uid, source_id, model, context=context)
+ sql_inherit = self.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'):
# if view_id is not a root view, climb back to the top.
base = v = self.browse(cr, uid, view_id, context=context)
- while v.inherit_id:
+ while v.mode != 'primary':
v = v.inherit_id
root_id = v.id
# read the view arch
[view] = self.read(cr, uid, [root_id], fields=fields, context=context)
- arch_tree = etree.fromstring(view['arch'].encode('utf-8'))
+ view_arch = etree.fromstring(view['arch'].encode('utf-8'))
+ if not v.inherit_id:
+ arch_tree = view_arch
+ else:
+ parent_view = self.read_combined(
+ cr, uid, v.inherit_id.id, fields=fields, context=context)
+ arch_tree = etree.fromstring(parent_view['arch'])
+ self.apply_inheritance_specs(
+ cr, uid, arch_tree, view_arch, parent_view['id'], context=context)
+
if context.get('inherit_branding'):
arch_tree.attrib.update({
modifiers = {}
Model = self.pool.get(model)
- if not Model:
+ if Model is None:
self.raise_view_error(cr, user, _('Model not found: %(model)s') % dict(model=model),
view_id, context)
:return: True if field should be included in the result of fields_view_get
"""
- if 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):
+ if node.tag == 'field' and node.get('name') in Model._fields:
+ field = Model._fields[node.get('name')]
+ if field.groups and not self.user_has_groups(
+ cr, user, groups=field.groups, context=context):
node.getparent().remove(node)
fields.pop(node.get('name'), None)
# no point processing view-level ``groups`` anymore, return
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:
+ field = Model._fields.get(node.get('name'))
+ if field:
children = False
views = {}
for f in node:
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)
+ xarch, xfields = self.postprocess_and_fields(cr, user, field.comodel_name, f, view_id, ctx)
views[str(f.tag)] = {
'arch': xarch,
'fields': xfields
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())
+ term = node.text.strip()
+ trans = Translations._get_source(cr, user, model, 'view', context['lang'], term)
if trans:
- node.text = node.text.replace(node.text.strip(), trans)
+ node.text = node.text.replace(term, trans)
if node.tail and node.tail.strip():
- trans = Translations._get_source(cr, user, model, 'view', context['lang'], node.tail.strip())
+ term = node.tail.strip()
+ trans = Translations._get_source(cr, user, model, 'view', context['lang'], term)
if trans:
- node.tail = node.tail.replace(node.tail.strip(), trans)
+ node.tail = node.tail.replace(term, 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 node.get('string') and node.get('string').strip() and not result:
+ term = node.get('string').strip()
+ trans = Translations._get_source(cr, user, model, 'view', context['lang'], term)
+ if trans == term 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'))
+ trans = Translations._get_source(cr, user, context['base_model_name'], 'view', context['lang'], term)
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 attr_value and attr_value.strip():
+ trans = Translations._get_source(cr, user, model, 'view', context['lang'], attr_value.strip())
if trans:
node.set(attr_name, trans)
orm.transfer_modifiers_to_node(modifiers, node)
return fields
+ def add_on_change(self, cr, user, model_name, arch):
+ """ Add attribute on_change="1" on fields that are dependencies of
+ computed fields on the same view.
+ """
+ # map each field object to its corresponding nodes in arch
+ field_nodes = collections.defaultdict(list)
+
+ def collect(node, model):
+ if node.tag == 'field':
+ field = model._fields.get(node.get('name'))
+ if field:
+ field_nodes[field].append(node)
+ if field.relational:
+ model = self.pool.get(field.comodel_name)
+ for child in node:
+ collect(child, model)
+
+ collect(arch, self.pool[model_name])
+
+ for field, nodes in field_nodes.iteritems():
+ # if field should trigger an onchange, add on_change="1" on the
+ # nodes referring to field
+ model = self.pool[field.model_name]
+ if model._has_onchange(field, field_nodes):
+ for node in nodes:
+ if not node.get('on_change'):
+ node.set('on_change', '1')
+
+ return arch
+
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:
"""
fields = {}
Model = self.pool.get(model)
- if not Model:
+ if Model is None:
self.raise_view_error(cr, user, _('Model not found: %(model)s') % dict(model=model), view_id, context)
if node.tag == 'diagram':
else:
fields = Model.fields_get(cr, user, None, context)
+ node = self.add_on_change(cr, user, model, node)
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'):
if not node.get(action) and not Model.check_access_rights(cr, user, operation, raise_exception=False):
node.set(action, 'false')
if node.tag in ('kanban'):
- group_by_field = node.get('default_group_by')
- if group_by_field and Model._all_columns.get(group_by_field):
- group_by_column = Model._all_columns[group_by_field].column
- if group_by_column._type == 'many2one':
- group_by_model = Model.pool.get(group_by_column._obj)
+ group_by_name = node.get('default_group_by')
+ if group_by_name in Model._fields:
+ group_by_field = Model._fields[group_by_name]
+ if group_by_field.type == 'many2one':
+ group_by_model = Model.pool[group_by_field.comodel_name]
for action, operation in (('group_create', 'create'), ('group_delete', 'unlink'), ('group_edit', 'write')):
if not node.get(action) and not group_by_model.check_access_rights(cr, user, operation, raise_exception=False):
node.set(action, 'false')
#------------------------------------------------------
# QWeb template views
#------------------------------------------------------
- @tools.ormcache_context(accepted_keys=('lang','inherit_branding', 'editable', 'translatable'))
- def read_template(self, cr, uid, xml_id, context=None):
- if isinstance(xml_id, (int, long)):
- view_id = xml_id
- else:
- if '.' not in xml_id:
- raise ValueError('Invalid template id: %r' % (xml_id,))
- view_id = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, xml_id, raise_if_not_found=True)
-
+ _read_template_cache = dict(accepted_keys=('lang','inherit_branding', 'editable', 'translatable'))
+ if config['dev_mode']:
+ _read_template_cache['size'] = 0
+ @tools.ormcache_context(**_read_template_cache)
+ def _read_template(self, cr, uid, view_id, context=None):
arch = self.read_combined(cr, uid, view_id, fields=['arch'], context=context)['arch']
arch_tree = etree.fromstring(arch)
arch = etree.tostring(root, encoding='utf-8', xml_declaration=True)
return arch
+ def read_template(self, cr, uid, xml_id, context=None):
+ if isinstance(xml_id, (int, long)):
+ view_id = xml_id
+ else:
+ if '.' not in xml_id:
+ raise ValueError('Invalid template id: %r' % (xml_id,))
+ view_id = self.get_view_id(cr, uid, xml_id, context=context)
+ return self._read_template(cr, uid, view_id, context=context)
+
+ @tools.ormcache(skiparg=3)
+ def get_view_id(self, cr, uid, xml_id, context=None):
+ return self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, xml_id, raise_if_not_found=True)
+
def clear_cache(self):
- self.read_template.clear_cache(self)
+ self._read_template.clear_cache(self)
def _contains_branded(self, node):
return node.tag == 't'\
e.set('data-oe-xpath', node_path)
if not e.get('data-oe-model'): return
- if set(('t-esc', 't-escf', 't-raw', 't-rawf')).intersection(e.attrib):
+ if {'t-esc', 't-raw'}.intersection(e.attrib):
# nodes which fully generate their content and have no reason to
# be branded because they can not sensibly be edited
self._pop_view_branding(e)
:rtype: boolean
"""
return any(
- (attr == 'data-oe-model' or (attr != 't-field' and attr.startswith('t-')))
+ (attr in ('data-oe-model', 'group') or (attr != 't-field' and attr.startswith('t-')))
for attr in node.attrib
)
return None
return Translations._get_source(cr, uid, 'website', 'view', lang, text, id_)
- if arch.tag not in ['script']:
+ if type(arch) not in SKIPPED_ELEMENT_TYPES and arch.tag not in SKIPPED_ELEMENTS:
text = get_trans(arch.text)
if text:
arch.text = arch.text.replace(arch.text.strip(), text)
if tail:
arch.tail = arch.tail.replace(arch.tail.strip(), tail)
- for attr_name in ('title', 'alt', 'placeholder'):
+ for attr_name in ('title', 'alt', 'label', 'placeholder'):
attr = get_trans(arch.get(attr_name))
if attr:
arch.set(attr_name, attr)
xmlid = imd.search_read(cr, uid, domain, ['module', 'name'])[0]
return '%s.%s' % (xmlid['module'], xmlid['name'])
+ @api.cr_uid_ids_context
def render(self, cr, uid, id_or_xml_id, values=None, engine='ir.qweb', context=None):
if isinstance(id_or_xml_id, list):
id_or_xml_id = id_or_xml_id[0]
)
qcontext.update(values)
- # TODO: remove this as soon as the following branch is merged
- # lp:~openerp-dev/openerp-web/trunk-module-closure-style-msh
- from openerp.addons.web.controllers.main import module_boot
- qcontext['modules'] = simplejson.dumps(module_boot()) if request else None
+ # TODO: This helper can be used by any template that wants to embedd the backend.
+ # It is currently necessary because the ir.ui.view bundle inheritance does not
+ # match the module dependency graph.
+ def get_modules_order():
+ if request:
+ from openerp.addons.web.controllers.main import module_boot
+ return simplejson.dumps(module_boot())
+ return '[]'
+ qcontext['get_modules_order'] = get_modules_order
def loader(name):
return self.read_template(cr, uid, name, context=context)