From cf0460f5bdb12be8e841372e04893e4331032395 Mon Sep 17 00:00:00 2001 From: Arnaud Fontaine <arnaud.fontaine@nexedi.com> Date: Thu, 25 Nov 2010 04:53:46 +0000 Subject: [PATCH] Add Attribute Equality Constraint for ZODB Property Sheets git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@40636 20353a03-c40f-0410-a6d1-a30d3c3de9de --- .../Core/AttributeEqualityConstraint.py | 129 ++++++++++++++++++ .../AttributeEqualityConstraint.py | 46 +++++++ product/ERP5Type/PropertySheet/__init__.py | 1 + product/ERP5Type/mixin/constraint.py | 4 + .../tests/testDynamicClassGeneration.py | 76 +++++++++-- 5 files changed, 244 insertions(+), 12 deletions(-) create mode 100644 product/ERP5Type/Core/AttributeEqualityConstraint.py create mode 100644 product/ERP5Type/PropertySheet/AttributeEqualityConstraint.py diff --git a/product/ERP5Type/Core/AttributeEqualityConstraint.py b/product/ERP5Type/Core/AttributeEqualityConstraint.py new file mode 100644 index 0000000000..344615a532 --- /dev/null +++ b/product/ERP5Type/Core/AttributeEqualityConstraint.py @@ -0,0 +1,129 @@ +############################################################################## +# +# Copyright (c) 2002-2010 Nexedi SARL and Contributors. All Rights Reserved. +# Sebastien Robin <seb@nexedi.com> +# Jean-Paul Smets <jp@nexedi.com> +# Romain Courteaud <romain@nexedi.com> +# Arnaud Fontaine <arnaud.fontaine@nexedi.com> +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability of assessing all potential +# consequences resulting from its eventual inadequacies and bugs +# End users who are looking for a ready-to-use solution with commercial +# garantees and support are strongly adviced to contract a Free Software +# Service Company +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +############################################################################## + +from Products.ERP5Type.mixin.constraint import ConstraintMixin +from AccessControl import ClassSecurityInfo +from Products.ERP5Type import Permissions, PropertySheet +from Products.CMFCore.Expression import Expression +from Products.ERP5Type.Utils import createExpressionContext + +class AttributeEqualityConstraint(ConstraintMixin): + """ + This constraint checks the values of a given attribute name on this + object. This is only relevant for ZODB Property Sheets (filesystem + Property Sheets rely on Products.ERP5Type.Constraint.PropertyExistence + instead). Note that the attribute expected value is now a TALES + Expression to be able to use any Python type and not only strings. + + For example, if we would like to check whether the attribute 'title' + has 'ObjectTitle' as its value, we would create a 'Attribute + Equality Constraint' within that Property Sheet and set 'title' as + the 'Attribute Name' and 'python: "ObjectTitle"' as the 'Attribute + Value', then set the 'Predicate' if necessary (known as 'condition' + for filesystem Property Sheets). + """ + meta_type = 'ERP5 Attribute Equality Constraint' + portal_type = 'Attribute Equality Constraint' + + # Declarative security + security = ClassSecurityInfo() + security.declareObjectProtected(Permissions.AccessContentsInformation) + + property_sheets = (PropertySheet.SimpleItem, + PropertySheet.Predicate, + PropertySheet.Reference, + PropertySheet.AttributeEqualityConstraint) + + # Define by default error messages + _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)" + + security.declareProtected(Permissions.AccessContentsInformation, + 'checkConsistency') + def checkConsistency(self, obj, fixit=False): + """ + Check the object's consistency. + """ + if not self.test(obj): + return [] + + attribute_name = self.getConstraintAttributeName() + + # If property does not exist, error will be raised by + # PropertyExistence Constraint, but the value has to be set at + # least once as there is no need to perform any check if it is the + # default value + if obj.hasProperty(attribute_name): + identical = True + + # The expected value of the attribute is a TALES Expression + attribute_expected_value_expression = Expression( + self.getConstraintAttributeValue()) + + attribute_expected_value = attribute_expected_value_expression( + createExpressionContext(obj)) + + attribute_value = obj.getProperty(attribute_name) + + if isinstance(attribute_expected_value, (list, tuple)): + # List type + if len(attribute_value) != len(attribute_expected_value): + identical = False + else: + for item in attribute_value: + if item not in attribute_expected_value: + identical = False + break + else: + # Other primitive type + identical = (attribute_expected_value == attribute_value) + + if not identical: + # Generate error and fix it if required + if fixit: + obj._setProperty(attribute_name, attribute_expected_value) + message_id = 'message_invalid_attribute_value_fixed' + else: + message_id = 'message_invalid_attribute_value' + + error = self._generateError( + obj, self._getMessage(message_id), + dict(attribute_name=attribute_name, + attribute_value=attribute_value, + expected_value=attribute_expected_value)) + + return [error] + + return [] diff --git a/product/ERP5Type/PropertySheet/AttributeEqualityConstraint.py b/product/ERP5Type/PropertySheet/AttributeEqualityConstraint.py new file mode 100644 index 0000000000..2645c87f5b --- /dev/null +++ b/product/ERP5Type/PropertySheet/AttributeEqualityConstraint.py @@ -0,0 +1,46 @@ +############################################################################## +# +# Copyright (c) 2010 Nexedi SARL and Contributors. All Rights Reserved. +# Arnaud Fontaine <arnaud.fontaine@nexedi.com> +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability of assessing all potential +# consequences resulting from its eventual inadequacies and bugs +# End users who are looking for a ready-to-use solution with commercial +# garantees and support are strongly adviced to contract a Free Software +# Service Company +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +############################################################################## + +class AttributeEqualityConstraint: + """ + Define an Attribute Equality Constraint for ZODB Property Sheets + """ + _properties = ( + { 'id': 'constraint_attribute_name', + 'type': 'string', + 'description' : 'Attribute name whose values are checked' }, + { 'id': 'constraint_attribute_value', + 'type': 'string', + 'description' : 'Valid values of the Attribute' }, + { 'id': 'message_invalid_attribute_value', + 'type': 'string', + 'description' : 'Error message when the attribute value is invalid' }, + { 'id': 'message_invalid_attribute_value_fixed', + 'type': 'string', + 'description' : 'Error message when the attribute value is invalid but has been fixed' }, + ) diff --git a/product/ERP5Type/PropertySheet/__init__.py b/product/ERP5Type/PropertySheet/__init__.py index 1084cacf17..c40d7e1e68 100644 --- a/product/ERP5Type/PropertySheet/__init__.py +++ b/product/ERP5Type/PropertySheet/__init__.py @@ -19,3 +19,4 @@ from AcquiredProperty import AcquiredProperty from DynamicCategoryProperty import DynamicCategoryProperty from CategoryExistenceConstraint import CategoryExistenceConstraint from PropertyExistenceConstraint import PropertyExistenceConstraint +from AttributeEqualityConstraint import AttributeEqualityConstraint diff --git a/product/ERP5Type/mixin/constraint.py b/product/ERP5Type/mixin/constraint.py index 732013ab2c..2ae8ecdbe9 100644 --- a/product/ERP5Type/mixin/constraint.py +++ b/product/ERP5Type/mixin/constraint.py @@ -42,6 +42,10 @@ class ConstraintMixin(Predicate): Mixin Constraint implementation (only relevant for ZODB Property sheets, use Products.ERP5Type.Constraint instead for filesystem Property Sheets) relying on Predicate + + @todo: Add code to import constraints requiring a new TALES + Expression field in predicate to be able to import + 'condition' properly """ # Declarative security security = ClassSecurityInfo() diff --git a/product/ERP5Type/tests/testDynamicClassGeneration.py b/product/ERP5Type/tests/testDynamicClassGeneration.py index 989b51dfe0..6a836a2162 100644 --- a/product/ERP5Type/tests/testDynamicClassGeneration.py +++ b/product/ERP5Type/tests/testDynamicClassGeneration.py @@ -284,7 +284,10 @@ class TestZodbPropertySheet(ERP5TypeTestCase): id=base_category_id, portal_type='Base Category') # Create a dummy sub-category - new_base_category.newContent(reference='sub_category', + new_base_category.newContent(reference='sub_category1', + portal_type='Category') + + new_base_category.newContent(reference='sub_category2', portal_type='Category') if operation_type == 'change': @@ -343,6 +346,25 @@ class TestZodbPropertySheet(ERP5TypeTestCase): # XXX # constraint_portal_type=('TODO',)) + def _newAttributeEqualityConstraint(self): + """ + Create a new Attribute Equality Constraint within test Property + Sheet + """ + # For testing primitive type as attribute value + self.test_property_sheet.newContent( + reference='test_attribute_equality_constraint', + portal_type='Attribute Equality Constraint', + constraint_attribute_name='title', + constraint_attribute_value='python: "my_valid_title"') + + # For testing list type as attribute value + self.test_property_sheet.newContent( + reference='test_attribute_list_equality_constraint', + portal_type='Attribute Equality Constraint', + constraint_attribute_name='categories_list', + constraint_attribute_value='python: ("sub_category1", "sub_category2")') + def afterSetUp(self): """ Create a test Property Sheet (and its properties) @@ -369,6 +391,10 @@ class TestZodbPropertySheet(ERP5TypeTestCase): # Sheet self._newCategoryExistenceConstraint() + # Create an Attribute Equality Constraint in the test Property + # Sheet + self._newAttributeEqualityConstraint() + # Create all the test Properties for operation_type in ('change', 'delete', 'assign'): self._newStandardProperty(operation_type) @@ -503,19 +529,19 @@ class TestZodbPropertySheet(ERP5TypeTestCase): # Category Property self.assertHasAttribute(new_person, 'setTestCategoryPropertyAssign') - new_person.setTestCategoryPropertyAssign('sub_category') + new_person.setTestCategoryPropertyAssign('sub_category1') self.assertEquals(new_person.getTestCategoryPropertyAssign(), - 'sub_category') + 'sub_category1') # Dynamic Category Property self.assertHasAttribute(new_person, 'setTestDynamicCategoryPropertyAssign') - new_person.setTestDynamicCategoryPropertyAssign('sub_category') + new_person.setTestDynamicCategoryPropertyAssign('sub_category1') self.assertEquals(new_person.getTestDynamicCategoryPropertyAssign(), - 'sub_category') + 'sub_category1') finally: # Perform a commit here because Workflow interactions keeps a @@ -729,10 +755,6 @@ class TestZodbPropertySheet(ERP5TypeTestCase): constraint_reference, setter_function, value): - """ - Check whether the given constraint has been properly defined and - checkConsistency() is correct - """ constraint = self._getConstraintByReference(constraint_reference) self.failIfEqual(None, constraint) self.assertEquals(1, len(constraint.checkConsistency(self.test_module))) @@ -745,9 +767,8 @@ class TestZodbPropertySheet(ERP5TypeTestCase): Take the test module and check whether the Property Existence Constraint is available. Until the property has been set to a value, the constraint should fail - - @see ERP5Type.Base.Base.hasProperty """ + # See ERP5Type.Base.Base.hasProperty() self._checkConstraint('test_property_existence_constraint', self.test_module.setTestStandardPropertyConstraint, 'foobar') @@ -760,7 +781,38 @@ class TestZodbPropertySheet(ERP5TypeTestCase): """ self._checkConstraint('test_category_existence_constraint', self.test_module.setTestCategoryPropertyConstraint, - 'sub_category') + 'sub_category1') + + def testAttributeEqualityConstraint(self): + """ + Take the test module and check whether the Attribute Equality + Constraint is available. Until the attribute to be checked has + been set to its expected value, the constraint should fail. The + purpose is to test only primitive types (e.g. not list) + """ + # As checkConsistency calls hasProperty before checking the value, + # the property to be tested has to be set at least once (whatever + # the value) + self.test_module.setTitle('invalid_value') + + self._checkConstraint('test_attribute_equality_constraint', + self.test_module.setTitle, + 'my_valid_title') + + def testAttributeListEqualityConstraint(self): + """ + Take the test module and check whether the Attribute Equality + Constraint is available. Until the attribute to be checked has + been set to its expected value (a list of categories), the + constraint should fail. The purpose is to test only list types + + @see testAttributeEqualityConstraint + """ + self.test_module.setCategoryList(('sub_category1',)) + + self._checkConstraint('test_attribute_list_equality_constraint', + self.test_module.setCategoryList, + ('sub_category1', 'sub_category2')) TestZodbPropertySheet = skip("ZODB Property Sheets code is not enabled yet")( TestZodbPropertySheet) -- 2.30.9