Merge pull request #1625 from odoo-dev/master-views-arch-on-disk-fme

[MERGE] Read ir.ui.view's arch from disk in development mode

A new switch named `--dev` has been added to the openerp.cli's arguments.

This development mode's purpose is to activate some specific behaviours in order to ease development.

For the moment, the only effect of `--dev` is to make the ir.ui.view's arch function field read the data from the original xml file (if any), allowing the developer to change a view and check the effect without having to update it's database.

This feature will currently work only with existing views. If you add a view you still have to update your database.
This commit is contained in:
Fabien Meghazi
2014-08-06 15:24:04 +02:00
7 changed files with 124 additions and 13 deletions
+60 -3
View File
@@ -37,8 +37,10 @@ from lxml import etree
import openerp
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
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
@@ -105,6 +107,30 @@ def _hasclass(context, *cls):
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)
raise ValueError("Could not find view arch definition in file '%s' for xmlid '%s'" % (filename, xmlid))
xpath_utils = etree.FunctionNamespace(None)
xpath_utils['hasclass'] = _hasclass
@@ -123,6 +149,33 @@ class view(osv.osv):
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),
@@ -137,7 +190,9 @@ class view(osv.osv):
('kanban', 'Kanban'),
('search','Search'),
('qweb', 'QWeb')], string='View Type'),
'arch': fields.text('View Architecture', required=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='cascade', select=True),
'inherit_children_ids': fields.one2many('ir.ui.view','inherit_id', 'Inherit Views'),
'field_parent': fields.char('Child Field'),
@@ -850,7 +905,9 @@ class view(osv.osv):
#------------------------------------------------------
# QWeb template views
#------------------------------------------------------
@tools.ormcache_context(accepted_keys=('lang','inherit_branding', 'editable', 'translatable'))
@tools.conditional(
not config['dev_mode'],
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
+2 -2
View File
@@ -663,7 +663,7 @@ class test_views(ViewCase):
name='base view',
model=model,
priority=1,
arch="""<?xml version="1.0"?>
arch_db="""<?xml version="1.0"?>
<tree string="view">
<field name="url"/>
</tree>
@@ -677,7 +677,7 @@ class test_views(ViewCase):
model=model,
priority=1,
inherit_id=vid,
arch="""<?xml version="1.0"?>
arch_db="""<?xml version="1.0"?>
<xpath expr="//field[@name='url']" position="before">
<field name="name"/>
</xpath>
+2 -1
View File
@@ -30,6 +30,7 @@ from openerp.modules.loading import load_modules
from openerp.modules.module import get_modules, get_modules_with_version, \
load_information_from_description_file, get_module_resource, get_module_path, \
initialize_sys_path, load_openerp_module, init_module_models, adapt_version
initialize_sys_path, load_openerp_module, init_module_models, adapt_version, \
get_resource_path, get_resource_from_path
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+28 -2
View File
@@ -154,7 +154,7 @@ def get_module_filetree(module, dir='.'):
return tree
def get_module_resource(module, *args):
def get_resource_path(module, *args):
"""Return the full path of a resource of the given module.
:param module: module name
@@ -163,7 +163,6 @@ def get_module_resource(module, *args):
:rtype: str
:return: absolute path to the resource
TODO name it get_resource_path
TODO make it available inside on osv object (self.get_resource_path)
"""
mod_path = get_module_path(module)
@@ -175,6 +174,33 @@ def get_module_resource(module, *args):
return resource_path
return False
# backwards compatibility
get_module_resource = get_resource_path
def get_resource_from_path(path):
"""Tries to extract the module name and the resource's relative path
out of an absolute resource path.
If operation is successfull, returns a tuple containing the module name, the relative path
to the resource using '/' as filesystem seperator[1] and the same relative path using
os.path.sep seperators.
[1] same convention as the resource path declaration in manifests
:param path: absolute resource path
:rtype: tuple
:return: tuple(module_name, relative_path, os_relative_path) if possible, else None
"""
resource = [path.replace(adpath, '') for adpath in ad_paths if path.startswith(adpath)]
if resource:
relative = resource[0].split(os.path.sep)
if not relative[0]:
relative.pop(0)
module = relative.pop(0)
return (module, '/'.join(relative), os.path.sep.join(relative))
return None
def get_module_icon(module):
iconpath = ['static', 'description', 'icon.png']
if get_module_resource(module, *iconpath):
+2 -1
View File
@@ -257,6 +257,7 @@ class configmanager(object):
group = optparse.OptionGroup(parser, "Advanced options")
if os.name == 'posix':
group.add_option('--auto-reload', dest='auto_reload', action='store_true', my_default=False, help='enable auto reload')
group.add_option('--dev', dest='dev_mode', action='store_true', my_default=False, help='enable developper mode')
group.add_option('--debug', dest='debug_mode', action='store_true', my_default=False, help='enable debug mode')
group.add_option("--stop-after-init", action="store_true", dest="stop_after_init", my_default=False,
help="stop the server after its initialization")
@@ -407,7 +408,7 @@ class configmanager(object):
# if defined but None take the configfile value
keys = [
'language', 'translate_out', 'translate_in', 'overwrite_existing_translations',
'debug_mode', 'smtp_ssl', 'load_language',
'debug_mode', 'dev_mode', 'smtp_ssl', 'load_language',
'stop_after_init', 'logrotate', 'without_demo', 'xmlrpc', 'syslog',
'list_db', 'xmlrpcs', 'proxy_mode',
'test_file', 'test_enable', 'test_commit', 'test_report_directory',
+16 -3
View File
@@ -774,9 +774,17 @@ form: module.record_id""" % (xml_id,)
rec_model = rec.get("model").encode('ascii')
model = self.pool[rec_model]
rec_id = rec.get("id",'').encode('ascii')
rec_context = rec.get("context", None)
rec_context = rec.get("context", {})
if rec_context:
rec_context = unsafe_eval(rec_context)
if self.xml_filename and rec_id:
rec_context['install_mode_data'] = dict(
xml_file=self.xml_filename,
xml_id=rec_id,
model=rec_model,
)
self._test_xml_id(rec_id)
# in update mode, the record won't be updated if the data node explicitely
# opt-out using @noupdate="1". A second check will be performed in
@@ -941,7 +949,7 @@ form: module.record_id""" % (xml_id,)
raise ParseError, (misc.ustr(e), etree.tostring(rec).rstrip(), rec.getroottree().docinfo.URL, rec.sourceline), exc_info[2]
return True
def __init__(self, cr, module, idref, mode, report=None, noupdate=False):
def __init__(self, cr, module, idref, mode, report=None, noupdate=False, xml_filename=None):
self.mode = mode
self.module = module
@@ -953,6 +961,7 @@ form: module.record_id""" % (xml_id,)
report = assertion_report.assertion_report()
self.assertion_report = report
self.noupdate = noupdate
self.xml_filename = xml_filename
self._tags = {
'menuitem': self._tag_menuitem,
'record': self._tag_record,
@@ -1066,7 +1075,11 @@ def convert_xml_import(cr, module, xmlfile, idref=None, mode='init', noupdate=Fa
if idref is None:
idref={}
obj = xml_import(cr, module, idref, mode, report=report, noupdate=noupdate)
if isinstance(xmlfile, file):
xml_filename = xmlfile.name
else:
xml_filename = xmlfile
obj = xml_import(cr, module, idref, mode, report=report, noupdate=noupdate, xml_filename=xml_filename)
obj.parse(doc.getroot())
return True
+14 -1
View File
@@ -20,7 +20,7 @@
#
##############################################################################
__all__ = ['synchronized', 'lazy_property']
__all__ = ['synchronized', 'lazy_property', 'conditional']
from functools import wraps
from inspect import getsourcefile
@@ -51,6 +51,19 @@ class lazy_property(object):
if isinstance(getattr(cls, name, None), lazy_property):
obj_dict.pop(name)
def conditional(condition, decorator):
""" Decorator for a conditionally applied decorator.
Example:
@conditional(get_config('use_cache'), ormcache)
def fn():
pass
"""
if condition:
return decorator
else:
return lambda fn: fn
def synchronized(lock_attr='_lock'):
def decorator(func):