# -*- coding: utf-8 -*- ############################################################################## # # OpenERP, Open Source Management Solution # Copyright (C) 2004-2009 Tiny SPRL (). # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # ############################################################################## import copy from functools import partial import itertools import logging import os import sys import time from lxml import etree from openerp import tools from openerp.modules import module from openerp.osv import fields, osv, orm from openerp.tools import graph, SKIPPED_ELEMENT_TYPES from openerp.tools.safe_eval import safe_eval as eval from openerp.tools.translate import _ from openerp.tools.view_validation import valid_view from openerp.tools import misc, qweb _logger = logging.getLogger(__name__) class view_custom(osv.osv): _name = 'ir.ui.view.custom' _order = 'create_date desc' # search(limit=1) should return the last customization _columns = { 'ref_id': fields.many2one('ir.ui.view', 'Original View', select=True, required=True, ondelete='cascade'), 'user_id': fields.many2one('res.users', 'User', select=True, required=True, ondelete='cascade'), 'arch': fields.text('View Architecture', required=True), } def _auto_init(self, cr, context=None): super(view_custom, self)._auto_init(cr, context) cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_ui_view_custom_user_id_ref_id\'') 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)') class view(osv.osv): _name = 'ir.ui.view' class NoViewError(Exception): pass class NoDefaultError(NoViewError): pass def _type_field(self, cr, uid, ids, name, args, context=None): result = {} for record in self.browse(cr, uid, ids, context): # Get the type from the inherited view if any. if record.inherit_id: result[record.id] = record.inherit_id.type else: result[record.id] = etree.fromstring(record.arch.encode('utf8')).tag return result def _arch_get(self, cr, uid, ids, name, arg, context=None): """ For each id being read, return arch_db or the content of arch_file """ result = {} for record in self.read(cr, uid, ids, ['arch_file', 'arch_db'], context=context): if record['arch_db']: result[record['id']] = record['arch_db'] continue view_module, path = record['arch_file'].split('/', 1) arch_path = module.get_module_resource(view_module, path) if not arch_path: raise IOError("No file '%s' in module '%s'" % (path, view_module)) with misc.file_open(arch_path) as f: result[record['id']] = f.read().decode('utf-8') return result def _arch_set(self, cr, uid, id, name, value, arg, context=None): """ Forward writing to arch_db """ self.write(cr, uid, id, {'arch_db': value}, context=context) _columns = { 'name': fields.char('View Name', required=True), 'model': fields.char('Object', size=64, select=True), 'priority': fields.integer('Sequence', required=True), 'type': fields.selection([ ('tree','Tree'), ('form','Form'), ('mdx','mdx'), ('graph', 'Graph'), ('calendar', 'Calendar'), ('diagram','Diagram'), ('gantt', 'Gantt'), ('kanban', 'Kanban'), ('search','Search'), ('qweb', 'QWeb')], string='View Type'), 'arch_file': fields.char("View path"), 'arch_db': fields.text("Arch content", oldname='arch'), 'arch': fields.function(_arch_get, fnct_inv=_arch_set, store=False, string="View Architecture", type='text', nodrop=True), 'inherit_id': fields.many2one('ir.ui.view', 'Inherited View', ondelete='cascade', select=True), 'field_parent': fields.char('Child Field',size=64), 'xml_id': fields.function(osv.osv.get_xml_id, type='char', size=128, string="External ID", help="ID of the view defined in xml file"), 'groups_id': fields.many2many('res.groups', 'ir_ui_view_group_rel', 'view_id', 'group_id', string='Groups', help="If this field is empty, the view applies to all users. Otherwise, the view applies to the users of those groups only."), 'model_ids': fields.one2many('ir.model.data', 'res_id', auto_join=True), } _defaults = { 'priority': 16, } _order = "priority,name" # Holds the RNG schema _relaxng_validator = None def create(self, cr, uid, values, context=None): if 'type' not in values: 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']) return super(view, self).create(cr, uid, values, context) def _relaxng(self): if not self._relaxng_validator: frng = tools.file_open(os.path.join('base','rng','view.rng')) try: relaxng_doc = etree.parse(frng) self._relaxng_validator = etree.RelaxNG(relaxng_doc) except Exception: _logger.exception('Failed to load RelaxNG XML schema for views validation') finally: frng.close() return self._relaxng_validator def _check_xml(self, cr, uid, ids, context=None): for view in self.browse(cr, uid, ids, context): # Sanity check: the view should not break anything upon rendering! try: fvg = self.read_combined(cr, uid, view.id, view.type, view.model, context=context) view_arch_utf8 = fvg['arch'] except Exception, e: _logger.exception(e) return False # RNG-based validation is not possible anymore with 7.0 forms # TODO 7.0: provide alternative assertion-based validation of view_arch_utf8 view_docs = [etree.fromstring(view_arch_utf8)] if view_docs[0].tag == 'data': # A element is a wrapper for multiple root nodes 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): for error in validator.error_log: _logger.error(tools.ustr(error)) return False if not valid_view(view_arch): return False return True def _check_model(self, cr, uid, ids, context=None): for view in self.browse(cr, uid, ids, context): if view.model and view.model not in self.pool: return False return True _constraints = [ (_check_xml, 'Invalid XML for View Architecture!', ['arch']) #(_check_model, 'The model name does not exist.', ['model']), ] def _auto_init(self, cr, context=None): super(view, self)._auto_init(cr, context) cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_ui_view_model_type_inherit_id\'') if not cr.fetchone(): cr.execute('CREATE INDEX ir_ui_view_model_type_inherit_id ON ir_ui_view (model, inherit_id)') def read_combined(self, cr, uid, view_id, view_type, model, fields=None, fallback=None, context=None): """ Utility function stringing together all method calls necessary to get a full, final view: * Gets the default view if no view_id is provided * Gets the top of the view tree if a sub-view is requested * Applies all inherited archs on the root view * Applies post-processing * Returns the view with all requested fields .. note:: ``arch`` is always added to the fields list even if not requested (similar to ``id``) If no view is available (no view_id or invalid view_id provided, or no view stored for (model, view_type)) a view record will be fetched from the ``defaults`` mapping? :param fallback: a mapping of {view_type: view_dict}, if no view can be found (read) will be used to provide a default before post-processing :type fallback: mapping """ if context is None: context = {} def clean_anotations(arch, parent_info=None): for child in arch: if child.tag == 't' or child.tag == 'field': # can not anote t and field while cleaning continue child_text = "".join([etree.tostring(x) for x in child]) if child.attrib.get('data-edit-model'): if child_text.find('data-edit-model') != -1 or child_text.find('= 0 else sys.maxint d = id_.find('.') d = d if d >= 0 else sys.maxint if d < s: # xml id IMD = self.pool['ir.model.data'] m, _, n = id_.partition('.') _, id_ = IMD.get_object_reference(cr, uid, m, n) else: # path id => read directly on disk # TODO apply inheritence try: with misc.file_open(id_) as f: return f.read().decode('utf-8') except Exception: raise ValueError('Invalid id: %r' % (id_,)) pp(id_) r = self.read_combined(cr, uid, id_, fields=['arch'], view_type=None, model=None, context=context) pp(r) return r['arch'] def render(self, cr, uid, id_or_xml_id, values, context=None): def loader(name): arch = self._get_arch(cr, uid, name, context=context) # parse arch # on the root tag of arch add the attribute t-name="" arch = u'{1}'.format(name, arch) return arch engine = qweb.QWebXml(loader) return engine.render(id_or_xml_id, values) class view_sc(osv.osv): _name = 'ir.ui.view_sc' _columns = { 'name': fields.char('Shortcut Name', size=64), # Kept for backwards compatibility only - resource name used instead (translatable) 'res_id': fields.integer('Resource Ref.', help="Reference of the target resource, whose model/table depends on the 'Resource Name' field."), 'sequence': fields.integer('Sequence'), 'user_id': fields.many2one('res.users', 'User Ref.', required=True, ondelete='cascade', select=True), 'resource': fields.char('Resource Name', size=64, required=True, select=True) } def _auto_init(self, cr, context=None): super(view_sc, self)._auto_init(cr, context) cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_ui_view_sc_user_id_resource\'') if not cr.fetchone(): cr.execute('CREATE INDEX ir_ui_view_sc_user_id_resource ON ir_ui_view_sc (user_id, resource)') def get_sc(self, cr, uid, user_id, model='ir.ui.menu', context=None): ids = self.search(cr, uid, [('user_id','=',user_id),('resource','=',model)], context=context) results = self.read(cr, uid, ids, ['res_id'], context=context) name_map = dict(self.pool[model].name_get(cr, uid, [x['res_id'] for x in results], context=context)) # Make sure to return only shortcuts pointing to exisintg menu items. filtered_results = filter(lambda result: result['res_id'] in name_map, results) for result in filtered_results: result.update(name=name_map[result['res_id']]) return filtered_results _order = 'sequence,name' _defaults = { 'resource': 'ir.ui.menu', 'user_id': lambda obj, cr, uid, context: uid, } _sql_constraints = [ ('shortcut_unique', 'unique(res_id, resource, user_id)', 'Shortcut for this menu already exists!'), ] # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: