diff --git a/addons/auth_signup/auth_signup_data.xml b/addons/auth_signup/auth_signup_data.xml index 8f057e6d490c6..cde496dd1ee58 100644 --- a/addons/auth_signup/auth_signup_data.xml +++ b/addons/auth_signup/auth_signup_data.xml @@ -28,7 +28,7 @@ A password reset was requested for the Odoo account linked to this email.

-

You may change your password by following this link.

+

You may change your password by following this link, which will remain valid during 24 hours

Note: If you do not expect this, you can safely ignore this email.

]]>
diff --git a/addons/website/models/website.py b/addons/website/models/website.py index 375af2c2fc098..b2d685f3f76b3 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -672,7 +672,7 @@ def replace_id(old_id, new_id): self.unlink(cr, uid, to_delete, context=context) for menu in data['data']: mid = menu['id'] - if isinstance(mid, str): + if isinstance(mid, basestring): new_id = self.create(cr, uid, {'name': menu['name']}, context=context) replace_id(mid, new_id) for menu in data['data']: diff --git a/openerp/addons/base/ir/ir_attachment.py b/openerp/addons/base/ir/ir_attachment.py index a4927931d08ea..b19f03df045a8 100644 --- a/openerp/addons/base/ir/ir_attachment.py +++ b/openerp/addons/base/ir/ir_attachment.py @@ -237,7 +237,8 @@ def check(self, cr, uid, ids, mode, context=None, values=None): if create_uid != uid: require_employee = True continue - res_ids.setdefault(rmod,set()).add(rid) + if rid: + res_ids.setdefault(rmod,set()).add(rid) if values: if values.get('res_model') and values.get('res_id'): res_ids.setdefault(values['res_model'],set()).add(values['res_id']) diff --git a/openerp/addons/base/ir/ir_http.py b/openerp/addons/base/ir/ir_http.py index c8bf0776d01f9..a22561007a36e 100644 --- a/openerp/addons/base/ir/ir_http.py +++ b/openerp/addons/base/ir/ir_http.py @@ -140,6 +140,9 @@ def _handle_exception(self, exception): if attach: return attach + # Don't handle exception but use werkeug debugger if server in --dev mode + if openerp.tools.config['dev_mode']: + raise # If handle_exception returns something different than None, it will be used as a response try: return request._handle_exception(exception) diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index dd043a32a0c89..96d7e7322d525 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -266,6 +266,13 @@ def render_node(self, element, qwebcontext): generated_attributes = "" t_render = None template_attributes = {} + + debugger = element.get('t-debug') + if debugger is not None: + if openerp.tools.config['dev_mode']: + __import__(debugger).set_trace() # pdb, ipdb, pudb, ... + else: + _logger.warning("@t-debug in template '%s' is only available in --dev mode" % qwebcontext['__template__']) for (attribute_name, attribute_value) in element.attrib.iteritems(): attribute_name = unicode(attribute_name) if attribute_name == "groups": diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 27294f3de21cf..453e82270d2e2 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -37,13 +37,16 @@ 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, SKIPPED_ELEMENTS +from openerp.tools import config 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 from openerp.tools.translate import _ +from openerp.tools.convert import _fix_multiple_roots _logger = logging.getLogger(__name__) @@ -105,6 +108,32 @@ 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) + _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 @@ -118,6 +147,56 @@ def _get_model_data(self, cr, uid, ids, fname, args, context=None): result.update(map(itemgetter('res_id', 'id'), data_ids)) return result + def _views_from_model_data(self, cr, uid, ids, context=None): + IMD = self.pool['ir.model.data'] + 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): + """backported from v9""" + 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): + """backported from v9""" + 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.split('.')[-1] == imd['xml_id'].split('.')[-1] + or view.xml_id == imd['xml_id'])): + # we store the relative path to the resource instead of the absolute path, if found + # (it will be missing e.g. when importing data-only modules using base_import_module) + path_info = get_resource_from_path(imd['xml_file']) + if path_info: + data['arch_fs'] = '/'.join(path_info[0:2]) + self.write(cr, uid, ids, data, context=context) + + return True + + @api.multi + def _arch_base_get(self, name, arg): + """ Return the field 'arch' without translation. """ + return self.with_context(lang=None)._arch_get(name, arg) + + @api.multi + def _arch_base_set(self, name, value, arg): + """ Assign the field 'arch' without translation. """ + return self.with_context(lang=None)._arch_set(name, value, arg) + _columns = { 'name': fields.char('View Name', required=True), 'model': fields.char('Object', select=True), @@ -132,7 +211,10 @@ def _get_model_data(self, cr, uid, ids, fname, args, context=None): ('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_base': fields.function(_arch_base_get, fnct_inv=_arch_base_set, string='View Architecture', type="text"), + 'arch_db': fields.text('Arch Blob', oldname='arch'), + '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'), @@ -223,7 +305,7 @@ def _check_xml(self, cr, uid, ids, context=None): " extend an other view"), ] _constraints = [ - (_check_xml, 'Invalid view definition', ['arch']), + (_check_xml, 'Invalid view definition', ['arch', 'arch_base']), ] def _auto_init(self, cr, context=None): @@ -260,6 +342,11 @@ def write(self, cr, uid, ids, vals, context=None): 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)]) @@ -840,15 +927,11 @@ def postprocess_and_fields(self, cr, user, model, node, view_id, context=None): #------------------------------------------------------ # 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) + # apply ormcache_context decorator unless in dev mode... + @tools.conditional(not config['dev_mode'], + tools.ormcache_context(accepted_keys=('lang','inherit_branding', 'editable', 'translatable'))) + 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) @@ -861,8 +944,22 @@ def read_template(self, cr, uid, xml_id, context=None): 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) + + 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) + if not config['dev_mode']: + self._read_template.clear_cache(self) def _contains_branded(self, node): return node.tag == 't'\ diff --git a/openerp/modules/__init__.py b/openerp/modules/__init__.py index 55eeefaaa1964..79ed266150a8c 100644 --- a/openerp/modules/__init__.py +++ b/openerp/modules/__init__.py @@ -30,6 +30,7 @@ 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: diff --git a/openerp/modules/loading.py b/openerp/modules/loading.py index 349479bd68448..2caa16c6ebf7b 100644 --- a/openerp/modules/loading.py +++ b/openerp/modules/loading.py @@ -30,6 +30,7 @@ import sys import threading import time +import md5 import openerp import openerp.modules.db @@ -113,6 +114,19 @@ def _load_data(cr, module_name, idref, mode, kind): for filename in _get_files_of_kind(kind): _logger.info("loading %s/%s", module_name, filename) noupdate = False + if tools.config.options.get('noupdate_if_unchanged'): + pathname = os.path.join(module_name, filename) + cr.execute('select value from ir_values where name=%s and key=%s', (pathname, 'digest')) + olddigest = (cr.fetchone() or (None,))[0] + if olddigest is None: + cr.execute('insert into ir_values (name, model, key, value) values (%s, %s, %s, NULL)', + (pathname, 'ir_module_module', 'digest',)) + with tools.file_open(pathname) as fp: + digest = md5.md5(fp.read()).hexdigest() + if digest == olddigest: + noupdate = True + else: + cr.execute('update ir_values set value=%s where name=%s and key=%s', (digest, pathname, 'digest')) if kind in ('demo', 'demo_xml') or (filename.endswith('.csv') and kind in ('init', 'init_xml')): noupdate = True tools.convert_file(cr, module_name, filename, idref, mode, noupdate, kind, report) diff --git a/openerp/modules/module.py b/openerp/modules/module.py index b9f19293fee5d..6ef174a5b0389 100644 --- a/openerp/modules/module.py +++ b/openerp/modules/module.py @@ -158,7 +158,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 @@ -167,7 +167,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) @@ -179,6 +178,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): diff --git a/openerp/tools/config.py b/openerp/tools/config.py index 647b8494d5215..1500cf17cdf66 100644 --- a/openerp/tools/config.py +++ b/openerp/tools/config.py @@ -284,6 +284,7 @@ def __init__(self, fname=None): 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") @@ -419,6 +420,7 @@ def die(cond, msg): 'xmlrpcs_interface', 'xmlrpcs_port', 'xmlrpcs', 'secure_cert_file', 'secure_pkey_file', 'dbfilter', 'log_level', 'log_db', 'log_db_level', 'geoip_database', + 'dev_mode', ] for arg in keys: diff --git a/openerp/tools/convert.py b/openerp/tools/convert.py index 5e4eb0b71eda6..de09001bf3a87 100644 --- a/openerp/tools/convert.py +++ b/openerp/tools/convert.py @@ -692,9 +692,17 @@ def _tag_record(self, cr, rec, data_node=None, mode=None): 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 @@ -857,7 +865,7 @@ def parse(self, de, mode=None): 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 @@ -869,6 +877,7 @@ def __init__(self, cr, module, idref, mode, report=None, noupdate=False): report = assertion_report.assertion_report() self.assertion_report = report self.noupdate = noupdate + self.xml_filename = xml_filename self._tags = { 'record': self._tag_record, 'delete': self._tag_delete, @@ -983,7 +992,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(), mode=mode) return True diff --git a/openerp/tools/func.py b/openerp/tools/func.py index 0a36166acee60..74d239162656d 100644 --- a/openerp/tools/func.py +++ b/openerp/tools/func.py @@ -20,7 +20,7 @@ # ############################################################################## -__all__ = ['synchronized', 'lazy_property', 'classproperty'] +__all__ = ['synchronized', 'lazy_property', 'classproperty', 'conditional'] from functools import wraps from inspect import getsourcefile @@ -56,6 +56,21 @@ def reset_all(obj): 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): @wraps(func)