Commit ff1b29f8 authored by Sylvain Thénault's avatar Sylvain Thénault
Browse files

[validation error] refactor validation error handling so translation is done on the web side

Users should now use cubicweb.validation_error helper function that will activate the feature
with other handy behaviours. Also test testing for message in errors should call
exception.tr(unicode) before comparing.

Using bare ValidationError keep backward compat.
parent c747242d22a6
......@@ -199,3 +199,26 @@ def onevent(event, *args, **kwargs):
CW_EVENT_MANAGER.bind(event, func, *args, **kwargs)
return func
return _decorator
from yams.schema import role_name as rname
def validation_error(entity, errors, substitutions=None, i18nvalues=None):
"""easy way to retrieve a :class:`cubicweb.ValidationError` for an entity or eid.
You may also have 2-tuple as error keys, :func:`yams.role_name` will be
called automatically for them.
Messages in errors **should not be translated yet**, though marked for
internationalization. You may give an additional substition dictionary that
will be used for interpolation after the translation.
"""
if substitutions is None:
# set empty dict else translation won't be done for backward
# compatibility reason (see ValidationError.tr method)
substitutions = {}
for key in errors.keys():
if isinstance(key, tuple):
errors[rname(*key)] = errors.pop(key)
return ValidationError(getattr(entity, 'eid', entity), errors,
substitutions, i18nvalues)
# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
......@@ -19,7 +19,7 @@
__docformat__ = "restructuredtext en"
from yams import ValidationError
from yams import ValidationError as ValidationError
# abstract exceptions #########################################################
......
# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
......@@ -20,12 +20,11 @@ validity
"""
__docformat__ = "restructuredtext en"
_ = unicode
from threading import Lock
from yams.schema import role_name
from cubicweb import ValidationError
from cubicweb import validation_error
from cubicweb.schema import (META_RTYPES, WORKFLOW_RTYPES,
RQLConstraint, RQLUniqueConstraint)
from cubicweb.predicates import is_instance
......@@ -87,11 +86,11 @@ class _CheckRequiredRelationOperation(hook.DataOperationMixIn,
continue
if not session.execute(self.base_rql % rtype, {'x': eid}):
etype = session.describe(eid)[0]
_ = session._
msg = _('at least one relation %(rtype)s is required on '
'%(etype)s (%(eid)s)')
msg %= {'rtype': _(rtype), 'etype': _(etype), 'eid': eid}
raise ValidationError(eid, {role_name(rtype, self.role): msg})
raise validation_error(eid, {(rtype, self.role): msg},
{'rtype': rtype, 'etype': etype, 'eid': eid},
['rtype', 'etype'])
class _CheckSRelationOp(_CheckRequiredRelationOperation):
......@@ -231,9 +230,9 @@ class CheckUniqueHook(IntegrityHook):
rql = '%s X WHERE X %s %%(val)s' % (entity.e_schema, attr)
rset = self._cw.execute(rql, {'val': val})
if rset and rset[0][0] != entity.eid:
msg = self._cw._('the value "%s" is already used, use another one')
qname = role_name(attr, 'subject')
raise ValidationError(entity.eid, {qname: msg % val})
msg = _('the value "%s" is already used, use another one')
raise validation_error(entity, {(attr, 'subject'): msg},
(val,))
class DontRemoveOwnersGroupHook(IntegrityHook):
......@@ -246,15 +245,12 @@ class DontRemoveOwnersGroupHook(IntegrityHook):
def __call__(self):
entity = self.entity
if self.event == 'before_delete_entity' and entity.name == 'owners':
msg = self._cw._('can\'t be deleted')
raise ValidationError(entity.eid, {None: msg})
raise validation_error(entity, {None: _("can't be deleted")})
elif self.event == 'before_update_entity' \
and 'name' in entity.cw_edited:
oldname, newname = entity.cw_edited.oldnewvalue('name')
if oldname == 'owners' and newname != oldname:
qname = role_name('name', 'subject')
msg = self._cw._('can\'t be changed')
raise ValidationError(entity.eid, {qname: msg})
raise validation_error(entity, {('name', 'subject'): _("can't be changed")})
class TidyHtmlFields(IntegrityHook):
......
# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
......@@ -24,6 +24,7 @@ checking for schema consistency is done in hooks.py
"""
__docformat__ = "restructuredtext en"
_ = unicode
from copy import copy
from yams.schema import BASE_TYPES, RelationSchema, RelationDefinitionSchema
......@@ -31,7 +32,7 @@ from yams import buildobjs as ybo, schema2sql as y2sql
from logilab.common.decorators import clear_cache
from cubicweb import ValidationError
from cubicweb import validation_error
from cubicweb.predicates import is_instance
from cubicweb.schema import (SCHEMA_TYPES, META_RTYPES, VIRTUAL_RTYPES,
CONSTRAINTS, ETYPE_NAME_MAP, display_name)
......@@ -127,10 +128,9 @@ def check_valid_changes(session, entity, ro_attrs=('name', 'final')):
if attr in ro_attrs:
origval, newval = entity.cw_edited.oldnewvalue(attr)
if newval != origval:
errors[attr] = session._("can't change the %s attribute") % \
display_name(session, attr)
errors[attr] = _("can't change this attribute")
if errors:
raise ValidationError(entity.eid, errors)
raise validation_error(entity, errors)
class _MockEntity(object): # XXX use a named tuple with python 2.6
......@@ -913,7 +913,7 @@ class DelCWETypeHook(SyncSchemaHook):
# final entities can't be deleted, don't care about that
name = self.entity.name
if name in CORE_TYPES:
raise ValidationError(self.entity.eid, {None: self._cw._('can\'t be deleted')})
raise validation_error(self.entity, {None: _("can't be deleted")})
# delete every entities of this type
if name not in ETYPE_NAME_MAP:
self._cw.execute('DELETE %s X' % name)
......@@ -983,7 +983,7 @@ class DelCWRTypeHook(SyncSchemaHook):
def __call__(self):
name = self.entity.name
if name in CORE_TYPES:
raise ValidationError(self.entity.eid, {None: self._cw._('can\'t be deleted')})
raise validation_error(self.entity, {None: _("can't be deleted")})
# delete relation definitions using this relation type
self._cw.execute('DELETE CWAttribute X WHERE X relation_type Y, Y eid %(x)s',
{'x': self.entity.eid})
......
# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
......@@ -18,9 +18,9 @@
"""Core hooks: synchronize living session on persistent data changes"""
__docformat__ = "restructuredtext en"
_ = unicode
from yams.schema import role_name
from cubicweb import UnknownProperty, ValidationError, BadConnectionId
from cubicweb import UnknownProperty, BadConnectionId, validation_error
from cubicweb.predicates import is_instance
from cubicweb.server import hook
......@@ -165,13 +165,11 @@ class AddCWPropertyHook(SyncSessionHook):
try:
value = session.vreg.typed_value(key, value)
except UnknownProperty:
qname = role_name('pkey', 'subject')
msg = session._('unknown property key %s') % key
raise ValidationError(self.entity.eid, {qname: msg})
msg = _('unknown property key %s')
raise validation_error(self.entity, {('pkey', 'subject'): msg}, (key,))
except ValueError, ex:
qname = role_name('value', 'subject')
raise ValidationError(self.entity.eid,
{qname: session._(str(ex))})
raise validation_error(self.entity,
{('value', 'subject'): str(ex)})
if not session.user.matching_groups('managers'):
session.add_relation(self.entity.eid, 'for_user', session.user.eid)
else:
......@@ -196,8 +194,7 @@ class UpdateCWPropertyHook(AddCWPropertyHook):
except UnknownProperty:
return
except ValueError, ex:
qname = role_name('value', 'subject')
raise ValidationError(entity.eid, {qname: session._(str(ex))})
raise validation_error(entity, {('value', 'subject'): str(ex)})
if entity.for_user:
for session_ in get_user_sessions(session.repo, entity.for_user[0].eid):
_ChangeCWPropertyOp(session, cwpropdict=session_.user.properties,
......@@ -237,10 +234,8 @@ class AddForUserRelationHook(SyncSessionHook):
key, value = session.execute('Any K,V WHERE P eid %(x)s,P pkey K,P value V',
{'x': eidfrom})[0]
if session.vreg.property_info(key)['sitewide']:
qname = role_name('for_user', 'subject')
msg = session._("site-wide property can't be set for user")
raise ValidationError(eidfrom,
{qname: msg})
msg = _("site-wide property can't be set for user")
raise validation_error(eidfrom, {('for_user', 'subject'): msg})
for session_ in get_user_sessions(session.repo, self.eidto):
_ChangeCWPropertyOp(session, cwpropdict=session_.user.properties,
key=key, value=value)
......
# copyright 2010-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# copyright 2010-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
......@@ -17,12 +17,13 @@
# with CubicWeb. If not, see <http://www.gnu.org/licenses/>.
"""hooks for repository sources synchronization"""
_ = unicode
from socket import gethostname
from logilab.common.decorators import clear_cache
from yams.schema import role_name
from cubicweb import ValidationError
from cubicweb import validation_error
from cubicweb.predicates import is_instance
from cubicweb.server import SOURCE_TYPES, hook
......@@ -46,9 +47,8 @@ class SourceAddedHook(SourceHook):
try:
sourcecls = SOURCE_TYPES[self.entity.type]
except KeyError:
msg = self._cw._('unknown source type')
raise ValidationError(self.entity.eid,
{role_name('type', 'subject'): msg})
msg = _('unknown source type')
raise validation_error(self.entity, {('type', 'subject'): msg})
sourcecls.check_conf_dict(self.entity.eid, self.entity.host_config,
fail_if_unknown=not self._cw.vreg.config.repairing)
SourceAddedOp(self._cw, entity=self.entity)
......@@ -65,7 +65,8 @@ class SourceRemovedHook(SourceHook):
events = ('before_delete_entity',)
def __call__(self):
if self.entity.name == 'system':
raise ValidationError(self.entity.eid, {None: 'cant remove system source'})
msg = _("You cannot remove the system source")
raise validation_error(self.entity, {None: msg})
SourceRemovedOp(self._cw, uri=self.entity.name)
......@@ -154,8 +155,8 @@ class SourceMappingImmutableHook(SourceHook):
events = ('before_add_relation',)
def __call__(self):
if not self._cw.added_in_transaction(self.eidfrom):
msg = self._cw._("can't change this relation")
raise ValidationError(self.eidfrom, {self.rtype: msg})
msg = _("You can't change this relation")
raise validation_error(self.eidfrom, {self.rtype: msg})
class SourceMappingChangedOp(hook.DataOperationMixIn, hook.Operation):
......
......@@ -170,6 +170,7 @@ class SchemaHooksTC(CubicWebTC):
try:
self.execute('INSERT CWUser X: X login "admin"')
except ValidationError, ex:
ex.tr(unicode)
self.assertIsInstance(ex.entity, int)
self.assertEqual(ex.errors, {'login-subject': 'the value "admin" is already used, use another one'})
......
......@@ -31,9 +31,11 @@ class CWPropertyHooksTC(CubicWebTC):
def test_unexistant_cwproperty(self):
with self.assertRaises(ValidationError) as cm:
self.execute('INSERT CWProperty X: X pkey "bla.bla", X value "hop", X for_user U')
cm.exception.tr(unicode)
self.assertEqual(cm.exception.errors, {'pkey-subject': 'unknown property key bla.bla'})
with self.assertRaises(ValidationError) as cm:
self.execute('INSERT CWProperty X: X pkey "bla.bla", X value "hop"')
cm.exception.tr(unicode)
self.assertEqual(cm.exception.errors, {'pkey-subject': 'unknown property key bla.bla'})
def test_site_wide_cwproperty(self):
......
# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
......@@ -18,12 +18,12 @@
"""Core hooks: workflow related hooks"""
__docformat__ = "restructuredtext en"
_ = unicode
from datetime import datetime
from yams.schema import role_name
from cubicweb import RepositoryError, ValidationError
from cubicweb import RepositoryError, validation_error
from cubicweb.predicates import is_instance, adaptable
from cubicweb.server import hook
......@@ -92,9 +92,8 @@ class _WorkflowChangedOp(hook.Operation):
if mainwf.eid == self.wfeid:
deststate = mainwf.initial
if not deststate:
qname = role_name('custom_workflow', 'subject')
msg = session._('workflow has no initial state')
raise ValidationError(entity.eid, {qname: msg})
msg = _('workflow has no initial state')
raise validation_error(entity, {('custom_workflow', 'subject'): msg})
if mainwf.state_by_eid(iworkflowable.current_state.eid):
# nothing to do
return
......@@ -119,9 +118,8 @@ class _CheckTrExitPoint(hook.Operation):
outputs = set()
for ep in tr.subworkflow_exit:
if ep.subwf_state.eid in outputs:
qname = role_name('subworkflow_exit', 'subject')
msg = self.session._("can't have multiple exits on the same state")
raise ValidationError(self.treid, {qname: msg})
msg = _("can't have multiple exits on the same state")
raise validation_error(self.treid, {('subworkflow_exit', 'subject'): msg})
outputs.add(ep.subwf_state.eid)
......@@ -137,13 +135,12 @@ class _SubWorkflowExitOp(hook.Operation):
wftr = iworkflowable.subworkflow_input_transition()
if wftr is None:
# inconsistency detected
qname = role_name('to_state', 'subject')
msg = session._("state doesn't belong to entity's current workflow")
raise ValidationError(self.trinfo.eid, {'to_state': msg})
msg = _("state doesn't belong to entity's current workflow")
raise validation_error(self.trinfo, {('to_state', 'subject'): msg})
tostate = wftr.get_exit_point(forentity, trinfo.cw_attr_cache['to_state'])
if tostate is not None:
# reached an exit point
msg = session._('exiting from subworkflow %s')
msg = _('exiting from subworkflow %s')
msg %= session._(iworkflowable.current_workflow.name)
session.transaction_data[(forentity.eid, 'subwfentrytr')] = True
iworkflowable.change_state(tostate, msg, u'text/plain', tr=wftr)
......@@ -186,9 +183,8 @@ class FireTransitionHook(WorkflowHook):
try:
foreid = entity.cw_attr_cache['wf_info_for']
except KeyError:
qname = role_name('wf_info_for', 'subject')
msg = session._('mandatory relation')
raise ValidationError(entity.eid, {qname: msg})
msg = _('mandatory relation')
raise validation_error(entity, {('wf_info_for', 'subject'): msg})
forentity = session.entity_from_eid(foreid)
# see comment in the TrInfo entity definition
entity.cw_edited['tr_count']=len(forentity.reverse_wf_info_for)
......@@ -201,13 +197,13 @@ class FireTransitionHook(WorkflowHook):
else:
wf = iworkflowable.current_workflow
if wf is None:
msg = session._('related entity has no workflow set')
raise ValidationError(entity.eid, {None: msg})
msg = _('related entity has no workflow set')
raise validation_error(entity, {None: msg})
# then check it has a state set
fromstate = iworkflowable.current_state
if fromstate is None:
msg = session._('related entity has no state')
raise ValidationError(entity.eid, {None: msg})
msg = _('related entity has no state')
raise validation_error(entity, {None: msg})
# True if we are coming back from subworkflow
swtr = session.transaction_data.pop((forentity.eid, 'subwfentrytr'), None)
cowpowers = (session.user.is_in_group('managers')
......@@ -219,47 +215,42 @@ class FireTransitionHook(WorkflowHook):
# no transition set, check user is a manager and destination state
# is specified (and valid)
if not cowpowers:
qname = role_name('by_transition', 'subject')
msg = session._('mandatory relation')
raise ValidationError(entity.eid, {qname: msg})
msg = _('mandatory relation')
raise validation_error(entity, {('by_transition', 'subject'): msg})
deststateeid = entity.cw_attr_cache.get('to_state')
if not deststateeid:
qname = role_name('by_transition', 'subject')
msg = session._('mandatory relation')
raise ValidationError(entity.eid, {qname: msg})
msg = _('mandatory relation')
raise validation_error(entity, {('by_transition', 'subject'): msg})
deststate = wf.state_by_eid(deststateeid)
if deststate is None:
qname = role_name('to_state', 'subject')
msg = session._("state doesn't belong to entity's workflow")
raise ValidationError(entity.eid, {qname: msg})
msg = _("state doesn't belong to entity's workflow")
raise validation_error(entity, {('to_state', 'subject'): msg})
else:
# check transition is valid and allowed, unless we're coming back
# from subworkflow
tr = session.entity_from_eid(treid)
if swtr is None:
qname = role_name('by_transition', 'subject')
qname = ('by_transition', 'subject')
if tr is None:
msg = session._("transition doesn't belong to entity's workflow")
raise ValidationError(entity.eid, {qname: msg})
msg = _("transition doesn't belong to entity's workflow")
raise validation_error(entity, {qname: msg})
if not tr.has_input_state(fromstate):
msg = session._("transition %(tr)s isn't allowed from %(st)s") % {
'tr': session._(tr.name), 'st': session._(fromstate.name)}
raise ValidationError(entity.eid, {qname: msg})
msg = _("transition %(tr)s isn't allowed from %(st)s")
raise validation_error(entity, {qname: msg}, {
'tr': tr.name, 'st': fromstate.name}, ['tr', 'st'])
if not tr.may_be_fired(foreid):
msg = session._("transition may not be fired")
raise ValidationError(entity.eid, {qname: msg})
msg = _("transition may not be fired")
raise validation_error(entity, {qname: msg})
deststateeid = entity.cw_attr_cache.get('to_state')
if deststateeid is not None:
if not cowpowers and deststateeid != tr.destination(forentity).eid:
qname = role_name('by_transition', 'subject')
msg = session._("transition isn't allowed")
raise ValidationError(entity.eid, {qname: msg})
msg = _("transition isn't allowed")
raise validation_error(entity, {('by_transition', 'subject'): msg})
if swtr is None:
deststate = session.entity_from_eid(deststateeid)
if not cowpowers and deststate is None:
qname = role_name('to_state', 'subject')
msg = session._("state doesn't belong to entity's workflow")
raise ValidationError(entity.eid, {qname: msg})
msg = _("state doesn't belong to entity's workflow")
raise validation_error(entity, {('to_state', 'subject'): msg})
else:
deststateeid = tr.destination(forentity).eid
# everything is ok, add missing information on the trinfo entity
......@@ -307,20 +298,18 @@ class CheckInStateChangeAllowed(WorkflowHook):
iworkflowable = entity.cw_adapt_to('IWorkflowable')
mainwf = iworkflowable.main_workflow
if mainwf is None:
msg = session._('entity has no workflow set')
raise ValidationError(entity.eid, {None: msg})
msg = _('entity has no workflow set')
raise validation_error(entity, {None: msg})
for wf in mainwf.iter_workflows():
if wf.state_by_eid(self.eidto):
break
else:
qname = role_name('in_state', 'subject')
msg = session._("state doesn't belong to entity's workflow. You may "
"want to set a custom workflow for this entity first.")
raise ValidationError(self.eidfrom, {qname: msg})
msg = _("state doesn't belong to entity's workflow. You may "
"want to set a custom workflow for this entity first.")
raise validation_error(self.eidfrom, {('in_state', 'subject'): msg})
if iworkflowable.current_workflow and wf.eid != iworkflowable.current_workflow.eid:
qname = role_name('in_state', 'subject')
msg = session._("state doesn't belong to entity's current workflow")
raise ValidationError(self.eidfrom, {qname: msg})
msg = _("state doesn't belong to entity's current workflow")
raise validation_error(self.eidfrom, {('in_state', 'subject'): msg})
class SetModificationDateOnStateChange(WorkflowHook):
......
......@@ -143,8 +143,7 @@ class EditedEntity(dict):
for rtype in self]
try:
entity.e_schema.check(dict_protocol_catcher(entity),
creation=creation, _=entity._cw._,
relations=relations)
creation=creation, relations=relations)
except ValidationError, ex:
ex.entity = self.entity
raise
......
......@@ -228,6 +228,7 @@ class UndoableTransactionTC(CubicWebTC):
"%s doesn't exist anymore." % g.eid])
with self.assertRaises(ValidationError) as cm:
self.commit()
cm.exception.tr(unicode)
self.assertEqual(cm.exception.entity, self.toto.eid)
self.assertEqual(cm.exception.errors,
{'in_group-subject': u'at least one relation in_group is '
......
......@@ -511,7 +511,7 @@ class CubicWebPublisher(object):
return ''
def validation_error_handler(self, req, ex):
ex.errors = dict((k, v) for k, v in ex.errors.items())
ex.tr(req._) # translate messages using ui language
if '__errorurl' in req.form:
forminfo = {'error': ex,
'values': req.form,
......
......@@ -77,6 +77,7 @@ class EditControllerTC(CubicWebTC):
}
with self.assertRaises(ValidationError) as cm:
self.ctrl_publish(req)
cm.exception.tr(unicode)
self.assertEqual(cm.exception.errors, {'login-subject': 'the value "admin" is already used, use another one'})
def test_user_editing_itself(self):
......@@ -249,6 +250,7 @@ class EditControllerTC(CubicWebTC):
}
with self.assertRaises(ValidationError) as cm:
self.ctrl_publish(req)
cm.exception.tr(unicode)
self.assertEqual(cm.exception.errors, {'amount-subject': 'value -10 must be >= 0'})
req = self.request(rollbackfirst=True)
req.form = {'eid': ['X'],
......@@ -259,6 +261,7 @@ class EditControllerTC(CubicWebTC):
}
with self.assertRaises(ValidationError) as cm:
self.ctrl_publish(req)
cm.exception.tr(unicode)
self.assertEqual(cm.exception.errors, {'amount-subject': 'value 110 must be <= 100'})
req = self.request(rollbackfirst=True)
req.form = {'eid': ['X'],
......
......@@ -190,6 +190,7 @@ class ViewController(Controller):
def _validation_error(req, ex):
req.cnx.rollback()
ex.tr(req._) # translate messages using ui language
# XXX necessary to remove existant validation error?
# imo (syt), it's not necessary
req.session.data.pop(req.form.get('__errorurl'), None)
......
# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This file is part of CubicWeb.
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment