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):
},
"""
_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):
"""
This is the check method, we return a list of string,
......@@ -54,9 +61,9 @@ class AttributeEquality(PropertyExistence):
return []
errors = PropertyExistence.checkConsistency(self, obj, fixit=fixit)
for attribute_name, expected_value in self.constraint_definition.items():
error_message = None
message_id = None
mapping = dict()
# If property does not exist, error will be raise by
# If property does not exist, error will be raised by
# PropertyExistence Constraint.
if obj.hasProperty(attribute_name):
identical = 1
......@@ -73,18 +80,16 @@ class AttributeEquality(PropertyExistence):
# Other type
identical = (expected_value == obj.getProperty(attribute_name))
if not identical:
# Generate error_message
error_message = "Attribute ${attribute_name} value is "\
"${current_value} but should be ${expected_value}"
message_id = 'message_invalid_attribute_value'
mapping(attribute_name=attribute_name,
attribute_value=obj.getProperty(attribute_name),
expected_value=expected_value)
# Generate error
if error_message is not None:
if message_id is not None:
if fixit:
obj._setProperty(attribute_name, expected_value)
error_message = "Attribute ${attribute_name} value is "\
"${current_value} but should be ${expected_value} (Fixed)"
errors.append(self._generateError(obj, error_message, mapping))
message_id = 'message_invalid_attribute_value_fixed'
errors.append(self._generateError(obj,
self._getMessage(message_id), mapping))
return errors
......@@ -30,19 +30,31 @@
from Constraint import Constraint
class CategoryExistence(Constraint):
"""
This method check and fix if an object respects the existence of
a category.
"""This constraint checks if an object respects the existence of
a category, without acquisition.
Configuration example:
{ 'id' : 'category_existence',
'description' : 'Category causality must be defined',
'type' : 'CategoryExistence',
'portal_type' : ('Person', 'Organisation')
'portal_type' : ('Person', 'Organisation'),
'causality' : None,
'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):
"""
This is the check method, we return a list of string,
......@@ -50,7 +62,8 @@ class CategoryExistence(Constraint):
"""
if not self._checkConstraintCondition(obj):
return []
errors = []
error_list = []
portal_type = self.constraint_definition.get('portal_type', ())
# For each attribute name, we check if defined
for base_category in self.constraint_definition.keys():
if base_category in ('portal_type', ):
......@@ -58,18 +71,36 @@ class CategoryExistence(Constraint):
mapping = dict(base_category=base_category)
# Check existence of base category
if base_category not in obj.getBaseCategoryList():
error_message = "Category existence error for base category "\
"${base_category}, this document has no such category"
elif len(obj.getCategoryMembershipList(base_category,
portal_type = self.constraint_definition\
.get('portal_type', ()))) == 0:
error_message = "Category existence error for base category "\
"${base_category}, this category is not defined"
error_message = 'message_category_not_associated_with_portal_type'
elif self._calculateArity(obj, base_category, portal_type) == 0:
error_message = 'message_category_not_set'
if base_category == 'destination_section':
import pdb; pdb.set_trace()
else:
error_message = None
# Raise error
if error_message:
errors.append(self._generateError(obj, error_message, mapping))
return errors
error_list.append(self._generateError(obj,
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):
'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):
base_category = self.constraint_definition['base_category']
......@@ -62,7 +82,7 @@ class CategoryMembershipArity(Constraint):
"""
if not self._checkConstraintCondition(obj):
return []
errors = []
error_list = []
# Retrieve configuration values from PropertySheet (_constraints)
base_category = self.constraint_definition['base_category']
min_arity = int(self.constraint_definition['min_arity'])
......@@ -82,25 +102,16 @@ class CategoryMembershipArity(Constraint):
# Generate error message
if portal_type is not ():
if max_arity is None:
error_message = "Arity Error for Relation ${base_category}"\
" and Type ${portal_type}"\
", arity is equal to ${current_arity} but "\
"should be at least ${min_arity}"
message_id = 'message_arity_with_portal_type_to_small'
else:
error_message = "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}"
message_id = 'message_arity_with_portal_type_not_in_range'
else:
if max_arity is None:
error_message = "Arity Error for Relation ${base_category}"\
", arity is equal to ${current_arity} but "\
"should be at least ${min_arity}"
message_id = 'message_arity_too_small'
else:
error_message = "Arity Error for Relation ${base_category}"\
", arity is equal to ${current_arity} but "\
"should be between ${min_arity} and ${max_arity}"
message_id = 'message_arity_not_in_range'
# Add error
errors.append(self._generateError(obj, error_message, mapping))
return errors
error_list.append(self._generateError(obj,
self._getMessage(message_id), mapping))
return error_list
......@@ -38,23 +38,22 @@ class Constraint:
"""
__implements__ = (IConstraint, )
_message_id_list = []
def __init__(self, id=None, description=None, type=None,
condition=None, **constraint_definition):
"""
Remove unwanted attributes from constraint definition and keep
them as instance attributes
"""Initialize a constraint.
"""
self.id = id
self.description = description
self.type = type
self.condition = condition
self.constraint_definition = constraint_definition
self.constraint_definition = dict()
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):
"""
Remove unwanted attributes from constraint definition and keep
them as instance attributes
"""Edit the constraint instance.
"""
if id is not None:
self.id = id
......@@ -62,11 +61,22 @@ class Constraint:
self.description = description
if type is not None:
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:
msg = ConsistencyMessage(self, obj.getRelativeUrl(),
......
......@@ -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):
"""
This is the check method, we return a list of string,
......@@ -48,22 +55,23 @@ class ContentExistence(Constraint):
"""
from Products.ERP5Type.Message import Message
obj = object
errors = []
error_list = []
if self._checkConstraintCondition(object):
# Retrieve configuration values from PropertySheet (_constraints)
portal_type = self.constraint_definition.get('portal_type', ())
if not len(obj.contentValues(portal_type=portal_type)):
# Generate error message
mapping = {}
error_message = "The document does not contain any subobject"
message_id = 'message_no_subobject'
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
if isinstance(portal_type, basestring):
portal_type = (portal_type, )
mapping['portal_type'] = str(Message('erp5_ui', ' or ')).join(
[str(Message('erp5_ui', pt)) for pt in portal_type])
# Add error
errors.append(self._generateError(obj, error_message, mapping))
return errors
error_list.append(self._generateError(obj,
self._getMessage(message_id), mapping))
return error_list
......@@ -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):
"""
This is the check method, we return a list of string,
......@@ -54,32 +67,28 @@ class PortalTypeClass(Constraint):
"""
if not self._checkConstraintCondition(obj):
return []
errors = []
error_list = []
types_tool = getToolByName(obj, 'portal_types')
type_info = types_tool._getOb(obj.getPortalType(), None)
if type_info is None :
errors.append(self._generateError(obj,
"Type Information ${type_name} not registred with the TypeTool",
error_list.append(self._generateError(obj,
self._getMessage('message_type_not_registred'),
mapping=dict(type_name=obj.getPortalType())))
elif type_info.content_meta_type != obj.meta_type :
errors.append(self._generateError(obj,
"Meta type is inconsistant with portal type definition."\
" Portal type meta type is ${portal_type_meta_type}"\
" class meta type is ${class_meta_type} ",
error_list.append(self._generateError(obj,
self._getMessage('message_inconsistent_meta_type'),
mapping=dict(portal_type_meta_type=type_info.content_meta_type,
class_meta_type=obj.meta_type)))
else :
portal_type_class = self._getClassForPortalType(obj, type_info)
obj_class = str(obj.__class__)
if portal_type_class != obj_class :
errors.append(self._generateError(obj,
"__class__ is inconsistant with portal type definition."\
" Portal Type class is ${portal_type_class},"
" document class is ${document_class}",
error_list.append(self._generateError(obj,
self._getMessage('message_inconsistent_class'),
mapping=dict(portal_type_class=portal_type_class,
document_class=obj_class)))
# TODO fixit argument can be implemented here.
return errors
return error_list
def _getClassForPortalType(self, obj, type_info):
......
......@@ -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):
"""
This is the check method, we return a list of string,
......@@ -51,23 +58,21 @@ class PropertyExistence(Constraint):
"""
if not self._checkConstraintCondition(obj):
return []
errors = []
error_list = []
# For each attribute name, we check if defined
for property_id in self.constraint_definition.keys():
# Check existence of property
mapping = dict(property_id=property_id)
if not obj.hasProperty(property_id):
error_message = "Property existence error for property "\
"${property_id}, this document has no such property"
error_message_id = "message_no_such_propery"
elif obj.getProperty(property_id) is None:
# If value is '', attribute is considered a defined
# XXX is this the default API ?
error_message = "Property existence error for property "\
"${property_id}, this property is not defined"
error_message_id = "message_property_not_set"
else:
error_message = None
# Return error
error = self._generateError(obj, error_message, mapping)
if error is not None:
errors.append(error)
return errors
error_message_id = None
if error_message_id:
error_list.append(self._generateError(obj,
self._getMessage(error_message_id), mapping))
return error_list
......@@ -62,12 +62,28 @@ class PropertyTypeValidity(Constraint):
# Properties of type eg. "object" can hold anything
_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):
"""
This is the check method, we return a list of string,
each string corresponds to an error.
"""
errors = []
error_list = []
# For each attribute name, we check type
for prop in obj.propertyMap():
property_id = prop['id']
......@@ -88,16 +104,14 @@ class PropertyTypeValidity(Constraint):
wrong_type = not isinstance(value, self._type_dict[property_type])
except KeyError:
wrong_type = 0
errors.append(self._generateError(obj,
"Attribute ${attribute_name} is defined with "
"an unknown type ${type_name}",
error_list.append(self._generateError(obj,
self._getMessage('message_unknown_type'),
mapping=dict(attribute_name=property_id,
type_name=property_type)))
if wrong_type:
# Type is wrong, so, raise constraint error
error_message = "Attribute ${attribute_name} should be of type"\
" ${expected_type} but is of type ${actual_type}"
error_message = 'message_incorrect_type'
mapping = dict(attribute_name=property_id,
expected_type=property_type,
actual_type=str(type(value)))
......@@ -107,19 +121,17 @@ class PropertyTypeValidity(Constraint):
try:
value = self._type_dict[property_type][0](value)
except (KeyError, ValueError), error:
error_message = "Attribute ${attribute_name} should be of type"\
" ${expected_type} but is of type ${actual_type} (Type cast"\
" failed with error ${type_cast_error}"
error_message = 'message_incorrect_type_fix_failed'
mapping['type_cast_error'] = str(error)
else:
obj.setProperty(property_id, value)
error_message = "Attribute ${attribute_name} should be of type"\
" ${expected_type} but is of type ${actual_type} (Fixed)"
error_message = 'message_incorrect_type_fixed'
errors.append(self._generateError(obj, error_message, mapping))
error_list.append(self._generateError(obj,
self._getMessage(error_message), mapping))
elif fixit:
oldvalue = getattr(obj, property_id, value)
if oldvalue != value:
obj.setProperty(property_id, oldvalue)
return errors
return error_list
......@@ -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):
"""
This is the check method, we return a list of string,
......@@ -62,8 +68,7 @@ class StringAttributeMatch(PropertyExistence):
# Generate error
error_list.append(self._generateError(object,
"Attribute ${attribute_name} is ${attribute_value} and"
" does not match ${regular_expression}.",
self._getMessage('message_attribute_does_not_match'),
mapping=dict(attribute_name=attribute_name,
attribute_value=repr(current_value),
regular_expression=repr(regular_expression))))
......
......@@ -51,26 +51,34 @@ class TALESConstraint(Constraint):
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):
"""See Interface """
# import this later to prevent circular import
from Products.ERP5Type.Utils import createExpressionContext
if not self._checkConstraintCondition(obj):
return []
errors = []
error_list = []
expression_text = self.constraint_definition['expression']
expression = Expression(expression_text)
econtext = createExpressionContext(obj)
try:
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):
raise
except Exception, e:
LOG('ERP5Type', PROBLEM, 'TALESConstraint error on "%s" on %s' %
(self.constraint_definition['expression'], obj), error=sys.exc_info())
errors.append(self._generateError(obj,
'Error while evaluating expression: ${error_text}',
error_list.append(self._generateError(obj,
self._getMessage('message_expression_error'),
mapping=dict(error_text=str(e))))
return errors
return error_list
......@@ -5,6 +5,7 @@ from CategoryRelatedMembershipArity import CategoryRelatedMembershipArity
from AttributeEquality import AttributeEquality
from PropertyExistence import PropertyExistence
from CategoryExistence import CategoryExistence
from CategoryExistence import CategoryAcquiredExistence
from PortalTypeClass import PortalTypeClass
from CategoryAcquiredMembershipArity import CategoryAcquiredMembershipArity
from TALESConstraint import TALESConstraint
......
......@@ -29,11 +29,8 @@
"""Constraint Interface.
"""
try:
from Interface import Interface
except ImportError:
# for Zope versions before 2.6.0
from Interface import Base as Interface
from Interface import Interface
from Interface import Attribute
class Constraint(Interface):
"""ERP5 Constraints are classes that are in charge of checking wether an
......@@ -60,12 +57,22 @@ class Constraint(Interface):
# 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
# script, where the document is not in that state yet ?
'condition': 'python: object.getPortalType() == "Foo"'
# script, where the document is not in that state yet ? /XXX
# 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.
# Constraint docstring should provide a configuration example and a
# 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",
}
)
......@@ -82,8 +89,19 @@ class Constraint(Interface):
def checkConsistency(obj, fixit=0):
"""This method checks the consistency of object 'obj', and fix errors if
the argument 'fixit' is true. Not all constraint have to support error
repairing, in that case, simply ignore the fixit parameter.
This method should return a list of errors, which are a list for now.
repairing, in that case, simply ignore the fixit parameter. This method
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={}):
......@@ -96,15 +114,28 @@ class Constraint(Interface):
Then this message ("Something is wrong !") will be translated when the
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
'mapping' for substitution, like this::
>>> if something_is_wrong:
>>> error_list.append(self._generateError(obj,
... 'Something is wrong: ${wrong_thing}',
... mapping=dict(wrong_thing=obj.getTheWrongThing())))
... self._getMessage('message_something_wrong'),
... mapping=dict(what=obj.getTheWrongThing())))
"""
......@@ -33,6 +33,7 @@ from Products.ERP5Type.tests.testERP5Type import PropertySheetTestCase
from AccessControl.SecurityManagement import newSecurityManager
from Products.ERP5Type.tests.Sequence import Sequence, SequenceList
class TestConstraint(PropertySheetTestCase):
run_all_test = 1
......@@ -1368,6 +1369,45 @@ class TestConstraint(PropertySheetTestCase):
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():
suite = unittest.TestSuite()
......
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