[MERGE] merged the branch with the RedirectWarnings

bzr revid: qdp-launchpad@openerp.com-20130215151416-p27n59r9wri2gp9f
This commit is contained in:
Quentin (OpenERP)
2013-02-15 16:14:16 +01:00
13 changed files with 214 additions and 35 deletions
+4
View File
@@ -12,3 +12,7 @@ Changelog
- Removed support for `<terp>` 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()
+2 -2
View File
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
##############################################################################
#
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
#
@@ -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 <http://www.gnu.org/licenses/>.
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
+3 -1
View File
@@ -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
+77 -4
View File
@@ -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:
+2
View File
@@ -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,
]
@@ -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])
+13 -13
View File
@@ -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)
+11
View File
@@ -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):
+11 -11
View File
@@ -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
-1
View File
@@ -19,7 +19,6 @@
#
##############################################################################
from openerp.osv.orm import except_orm, Model, TransientModel, AbstractModel
# Deprecated, kept for backward compatibility.
+2 -2
View File
@@ -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 []
@@ -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:
@@ -21,6 +21,10 @@
<button name="generate_warning" string="Warning" type="object" icon="gtk-ok" colspan="1"/>
<label string="Warning-description"/>
</group>
<group colspan="8" col="8">
<button name="generate_redirect_warning" string="RedirectWarning" type="object" icon="gtk-ok" colspan="1"/>
<label string="Warning-description-redirection button"/>
</group>
<group colspan="8" col="8">
<button name="generate_access_denied" string="AccessDenied" type="object" icon="gtk-ok" colspan="1"/>
<label string="Access denied-traceback"/>