diff --git a/doc/changelog.rst b/doc/changelog.rst index d340b281225..84a3cdcb12e 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -12,3 +12,7 @@ Changelog - Removed support for `` root element in XML files. - Removed support for the non-openerp namespace (e.g. importing `tools` instead of `openerp.tools` in an addons). +- Add a new type of exception that allows redirections: + openerp.exceptions.RedirectWarning. +- Give a pair of new methods to res.config.settings and a helper to make them + easier to use: get_config_warning() diff --git a/openerp/__init__.py b/openerp/__init__.py index e4c08863af9..b9f4f71d76a 100644 --- a/openerp/__init__.py +++ b/openerp/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- ############################################################################## -# +# # OpenERP, Open Source Management Solution # Copyright (C) 2004-2009 Tiny SPRL (). # @@ -15,7 +15,7 @@ # 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 . +# along with this program. If not, see . # ############################################################################## diff --git a/openerp/addons/base/ir/ir_ui_menu.py b/openerp/addons/base/ir/ir_ui_menu.py index 5da85c43577..95379098cf4 100644 --- a/openerp/addons/base/ir/ir_ui_menu.py +++ b/openerp/addons/base/ir/ir_ui_menu.py @@ -30,6 +30,8 @@ from openerp.osv import fields, osv from openerp.tools.translate import _ from openerp import SUPERUSER_ID +MENU_ITEM_SEPARATOR = "/" + def one_in(setA, setB): """Check the presence of an element of setA in setB """ @@ -154,7 +156,7 @@ class ir_ui_menu(osv.osv): if level<=0: return '...' if elmt.parent_id: - parent_path = self._get_one_full_name(elmt.parent_id, level-1) + "/" + parent_path = self._get_one_full_name(elmt.parent_id, level-1) + MENU_ITEM_SEPARATOR else: parent_path = '' return parent_path + elmt.name diff --git a/openerp/addons/base/res/res_config.py b/openerp/addons/base/res/res_config.py index a2368a9f508..354a612d075 100644 --- a/openerp/addons/base/res/res_config.py +++ b/openerp/addons/base/res/res_config.py @@ -20,12 +20,14 @@ ############################################################################## import logging from operator import attrgetter +import re import openerp -from openerp import pooler +from openerp import pooler, SUPERUSER_ID from openerp.osv import osv, fields from openerp.tools import ustr from openerp.tools.translate import _ +from openerp import exceptions _logger = logging.getLogger(__name__) @@ -572,17 +574,17 @@ class res_config_settings(osv.osv_memory): if action_ids: return act_window.read(cr, uid, action_ids[0], [], context=context) return {} - + def name_get(self, cr, uid, ids, context=None): """ Override name_get method to return an appropriate configuration wizard name, and not the generated name.""" - + if not ids: return [] # name_get may receive int id instead of an id list if isinstance(ids, (int, long)): ids = [ids] - + act_window = self.pool.get('ir.actions.act_window') action_ids = act_window.search(cr, uid, [('res_model', '=', self._name)], context=context) name = self._name @@ -590,4 +592,75 @@ class res_config_settings(osv.osv_memory): name = act_window.read(cr, uid, action_ids[0], ['name'], context=context)['name'] return [(record.id, name) for record in self.browse(cr, uid , ids, context=context)] + def get_option_path(self, cr, uid, menu_xml_id, context=None): + """ + Fetch the path to a specified configuration view and the action id to access it. + + :param string menu_xml_id: the xml id of the menuitem where the view is located, + structured as follows: module_name.menuitem_xml_id (e.g.: "base.menu_sale_config") + :return tuple: + - t[0]: string: full path to the menuitem (e.g.: "Settings/Configuration/Sales") + - t[1]: long: id of the menuitem's action + """ + module_name, menu_xml_id = menu_xml_id.split('.') + dummy, menu_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, module_name, menu_xml_id) + ir_ui_menu = self.pool.get('ir.ui.menu').browse(cr, uid, menu_id, context=context) + + return (ir_ui_menu.complete_name, ir_ui_menu.action.id) + + def get_option_name(self, cr, uid, full_field_name, context=None): + """ + Fetch the human readable name of a specified configuration option. + + :param string full_field_name: the full name of the field, structured as follows: + model_name.field_name (e.g.: "sale.config.settings.fetchmail_lead") + :return string: human readable name of the field (e.g.: "Create leads from incoming mails") + """ + model_name, field_name = full_field_name.rsplit('.', 1) + + return self.pool.get(model_name).fields_get(cr, uid, allfields=[field_name], context=context)[field_name]['string'] + + def get_config_warning(self, cr, msg, context=None): + """ + Helper: return a Warning exception with the given message where the %(field:xxx)s + and/or %(menu:yyy)s are replaced by the human readable field's name and/or menuitem's + full path. + + Usage: + ------ + Just include in your error message %(field:model_name.field_name)s to obtain the human + readable field's name, and/or %(menu:module_name.menuitem_xml_id)s to obtain the menuitem's + full path. + + Example of use: + --------------- + from openerp.addons.base.res.res_config import get_warning_config + raise get_warning_config(cr, _("Error: this action is prohibited. You should check the field %(field:sale.config.settings.fetchmail_lead)s in %(menu:base.menu_sale_config)s."), context=context) + + This will return an exception containing the following message: + Error: this action is prohibited. You should check the field Create leads from incoming mails in Settings/Configuration/Sales. + """ + + res_config_obj = pooler.get_pool(cr.dbname).get('res.config.settings') + regex_path = r'%\(((?:menu|field):[a-z_\.]*)\)s' + + # Process the message + # 1/ find the menu and/or field references, put them in a list + references = re.findall(regex_path, msg, flags=re.I) + + # 2/ fetch the menu and/or field replacement values (full path and + # human readable field's name) and the action_id if any + values = {} + action_id = None + for item in references: + ref_type, ref = item.split(':') + if ref_type == 'menu': + values[item], action_id = res_config_obj.get_option_path(cr, SUPERUSER_ID, ref, context=context) + elif ref_type == 'field': + values[item] = res_config_obj.get_option_name(cr, SUPERUSER_ID, ref, context=context) + + # 3/ substitute and return the result + if (action_id): + return exceptions.RedirectWarning(msg % values, action_id, _('Go to the configuration panel')) + return exceptions.Warning(msg % values) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/tests/__init__.py b/openerp/addons/base/tests/__init__.py index 5023494172c..e54fc892282 100644 --- a/openerp/addons/base/tests/__init__.py +++ b/openerp/addons/base/tests/__init__.py @@ -3,6 +3,7 @@ import test_expression import test_ir_attachment import test_ir_values import test_menu +import test_res_config import test_res_lang import test_search @@ -12,6 +13,7 @@ checks = [ test_ir_attachment, test_ir_values, test_menu, + test_res_config, test_res_lang, test_search, ] diff --git a/openerp/addons/base/tests/test_res_config.py b/openerp/addons/base/tests/test_res_config.py new file mode 100644 index 00000000000..0508aab67ee --- /dev/null +++ b/openerp/addons/base/tests/test_res_config.py @@ -0,0 +1,80 @@ +import unittest2 + +import openerp +import openerp.tests.common as common + +class test_res_config(common.TransactionCase): + + def setUp(self): + super(test_res_config, self).setUp() + self.res_config = self.registry('res.config.settings') + + # Define the test values + self.menu_xml_id = 'base.menu_action_res_users' + self.full_field_name = 'res.partner.lang' + self.error_msg = "WarningRedirect test string: %(field:res.partner.lang)s - %(menu:base.menu_action_res_users)s." + self.error_msg_wo_menu = "WarningRedirect test string: %(field:res.partner.lang)s." + # Note: see the get_config_warning() doc for a better example + + # Fetch the expected values + module_name, menu_xml_id = self.menu_xml_id.split('.') + dummy, menu_id = self.registry('ir.model.data').get_object_reference(self.cr, self.uid, module_name, menu_xml_id) + ir_ui_menu = self.registry('ir.ui.menu').browse(self.cr, self.uid, menu_id, context=None) + + model_name, field_name = self.full_field_name.rsplit('.', 1) + + self.expected_path = ir_ui_menu.complete_name + self.expected_action_id = ir_ui_menu.action.id + self.expected_name = self.registry(model_name).fields_get(self.cr, self.uid, allfields=[field_name], context=None)[field_name]['string'] + self.expected_final_error_msg = self.error_msg % { + 'field:res.partner.lang': self.expected_name, + 'menu:base.menu_action_res_users': self.expected_path + } + self.expected_final_error_msg_wo_menu = self.error_msg_wo_menu % { + 'field:res.partner.lang': self.expected_name, + } + + def test_00_get_option_path(self): + """ The get_option_path() method should return a tuple containing a string and an integer """ + res = self.res_config.get_option_path(self.cr, self.uid, self.menu_xml_id, context=None) + + # Check types + self.assertTrue(isinstance(res, tuple)), "The result of get_option_path() should be a tuple (got %s)" % type(res) + self.assertTrue(len(res) == 2), "The tuple should contain 2 elements (got %s)" % len(res) + self.assertTrue(isinstance(res[0], basestring)), "The first element of the tuple should be a string (got %s)" % type(res[0]) + self.assertTrue(isinstance(res[1], long)), "The second element of the tuple should be an long (got %s)" % type(res[1]) + + # Check returned values + self.assertTrue(res[0] == self.expected_path), "Result mismatch: expected %s, got %s" % (self.expected_path, res[0]) + self.assertTrue(res[1] == self.expected_action_id), "Result mismatch: expected %s, got %s" % (self.expected_action_id, res[1]) + + def test_10_get_option_name(self): + """ The get_option_name() method should return a string """ + res = self.res_config.get_option_name(self.cr, self.uid, self.full_field_name, context=None) + + # Check type + self.assertTrue(isinstance(res, basestring)), "Result type mismatch: expected basestring, got %s" % type(res) + + # Check returned value + self.assertTrue(res == self.expected_name), "Result mismatch: expected %s, got %s" % (self.expected_name, res) + + def test_20_get_config_warning(self): + """ The get_config_warning() method should return a RedirectWarning """ + res = self.res_config.get_config_warning(self.cr, self.error_msg, context=None) + + # Check type + self.assertTrue(isinstance(res, openerp.exceptions.RedirectWarning)), "Result type mismatch: expected openerp.exceptions.RedirectWarning, got %s" % type(res) + + # Check returned value + self.assertTrue(res.args[0], self.expected_final_error_msg), "Result mismatch: expected %s, got %s" % (self.expected_final_error_msg, res.args[0]) + self.assertTrue(res.args[1], self.expected_action_id), "Result mismatch: expected %s, got %s" % (self.expected_action_id, res.args[1]) + + def test_30_get_config_warning_wo_menu(self): + """ The get_config_warning() method should return a Warning exception """ + res = self.res_config.get_config_warning(self.cr, self.error_msg_wo_menu, context=None) + + # Check type + self.assertTrue(isinstance(res, openerp.exceptions.Warning)), "Result type mismatch: expected openerp.exceptions.Warning, got %s" % type(res) + + # Check returned value + self.assertTrue(res.args[0], self.expected_final_error_msg), "Result mismatch: expected %s, got %s" % (self.expected_final_error_msg_wo_menu, res.args[0]) diff --git a/openerp/addons/base/tests/test_res_lang.py b/openerp/addons/base/tests/test_res_lang.py index 325d1f72065..1206a1de831 100644 --- a/openerp/addons/base/tests/test_res_lang.py +++ b/openerp/addons/base/tests/test_res_lang.py @@ -7,20 +7,20 @@ class test_res_lang(common.TransactionCase): def test_00_intersperse(self): from openerp.addons.base.res.res_lang import intersperse - assert intersperse("", []) == ("", 0), "Assert passed" - assert intersperse("0", []) == ("0", 0), "Assert passed" - assert intersperse("012", []) == ("012", 0), "Assert passed" - assert intersperse("1", []) == ("1", 0), "Assert passed" - assert intersperse("12", []) == ("12", 0), "Assert passed" - assert intersperse("123", []) == ("123", 0), "Assert passed" - assert intersperse("1234", []) == ("1234", 0), "Assert passed" - assert intersperse("123456789", []) == ("123456789", 0), "Assert passed" - assert intersperse("&ab%#@1", []) == ("&ab%#@1", 0), "Assert passed" + assert intersperse("", []) == ("", 0) + assert intersperse("0", []) == ("0", 0) + assert intersperse("012", []) == ("012", 0) + assert intersperse("1", []) == ("1", 0) + assert intersperse("12", []) == ("12", 0) + assert intersperse("123", []) == ("123", 0) + assert intersperse("1234", []) == ("1234", 0) + assert intersperse("123456789", []) == ("123456789", 0) + assert intersperse("&ab%#@1", []) == ("&ab%#@1", 0) - assert intersperse("0", []) == ("0", 0), "Assert passed" - assert intersperse("0", [1]) == ("0", 0), "Assert passed" - assert intersperse("0", [2]) == ("0", 0), "Assert passed" - assert intersperse("0", [200]) == ("0", 0), "Assert passed" + assert intersperse("0", []) == ("0", 0) + assert intersperse("0", [1]) == ("0", 0) + assert intersperse("0", [2]) == ("0", 0) + assert intersperse("0", [200]) == ("0", 0) assert intersperse("12345678", [1], '.') == ('1234567.8', 1) assert intersperse("12345678", [1], '.') == ('1234567.8', 1) diff --git a/openerp/exceptions.py b/openerp/exceptions.py index fbf52c7f3a9..7789345585c 100644 --- a/openerp/exceptions.py +++ b/openerp/exceptions.py @@ -25,11 +25,22 @@ This module defines a few exception types. Those types are understood by the RPC layer. Any other exception type bubbling until the RPC layer will be treated as a 'Server error'. +If you consider introducing new exceptions, check out the test_exceptions addon. """ class Warning(Exception): pass +class RedirectWarning(Exception): + """ Warning with a possibility to redirect the user instead of simply + diplaying the warning message. + + Should receive as parameters: + :param int action_id: id of the action where to perform the redirection + :param string button_text: text to put on the button that will trigger + the redirection. + """ + class AccessDenied(Exception): """ Login/password error. No message, no traceback. """ def __init__(self): diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index ba1ea94dfb1..ebde4c17a51 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -2283,7 +2283,7 @@ class BaseModel(object): if view_type == 'tree' or not action[2].get('multi')] resprint = [clean(print_) for print_ in resprint if view_type == 'tree' or not print_[2].get('multi')] - #When multi="True" set it will display only in More of the list view + #When multi="True" set it will display only in More of the list view resrelate = [clean(action) for action in resrelate if (action[2].get('multi') and view_type == 'tree') or (not action[2].get('multi') and view_type == 'form')] @@ -2678,17 +2678,17 @@ class BaseModel(object): order = orderby or groupby data_ids = self.search(cr, uid, [('id', 'in', alldata.keys())], order=order, context=context) - + # the IDs of records that have groupby field value = False or '' should be included too data_ids += set(alldata.keys()).difference(data_ids) - - if groupby: + + if groupby: data = self.read(cr, uid, data_ids, [groupby], context=context) # restore order of the search as read() uses the default _order (this is only for groups, so the footprint of data should be small): data_dict = dict((d['id'], d[groupby] ) for d in data) result = [{'id': i, groupby: data_dict[i]} for i in data_ids] else: - result = [{'id': i} for i in data_ids] + result = [{'id': i} for i in data_ids] for d in result: if groupby: @@ -2752,7 +2752,7 @@ class BaseModel(object): return _logger.info('Computing parent left and right for table %s...', self._table) def browse_rec(root, pos=0): -# TODO: set order + # TODO: set order where = self._parent_name+'='+str(root) if not root: where = self._parent_name+' IS NULL' @@ -3863,7 +3863,7 @@ class BaseModel(object): if operation in ('read','unlink'): # No need to warn about deleting an already deleted record. # And no error when reading a record that was deleted, to prevent spurious - # errors for non-transactional search/read sequences coming from clients + # errors for non-transactional search/read sequences coming from clients return _logger.warning('Failed operation on deleted record(s): %s, uid: %s, model: %s', operation, uid, self._name) raise except_orm(_('Missing document(s)'), @@ -4375,12 +4375,12 @@ class BaseModel(object): del vals[self._inherits[table]] record_id = tocreate[table].pop('id', None) - + # When linking/creating parent records, force context without 'no_store_function' key that - # defers stored functions computing, as these won't be computed in batch at the end of create(). + # defers stored functions computing, as these won't be computed in batch at the end of create(). parent_context = dict(context) parent_context.pop('no_store_function', None) - + if record_id is None or not record_id: record_id = self.pool.get(table).create(cr, user, tocreate[table], context=parent_context) else: @@ -4429,7 +4429,7 @@ class BaseModel(object): upd0 = upd0 + ',"' + field + '"' upd1 = upd1 + ',' + self._columns[field]._symbol_set[0] upd2.append(self._columns[field]._symbol_set[1](vals[field])) - #for the function fields that receive a value, we set them directly in the database + #for the function fields that receive a value, we set them directly in the database #(they may be required), but we also need to trigger the _fct_inv() if (hasattr(self._columns[field], '_fnct_inv')) and not isinstance(self._columns[field], fields.related): #TODO: this way to special case the related fields is really creepy but it shouldn't be changed at diff --git a/openerp/osv/osv.py b/openerp/osv/osv.py index b4a9fdd36e5..d56c0025df0 100644 --- a/openerp/osv/osv.py +++ b/openerp/osv/osv.py @@ -19,7 +19,6 @@ # ############################################################################## - from openerp.osv.orm import except_orm, Model, TransientModel, AbstractModel # Deprecated, kept for backward compatibility. diff --git a/openerp/service/wsgi_server.py b/openerp/service/wsgi_server.py index 8d9d7b4a83c..1513094394a 100644 --- a/openerp/service/wsgi_server.py +++ b/openerp/service/wsgi_server.py @@ -94,7 +94,7 @@ def xmlrpc_handle_exception(e): if isinstance(e, openerp.osv.orm.except_orm): # legacy fault = xmlrpclib.Fault(RPC_FAULT_CODE_WARNING, openerp.tools.ustr(e.value)) response = xmlrpclib.dumps(fault, allow_none=False, encoding=None) - elif isinstance(e, openerp.exceptions.Warning): + elif isinstance(e, openerp.exceptions.Warning) or isinstance(e, openerp.exceptions.RedirectWarning): fault = xmlrpclib.Fault(RPC_FAULT_CODE_WARNING, str(e)) response = xmlrpclib.dumps(fault, allow_none=False, encoding=None) elif isinstance (e, openerp.exceptions.AccessError): @@ -312,7 +312,7 @@ def http_to_wsgi(http_dir): handler.auth_provider.checkRequest(handler, path) except websrv_lib.AuthRequiredExc, ae: # Darwin 9.x.x webdav clients will report "HTTP/1.0" to us, while they support (and need) the - # authorisation features of HTTP/1.1 + # authorisation features of HTTP/1.1 if request_version != 'HTTP/1.1' and ('Darwin/9.' not in handler.headers.get('User-Agent', '')): start_response("403 Forbidden", []) return [] diff --git a/openerp/tests/addons/test_exceptions/models.py b/openerp/tests/addons/test_exceptions/models.py index 45f0e6dbd47..2735b43f509 100644 --- a/openerp/tests/addons/test_exceptions/models.py +++ b/openerp/tests/addons/test_exceptions/models.py @@ -19,6 +19,9 @@ class m(openerp.osv.osv.Model): def generate_warning(self, cr, uid, ids, context=None): raise openerp.exceptions.Warning('description') + def generate_redirect_warning(self, cr, uid, ids, context=None): + raise openerp.exceptions.RedirectWarning('description', self.pool.get('ir.model.data').get_object_reference(cr, uid, 'base', 'menu_action_res_users'), 'go to the redirection') + def generate_access_denied(self, cr, uid, ids, context=None): raise openerp.exceptions.AccessDenied() @@ -29,5 +32,6 @@ class m(openerp.osv.osv.Model): raise Exception('AccessDenied') def generate_undefined(self, cr, uid, ids, context=None): - self.surely_undefined_sumbol + self.surely_undefined_symbol + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/tests/addons/test_exceptions/view.xml b/openerp/tests/addons/test_exceptions/view.xml index 180029e739a..0c8e373a278 100644 --- a/openerp/tests/addons/test_exceptions/view.xml +++ b/openerp/tests/addons/test_exceptions/view.xml @@ -21,6 +21,10 @@