Commit 0378a3ac authored by Jérome Perrin's avatar Jérome Perrin

Change the way messages are generated in constraints: possible messages in

each constraint class are predefined on the class and constraint users can
override the message in the propertysheet.
The motivation is to be able to provide user friendly context dependant
messages, eg. "Please Enter the Shipping Date" instead of technical messages
like "Property start_date is not defined".
See test_OverrideMessage for an example use, and the interface for more info.

(Also add a new CategoryAcquiredExistence constraint)



git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@18364 20353a03-c40f-0410-a6d1-a30d3c3de9de
parent a5eedf0e
...@@ -43,6 +43,13 @@ class AttributeEquality(PropertyExistence): ...@@ -43,6 +43,13 @@ class AttributeEquality(PropertyExistence):
}, },
""" """
_message_id_list = ['message_invalid_attribute_value',
'message_invalid_attribute_value_fixed']
message_invalid_attribute_value = "Attribute ${attribute_name} "\
"value is ${current_value} but should be ${expected_value}"
message_invalid_attribute_value_fixed = "Attribute ${attribute_name} "\
"value is ${current_value} but should be ${expected_value} (Fixed)"
def checkConsistency(self, obj, fixit=0): def checkConsistency(self, obj, fixit=0):
""" """
This is the check method, we return a list of string, This is the check method, we return a list of string,
...@@ -54,9 +61,9 @@ class AttributeEquality(PropertyExistence): ...@@ -54,9 +61,9 @@ class AttributeEquality(PropertyExistence):
return [] return []
errors = PropertyExistence.checkConsistency(self, obj, fixit=fixit) errors = PropertyExistence.checkConsistency(self, obj, fixit=fixit)
for attribute_name, expected_value in self.constraint_definition.items(): for attribute_name, expected_value in self.constraint_definition.items():
error_message = None message_id = None
mapping = dict() mapping = dict()
# If property does not exist, error will be raise by # If property does not exist, error will be raised by
# PropertyExistence Constraint. # PropertyExistence Constraint.
if obj.hasProperty(attribute_name): if obj.hasProperty(attribute_name):
identical = 1 identical = 1
...@@ -73,18 +80,16 @@ class AttributeEquality(PropertyExistence): ...@@ -73,18 +80,16 @@ class AttributeEquality(PropertyExistence):
# Other type # Other type
identical = (expected_value == obj.getProperty(attribute_name)) identical = (expected_value == obj.getProperty(attribute_name))
if not identical: if not identical:
# Generate error_message message_id = 'message_invalid_attribute_value'
error_message = "Attribute ${attribute_name} value is "\
"${current_value} but should be ${expected_value}"
mapping(attribute_name=attribute_name, mapping(attribute_name=attribute_name,
attribute_value=obj.getProperty(attribute_name), attribute_value=obj.getProperty(attribute_name),
expected_value=expected_value) expected_value=expected_value)
# Generate error # Generate error
if error_message is not None: if message_id is not None:
if fixit: if fixit:
obj._setProperty(attribute_name, expected_value) obj._setProperty(attribute_name, expected_value)
error_message = "Attribute ${attribute_name} value is "\ message_id = 'message_invalid_attribute_value_fixed'
"${current_value} but should be ${expected_value} (Fixed)" errors.append(self._generateError(obj,
errors.append(self._generateError(obj, error_message, mapping)) self._getMessage(message_id), mapping))
return errors return errors
...@@ -30,19 +30,31 @@ ...@@ -30,19 +30,31 @@
from Constraint import Constraint from Constraint import Constraint
class CategoryExistence(Constraint): class CategoryExistence(Constraint):
""" """This constraint checks if an object respects the existence of
This method check and fix if an object respects the existence of a category, without acquisition.
a category.
Configuration example: Configuration example:
{ 'id' : 'category_existence', { 'id' : 'category_existence',
'description' : 'Category causality must be defined', 'description' : 'Category causality must be defined',
'type' : 'CategoryExistence', 'type' : 'CategoryExistence',
'portal_type' : ('Person', 'Organisation') 'portal_type' : ('Person', 'Organisation'),
'causality' : None, 'causality' : None,
'condition' : 'python: object.getPortalType() == 'Foo', 'condition' : 'python: object.getPortalType() == 'Foo',
}, },
""" """
_message_id_list = [ 'message_category_not_set',
'message_category_not_associated_with_portal_type' ]
message_category_not_set = "Category existence error for base"\
" category ${base_category}, this category is not defined"
message_category_not_associated_with_portal_type = "Category existence"\
" error for base category ${base_category}, this"\
" document has no such category"
def _calculateArity(self, obj, base_category, portal_type):
return len(obj.getCategoryMembershipList(base_category,
portal_type=portal_type))
def checkConsistency(self, obj, fixit=0): def checkConsistency(self, obj, fixit=0):
""" """
This is the check method, we return a list of string, This is the check method, we return a list of string,
...@@ -50,7 +62,8 @@ class CategoryExistence(Constraint): ...@@ -50,7 +62,8 @@ class CategoryExistence(Constraint):
""" """
if not self._checkConstraintCondition(obj): if not self._checkConstraintCondition(obj):
return [] return []
errors = [] error_list = []
portal_type = self.constraint_definition.get('portal_type', ())
# For each attribute name, we check if defined # For each attribute name, we check if defined
for base_category in self.constraint_definition.keys(): for base_category in self.constraint_definition.keys():
if base_category in ('portal_type', ): if base_category in ('portal_type', ):
...@@ -58,18 +71,36 @@ class CategoryExistence(Constraint): ...@@ -58,18 +71,36 @@ class CategoryExistence(Constraint):
mapping = dict(base_category=base_category) mapping = dict(base_category=base_category)
# Check existence of base category # Check existence of base category
if base_category not in obj.getBaseCategoryList(): if base_category not in obj.getBaseCategoryList():
error_message = "Category existence error for base category "\ error_message = 'message_category_not_associated_with_portal_type'
"${base_category}, this document has no such category" elif self._calculateArity(obj, base_category, portal_type) == 0:
elif len(obj.getCategoryMembershipList(base_category, error_message = 'message_category_not_set'
portal_type = self.constraint_definition\ if base_category == 'destination_section':
.get('portal_type', ()))) == 0: import pdb; pdb.set_trace()
error_message = "Category existence error for base category "\
"${base_category}, this category is not defined"
else: else:
error_message = None error_message = None
# Raise error # Raise error
if error_message: if error_message:
errors.append(self._generateError(obj, error_message, mapping)) error_list.append(self._generateError(obj,
return errors self._getMessage(error_message), mapping))
return error_list
class CategoryAcquiredExistence(CategoryExistence):
"""This constraint check an object respects the existence of a category, with
acquisition.
Configuration example:
{ 'id' : 'category_existence',
'description' : 'Category causality must be defined',
'type' : 'CategoryExistence',
'portal_type' : ('Person', 'Organisation'),
'causality' : None,
'condition' : 'python: object.getPortalType() == 'Foo',
},
"""
def _calculateArity(self, obj, base_category, portal_type):
return len(obj.getAcquiredCategoryMembershipList(base_category,
portal_type=portal_type))
...@@ -45,6 +45,26 @@ class CategoryMembershipArity(Constraint): ...@@ -45,6 +45,26 @@ class CategoryMembershipArity(Constraint):
'condition' : 'python: object.getPortalType() == 'Foo', 'condition' : 'python: object.getPortalType() == 'Foo',
}, },
""" """
_message_id_list = ['message_arity_too_small',
'message_arity_not_in_range',
'message_arity_with_portal_type_to_small',
'message_arity_with_portal_type_not_in_range']
message_arity_too_small = "Arity Error for Relation ${base_category}"\
", arity is equal to ${current_arity} but "\
"should be at least ${min_arity}"
message_arity_not_in_range = "Arity Error for Relation ${base_category}"\
", arity is equal to ${current_arity} but "\
"should be between ${min_arity} and ${max_arity}"
message_arity_with_portal_type_to_small = "Arity Error for Relation"\
" ${base_category} and Type ${portal_type}"\
", arity is equal to ${current_arity} but "\
"should be at least ${min_arity}"
message_arity_with_portal_type_not_in_range = "Arity Error for Relation"\
" ${base_category} and Type ${portal_type}"\
", arity is equal to ${current_arity} but "\
"should be between ${min_arity} and ${max_arity}"
def _calculateArity(self, obj): def _calculateArity(self, obj):
base_category = self.constraint_definition['base_category'] base_category = self.constraint_definition['base_category']
...@@ -62,7 +82,7 @@ class CategoryMembershipArity(Constraint): ...@@ -62,7 +82,7 @@ class CategoryMembershipArity(Constraint):
""" """
if not self._checkConstraintCondition(obj): if not self._checkConstraintCondition(obj):
return [] return []
errors = [] error_list = []
# Retrieve configuration values from PropertySheet (_constraints) # Retrieve configuration values from PropertySheet (_constraints)
base_category = self.constraint_definition['base_category'] base_category = self.constraint_definition['base_category']
min_arity = int(self.constraint_definition['min_arity']) min_arity = int(self.constraint_definition['min_arity'])
...@@ -82,25 +102,16 @@ class CategoryMembershipArity(Constraint): ...@@ -82,25 +102,16 @@ class CategoryMembershipArity(Constraint):
# Generate error message # Generate error message
if portal_type is not (): if portal_type is not ():
if max_arity is None: if max_arity is None:
error_message = "Arity Error for Relation ${base_category}"\ message_id = 'message_arity_with_portal_type_to_small'
" and Type ${portal_type}"\
", arity is equal to ${current_arity} but "\
"should be at least ${min_arity}"
else: else:
error_message = "Arity Error for Relation ${base_category}"\ message_id = 'message_arity_with_portal_type_not_in_range'
" and Type ${portal_type}"\
", arity is equal to ${current_arity} but "\
"should be between ${min_arity} and ${max_arity}"
else: else:
if max_arity is None: if max_arity is None:
error_message = "Arity Error for Relation ${base_category}"\ message_id = 'message_arity_too_small'
", arity is equal to ${current_arity} but "\
"should be at least ${min_arity}"
else: else:
error_message = "Arity Error for Relation ${base_category}"\ message_id = 'message_arity_not_in_range'
", arity is equal to ${current_arity} but "\
"should be between ${min_arity} and ${max_arity}"
# Add error # Add error
errors.append(self._generateError(obj, error_message, mapping)) error_list.append(self._generateError(obj,
return errors self._getMessage(message_id), mapping))
return error_list
...@@ -38,23 +38,22 @@ class Constraint: ...@@ -38,23 +38,22 @@ class Constraint:
""" """
__implements__ = (IConstraint, ) __implements__ = (IConstraint, )
_message_id_list = []
def __init__(self, id=None, description=None, type=None, def __init__(self, id=None, description=None, type=None,
condition=None, **constraint_definition): condition=None, **constraint_definition):
""" """Initialize a constraint.
Remove unwanted attributes from constraint definition and keep
them as instance attributes
""" """
self.id = id self.id = id
self.description = description self.description = description
self.type = type self.type = type
self.condition = condition self.constraint_definition = dict()
self.constraint_definition = constraint_definition self.message_id_dict = dict()
self.edit(id, description, type, condition, **constraint_definition)
def edit(self, id=None, description=None, type=None, def edit(self, id=None, description=None, type=None, condition=None,
**constraint_definition): **constraint_definition):
""" """Edit the constraint instance.
Remove unwanted attributes from constraint definition and keep
them as instance attributes
""" """
if id is not None: if id is not None:
self.id = id self.id = id
...@@ -62,11 +61,22 @@ class Constraint: ...@@ -62,11 +61,22 @@ class Constraint:
self.description = description self.description = description
if type is not None: if type is not None:
self.type = type self.type = type
self.constraint_definition.update(constraint_definition) self.condition = condition
for key, value in constraint_definition.items():
if key in self._message_id_list:
self.message_id_dict[key] = value
else:
self.constraint_definition[key] = value
def _generateError(self, obj, error_message, mapping={}): def _getMessage(self, message_id):
"""Get the message corresponding to this message_id.
""" """
Generic method used to generate error in checkConsistency. if message_id in self.message_id_dict:
return self.message_id_dict[message_id]
return getattr(self, message_id)
def _generateError(self, obj, error_message, mapping={}):
"""Generic method used to generate error in checkConsistency.
""" """
if error_message is not None: if error_message is not None:
msg = ConsistencyMessage(self, obj.getRelativeUrl(), msg = ConsistencyMessage(self, obj.getRelativeUrl(),
......
...@@ -40,6 +40,13 @@ class ContentExistence(Constraint): ...@@ -40,6 +40,13 @@ class ContentExistence(Constraint):
}, },
""" """
_message_id_list = [ 'message_no_subobject',
'message_no_subobject_portal_type' ]
message_no_subobject = "The document does not contain any subobject"
message_no_subobject_portal_type = "The document does not contain any"\
" subobject of portal portal type ${portal_type}"
def checkConsistency(self, object, fixit=0): def checkConsistency(self, object, fixit=0):
""" """
This is the check method, we return a list of string, This is the check method, we return a list of string,
...@@ -48,22 +55,23 @@ class ContentExistence(Constraint): ...@@ -48,22 +55,23 @@ class ContentExistence(Constraint):
""" """
from Products.ERP5Type.Message import Message from Products.ERP5Type.Message import Message
obj = object obj = object
errors = [] error_list = []
if self._checkConstraintCondition(object): if self._checkConstraintCondition(object):
# Retrieve configuration values from PropertySheet (_constraints) # Retrieve configuration values from PropertySheet (_constraints)
portal_type = self.constraint_definition.get('portal_type', ()) portal_type = self.constraint_definition.get('portal_type', ())
if not len(obj.contentValues(portal_type=portal_type)): if not len(obj.contentValues(portal_type=portal_type)):
# Generate error message # Generate error message
mapping = {} mapping = {}
error_message = "The document does not contain any subobject" message_id = 'message_no_subobject'
if portal_type is not (): if portal_type is not ():
error_message += " of portal type ${portal_type}" message_id = 'message_no_subobject_portal_type'
# XXX maybe this could be factored out # XXX maybe this could be factored out
if isinstance(portal_type, basestring): if isinstance(portal_type, basestring):
portal_type = (portal_type, ) portal_type = (portal_type, )
mapping['portal_type'] = str(Message('erp5_ui', ' or ')).join( mapping['portal_type'] = str(Message('erp5_ui', ' or ')).join(
[str(Message('erp5_ui', pt)) for pt in portal_type]) [str(Message('erp5_ui', pt)) for pt in portal_type])
# Add error # Add error
errors.append(self._generateError(obj, error_message, mapping)) error_list.append(self._generateError(obj,
return errors self._getMessage(message_id), mapping))
return error_list
...@@ -47,6 +47,19 @@ class PortalTypeClass(Constraint): ...@@ -47,6 +47,19 @@ class PortalTypeClass(Constraint):
}, },
""" """
_message_id_list = [ 'message_type_not_registred',
'message_inconsistent_meta_type',
'message_inconsistent_class' ]
message_type_not_registred = "Type Information ${type_name} not "\
"registred with the TypeTool"
message_inconsistent_meta_type = "Meta type is inconsistant with portal"\
" type definition. Portal type meta type is ${portal_type_meta_type}"\
" class meta type is ${class_meta_type}"
message_inconsistent_class = "__class__ is inconsistant with portal type"\
" definition. Portal Type class is ${portal_type_class},"\
" document class is ${document_class}"
def checkConsistency(self, obj, fixit=0): def checkConsistency(self, obj, fixit=0):
""" """
This is the check method, we return a list of string, This is the check method, we return a list of string,
...@@ -54,32 +67,28 @@ class PortalTypeClass(Constraint): ...@@ -54,32 +67,28 @@ class PortalTypeClass(Constraint):
""" """
if not self._checkConstraintCondition(obj): if not self._checkConstraintCondition(obj):
return [] return []
errors = [] error_list = []
types_tool = getToolByName(obj, 'portal_types') types_tool = getToolByName(obj, 'portal_types')
type_info = types_tool._getOb(obj.getPortalType(), None) type_info = types_tool._getOb(obj.getPortalType(), None)
if type_info is None : if type_info is None :
errors.append(self._generateError(obj, error_list.append(self._generateError(obj,
"Type Information ${type_name} not registred with the TypeTool", self._getMessage('message_type_not_registred'),
mapping=dict(type_name=obj.getPortalType()))) mapping=dict(type_name=obj.getPortalType())))
elif type_info.content_meta_type != obj.meta_type : elif type_info.content_meta_type != obj.meta_type :
errors.append(self._generateError(obj, error_list.append(self._generateError(obj,
"Meta type is inconsistant with portal type definition."\ self._getMessage('message_inconsistent_meta_type'),
" Portal type meta type is ${portal_type_meta_type}"\
" class meta type is ${class_meta_type} ",
mapping=dict(portal_type_meta_type=type_info.content_meta_type, mapping=dict(portal_type_meta_type=type_info.content_meta_type,
class_meta_type=obj.meta_type))) class_meta_type=obj.meta_type)))
else : else :
portal_type_class = self._getClassForPortalType(obj, type_info) portal_type_class = self._getClassForPortalType(obj, type_info)
obj_class = str(obj.__class__) obj_class = str(obj.__class__)
if portal_type_class != obj_class : if portal_type_class != obj_class :
errors.append(self._generateError(obj, error_list.append(self._generateError(obj,
"__class__ is inconsistant with portal type definition."\ self._getMessage('message_inconsistent_class'),
" Portal Type class is ${portal_type_class},"
" document class is ${document_class}",
mapping=dict(portal_type_class=portal_type_class, mapping=dict(portal_type_class=portal_type_class,
document_class=obj_class))) document_class=obj_class)))
# TODO fixit argument can be implemented here. # TODO fixit argument can be implemented here.
return errors return error_list
def _getClassForPortalType(self, obj, type_info): def _getClassForPortalType(self, obj, type_info):
......
...@@ -44,6 +44,13 @@ class PropertyExistence(Constraint): ...@@ -44,6 +44,13 @@ class PropertyExistence(Constraint):
}, },
""" """
_message_id_list = ['message_no_such_propery',
'message_property_not_set']
message_no_such_propery = "Property existence error for property "\
"${property_id}, this document has no such property"
message_property_not_set = "Property existence error for property "\
"${property_id}, this property is not defined"
def checkConsistency(self, obj, fixit=0): def checkConsistency(self, obj, fixit=0):
""" """
This is the check method, we return a list of string, This is the check method, we return a list of string,
...@@ -51,23 +58,21 @@ class PropertyExistence(Constraint): ...@@ -51,23 +58,21 @@ class PropertyExistence(Constraint):
""" """
if not self._checkConstraintCondition(obj): if not self._checkConstraintCondition(obj):
return [] return []
errors = [] error_list = []
# For each attribute name, we check if defined # For each attribute name, we check if defined
for property_id in self.constraint_definition.keys(): for property_id in self.constraint_definition.keys():
# Check existence of property # Check existence of property
mapping = dict(property_id=property_id) mapping = dict(property_id=property_id)
if not obj.hasProperty(property_id): if not obj.hasProperty(property_id):
error_message = "Property existence error for property "\ error_message_id = "message_no_such_propery"
"${property_id}, this document has no such property"
elif obj.getProperty(property_id) is None: elif obj.getProperty(property_id) is None:
# If value is '', attribute is considered a defined # If value is '', attribute is considered a defined
# XXX is this the default API ? # XXX is this the default API ?
error_message = "Property existence error for property "\ error_message_id = "message_property_not_set"
"${property_id}, this property is not defined"
else: else:
error_message = None error_message_id = None
# Return error
error = self._generateError(obj, error_message, mapping) if error_message_id:
if error is not None: error_list.append(self._generateError(obj,
errors.append(error) self._getMessage(error_message_id), mapping))
return errors return error_list
...@@ -62,12 +62,28 @@ class PropertyTypeValidity(Constraint): ...@@ -62,12 +62,28 @@ class PropertyTypeValidity(Constraint):
# Properties of type eg. "object" can hold anything # Properties of type eg. "object" can hold anything
_permissive_type_list = ('object', ) _permissive_type_list = ('object', )
_message_id_list = [ 'message_unknown_type',
'message_incorrect_type',
'message_incorrect_type_fix_failed',
'message_incorrect_type_fixed']
message_unknown_type = "Attribute ${attribute_name} is defined with"\
" an unknown type ${type_name}"
message_incorrect_type = "Attribute ${attribute_name}"\
" should be of type ${expected_type} but is of type ${actual_type} (Fixed)"
message_incorrect_type_fix_failed = "Attribute ${attribute_name}"\
" should be of type ${expected_type} but is of type ${actual_type}"\
" (Type cast failed with error ${type_cast_error})"
message_incorrect_type_fixed = "Attribute ${attribute_name}"\
" should be of type ${expected_type} but is of type ${actual_type} (Fixed)"
def checkConsistency(self, obj, fixit=0): def checkConsistency(self, obj, fixit=0):
""" """
This is the check method, we return a list of string, This is the check method, we return a list of string,
each string corresponds to an error. each string corresponds to an error.
""" """
errors = [] error_list = []
# For each attribute name, we check type # For each attribute name, we check type
for prop in obj.propertyMap(): for prop in obj.propertyMap():
property_id = prop['id'] property_id = prop['id']
...@@ -88,16 +104,14 @@ class PropertyTypeValidity(Constraint): ...@@ -88,16 +104,14 @@ class PropertyTypeValidity(Constraint):
wrong_type = not isinstance(value, self._type_dict[property_type]) wrong_type = not isinstance(value, self._type_dict[property_type])
except KeyError: except KeyError:
wrong_type = 0 wrong_type = 0
errors.append(self._generateError(obj, error_list.append(self._generateError(obj,
"Attribute ${attribute_name} is defined with " self._getMessage('message_unknown_type'),
"an unknown type ${type_name}",
mapping=dict(attribute_name=property_id, mapping=dict(attribute_name=property_id,
type_name=property_type))) type_name=property_type)))
if wrong_type: if wrong_type:
# Type is wrong, so, raise constraint error # Type is wrong, so, raise constraint error
error_message = "Attribute ${attribute_name} should be of type"\ error_message = 'message_incorrect_type'
" ${expected_type} but is of type ${actual_type}"
mapping = dict(attribute_name=property_id, mapping = dict(attribute_name=property_id,
expected_type=property_type, expected_type=property_type,
actual_type=str(type(value))) actual_type=str(type(value)))
...@@ -107,19 +121,17 @@ class PropertyTypeValidity(Constraint): ...@@ -107,19 +121,17 @@ class PropertyTypeValidity(Constraint):
try: try:
value = self._type_dict[property_type][0](value) value = self._type_dict[property_type][0](value)
except (KeyError, ValueError), error: except (KeyError, ValueError), error:
error_message = "Attribute ${attribute_name} should be of type"\ error_message = 'message_incorrect_type_fix_failed'
" ${expected_type} but is of type ${actual_type} (Type cast"\
" failed with error ${type_cast_error}"
mapping['type_cast_error'] = str(error) mapping['type_cast_error'] = str(error)
else: else:
obj.setProperty(property_id, value) obj.setProperty(property_id, value)
error_message = "Attribute ${attribute_name} should be of type"\ error_message = 'message_incorrect_type_fixed'
" ${expected_type} but is of type ${actual_type} (Fixed)"
errors.append(self._generateError(obj, error_message, mapping)) error_list.append(self._generateError(obj,
self._getMessage(error_message), mapping))
elif fixit: elif fixit:
oldvalue = getattr(obj, property_id, value) oldvalue = getattr(obj, property_id, value)
if oldvalue != value: if oldvalue != value:
obj.setProperty(property_id, oldvalue) obj.setProperty(property_id, oldvalue)
return errors return error_list
...@@ -41,6 +41,12 @@ class StringAttributeMatch(PropertyExistence): ...@@ -41,6 +41,12 @@ class StringAttributeMatch(PropertyExistence):
}, },
""" """
_message_id_list = PropertyExistence._message_id_list +\
['message_attribute_does_not_match']
message_attribute_does_not_match = "Attribute ${attribute_name} is "\
"${attribute_value} and does not match ${regular_expression}."
def checkConsistency(self, object, fixit=0): def checkConsistency(self, object, fixit=0):
""" """
This is the check method, we return a list of string, This is the check method, we return a list of string,
...@@ -62,8 +68,7 @@ class StringAttributeMatch(PropertyExistence): ...@@ -62,8 +68,7 @@ class StringAttributeMatch(PropertyExistence):
# Generate error # Generate error
error_list.append(self._generateError(object, error_list.append(self._generateError(object,
"Attribute ${attribute_name} is ${attribute_value} and" self._getMessage('message_attribute_does_not_match'),
" does not match ${regular_expression}.",
mapping=dict(attribute_name=attribute_name, mapping=dict(attribute_name=attribute_name,
attribute_value=repr(current_value), attribute_value=repr(current_value),
regular_expression=repr(regular_expression)))) regular_expression=repr(regular_expression))))
......
...@@ -50,6 +50,13 @@ class TALESConstraint(Constraint): ...@@ -50,6 +50,13 @@ class TALESConstraint(Constraint):
For readability, please don't abuse this constraint to evaluate complex For readability, please don't abuse this constraint to evaluate complex
things. If necessary, write your own constraint class. things. If necessary, write your own constraint class.
""" """
_message_id_list = [ 'message_expression_false',
'message_expression_error' ]
message_expression_false = "Expression was false"
message_expression_error = \
"Error while evaluating expression: ${error_text}"
def checkConsistency(self, obj, fixit=0): def checkConsistency(self, obj, fixit=0):
"""See Interface """ """See Interface """
...@@ -57,20 +64,21 @@ class TALESConstraint(Constraint): ...@@ -57,20 +64,21 @@ class TALESConstraint(Constraint):
from Products.ERP5Type.Utils import createExpressionContext from Products.ERP5Type.Utils import createExpressionContext
if not self._checkConstraintCondition(obj): if not self._checkConstraintCondition(obj):
return [] return []
errors = [] error_list = []
expression_text = self.constraint_definition['expression'] expression_text = self.constraint_definition['expression']
expression = Expression(expression_text) expression = Expression(expression_text)
econtext = createExpressionContext(obj) econtext = createExpressionContext(obj)
try: try:
if not expression(econtext): if not expression(econtext):
errors.append(self._generateError(obj, 'Expression was false')) error_list.append(self._generateError(obj,
self._getMessage('message_expression_false')))
except (ConflictError, CompilerError): except (ConflictError, CompilerError):
raise raise
except Exception, e: except Exception, e:
LOG('ERP5Type', PROBLEM, 'TALESConstraint error on "%s" on %s' % LOG('ERP5Type', PROBLEM, 'TALESConstraint error on "%s" on %s' %
(self.constraint_definition['expression'], obj), error=sys.exc_info()) (self.constraint_definition['expression'], obj), error=sys.exc_info())
errors.append(self._generateError(obj, error_list.append(self._generateError(obj,
'Error while evaluating expression: ${error_text}', self._getMessage('message_expression_error'),
mapping=dict(error_text=str(e)))) mapping=dict(error_text=str(e))))
return errors return error_list
...@@ -5,6 +5,7 @@ from CategoryRelatedMembershipArity import CategoryRelatedMembershipArity ...@@ -5,6 +5,7 @@ from CategoryRelatedMembershipArity import CategoryRelatedMembershipArity
from AttributeEquality import AttributeEquality from AttributeEquality import AttributeEquality
from PropertyExistence import PropertyExistence from PropertyExistence import PropertyExistence
from CategoryExistence import CategoryExistence from CategoryExistence import CategoryExistence
from CategoryExistence import CategoryAcquiredExistence
from PortalTypeClass import PortalTypeClass from PortalTypeClass import PortalTypeClass
from CategoryAcquiredMembershipArity import CategoryAcquiredMembershipArity from CategoryAcquiredMembershipArity import CategoryAcquiredMembershipArity
from TALESConstraint import TALESConstraint from TALESConstraint import TALESConstraint
......
...@@ -29,11 +29,8 @@ ...@@ -29,11 +29,8 @@
"""Constraint Interface. """Constraint Interface.
""" """
try: from Interface import Interface
from Interface import Interface from Interface import Attribute
except ImportError:
# for Zope versions before 2.6.0
from Interface import Base as Interface
class Constraint(Interface): class Constraint(Interface):
"""ERP5 Constraints are classes that are in charge of checking wether an """ERP5 Constraints are classes that are in charge of checking wether an
...@@ -60,12 +57,22 @@ class Constraint(Interface): ...@@ -60,12 +57,22 @@ class Constraint(Interface):
# XXX condition is a TALES Expression; is it part of the API ? # XXX condition is a TALES Expression; is it part of the API ?
# how to use condition based on a workflow state in a workflow before # how to use condition based on a workflow state in a workflow before
# script, where the document is not in that state yet ? # script, where the document is not in that state yet ? /XXX
'condition': 'python: object.getPortalType() == "Foo"'
# You can add a condition, and this constraint will only be checked
# if the condition evaluates to a true value.
'condition': 'python: object.getPortalType() == "Foo"',
# Additional Constraint parameters are configured here. # Additional Constraint parameters are configured here.
# Constraint docstring should provide a configuration example and a # Constraint docstring should provide a configuration example and a
# documentation on parameter they accept. # documentation on parameter they accept.
# Here is also the place where Constraint users may override message
# existing for this constraint. For instance, you can use a
# CategoryExistence constraint to check if a `source` property is
# defined, and return a nice "Please set the Supplier" (translated in
# the user language) as workflow validation failure message.
'message_category_not_set': "Please set the Supplier",
} }
) )
...@@ -78,12 +85,23 @@ class Constraint(Interface): ...@@ -78,12 +85,23 @@ class Constraint(Interface):
recursivly. recursivly.
""" """
def checkConsistency(obj, fixit=0): def checkConsistency(obj, fixit=0):
"""This method checks the consistency of object 'obj', and fix errors if """This method checks the consistency of object 'obj', and fix errors if
the argument 'fixit' is true. Not all constraint have to support error the argument 'fixit' is true. Not all constraint have to support error
repairing, in that case, simply ignore the fixit parameter. repairing, in that case, simply ignore the fixit parameter. This method
This method should return a list of errors, which are a list for now. should return a list of errors, which are a list of `ConsistencyMessage`,
with a `getTranslatedMessage` method for user interaction.
"""
_message_id_list = Attribute("The list of messages IDs that can be "
"overriden for this constraint.")
def _getMessage(message_id):
"""Returns the message for this message_id.
A message_id can be overriden in the property sheet using this constraint.
Default message values are defined in the constraint class.
""" """
def _generateError(obj, error_message, mapping={}): def _generateError(obj, error_message, mapping={}):
...@@ -96,15 +114,28 @@ class Constraint(Interface): ...@@ -96,15 +114,28 @@ class Constraint(Interface):
Then this message ("Something is wrong !") will be translated when the Then this message ("Something is wrong !") will be translated when the
caller of document.checkConsistency() calls getTranslatedMessage() on caller of document.checkConsistency() calls getTranslatedMessage() on
ConsistencyMessage instances returned by checkConsistency. a ConsistencyMessage instance returned by checkConsistency.
Possible messages should be defined in constraint definition, in the list
_message_id_list, and a default message value should be defined as class
attribute.
In the example, you would have in the constraint class definition::
# list of existing messages
_message_id_list = ['message_something_wrong']
# messages default value
message_something_wrong = 'Something is wrong: ${what}'
We'll use _getMessage to get the corresponding message.
The implementation uses ERP5Type's Messages, so it's possible to use a The implementation uses ERP5Type's Messages, so it's possible to use a
'mapping' for substitution, like this:: 'mapping' for substitution, like this::
>>> if something_is_wrong: >>> if something_is_wrong:
>>> error_list.append(self._generateError(obj, >>> error_list.append(self._generateError(obj,
... 'Something is wrong: ${wrong_thing}', ... self._getMessage('message_something_wrong'),
... mapping=dict(wrong_thing=obj.getTheWrongThing()))) ... mapping=dict(what=obj.getTheWrongThing())))
""" """
...@@ -33,6 +33,7 @@ from Products.ERP5Type.tests.testERP5Type import PropertySheetTestCase ...@@ -33,6 +33,7 @@ from Products.ERP5Type.tests.testERP5Type import PropertySheetTestCase
from AccessControl.SecurityManagement import newSecurityManager from AccessControl.SecurityManagement import newSecurityManager
from Products.ERP5Type.tests.Sequence import Sequence, SequenceList from Products.ERP5Type.tests.Sequence import Sequence, SequenceList
class TestConstraint(PropertySheetTestCase): class TestConstraint(PropertySheetTestCase):
run_all_test = 1 run_all_test = 1
...@@ -1368,7 +1369,46 @@ class TestConstraint(PropertySheetTestCase): ...@@ -1368,7 +1369,46 @@ class TestConstraint(PropertySheetTestCase):
sequence_list.play(self, quiet=quiet) sequence_list.play(self, quiet=quiet)
def test_RegisterWithPropertySheet(self):
# constraint are registred in property sheets
obj = self._makeOne()
obj.setTitle('b')
self._addPropertySheet(obj.getPortalType(),
'''class TestPropertySheet:
_constraints = (
{ 'id': 'testing_constraint',
'type': 'StringAttributeMatch',
'title': 'a.*', },)
''')
consistency_message_list = obj.checkConsistency()
self.assertEquals(1, len(consistency_message_list))
message = consistency_message_list[0]
from Products.ERP5Type.ConsistencyMessage import ConsistencyMessage
self.assertTrue(isinstance(message, ConsistencyMessage))
self.assertEquals(message.class_name, 'StringAttributeMatch')
obj.setTitle('a')
self.assertEquals(obj.checkConsistency(), [])
def test_OverrideMessage(self):
# messages can be overriden in property sheet
obj = self._makeOne()
obj.setTitle('b')
self._addPropertySheet(obj.getPortalType(),
'''class TestPropertySheet:
_constraints = (
{ 'id': 'testing_constraint',
'message_attribute_does_not_match':
'Attribute ${attribute_name} does not match',
'type': 'StringAttributeMatch',
'title': 'a.*', },)
''')
consistency_message_list = obj.checkConsistency()
self.assertEquals(1, len(consistency_message_list))
message = consistency_message_list[0]
self.assertEquals('Attribute title does not match',
str(message.getTranslatedMessage()))
def test_suite(): def test_suite():
suite = unittest.TestSuite() suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(TestConstraint)) suite.addTest(unittest.makeSuite(TestConstraint))
......
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