finish yesterday work on rql constraints: stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Thu, 03 Dec 2009 09:55:18 +0100
branchstable
changeset 3978 2c95e3033f64
parent 3977 5c84c6df6798
child 3979 8f1b3fbb158f
finish yesterday work on rql constraints: * Fix inheritance pb: there are some places where we filter constraints according to the class hierarchy, so rql unique constraint should'nt be neither a RQLContraint nor a RQLVocabularyConstraint subclass. Added test in unittest_schema reflecting this. * So now we have to get explicitly RQLUniqueConstraint where desired (eg in server/hooks.py) * Update i18ncubicweb command to include constraint types in generated pot file (this should have been there for a while...) * Update unittest_schemaserial which has been broken when serializing format for rql constraints has changed
devtools/devctl.py
schema.py
server/hooks.py
server/test/unittest_schemaserial.py
test/unittest_schema.py
--- a/devtools/devctl.py	Wed Dec 02 18:36:16 2009 +0100
+++ b/devtools/devctl.py	Thu Dec 03 09:55:18 2009 +0100
@@ -21,9 +21,10 @@
 from logilab.common.shellutils import ASK
 from logilab.common.clcommands import register_commands
 
+from cubicweb.__pkginfo__ import version as cubicwebversion
 from cubicweb import (CW_SOFTWARE_ROOT as BASEDIR, BadCommandUsage,
                       underline_title)
-from cubicweb.__pkginfo__ import version as cubicwebversion
+from cubicweb.schema import CONSTRAINTS
 from cubicweb.toolsutils import Command, copy_skeleton
 from cubicweb.web.webconfig import WebConfiguration
 from cubicweb.server.serverconfig import ServerConfiguration
@@ -138,6 +139,8 @@
         libschema = {}
         rinlined = uicfg.autoform_is_inlined
         appearsin_addmenu = uicfg.actionbox_appearsin_addmenu
+        for cstrtype in CONSTRAINTS:
+            add_msg(w, cstrtype)
     done = set()
     for eschema in sorted(schema.entities()):
         etype = eschema.type
--- a/schema.py	Wed Dec 02 18:36:16 2009 +0100
+++ b/schema.py	Thu Dec 03 09:55:18 2009 +0100
@@ -119,6 +119,37 @@
 
 __builtins__['display_name'] = deprecated('display_name should be imported from cubicweb.schema')(display_name)
 
+
+# rql expression utilities function ############################################
+
+def guess_rrqlexpr_mainvars(expression):
+    defined = set(split_expression(expression))
+    mainvars = []
+    if 'S' in defined:
+        mainvars.append('S')
+    if 'O' in defined:
+        mainvars.append('O')
+    if 'U' in defined:
+        mainvars.append('U')
+    if not mainvars:
+        raise Exception('unable to guess selection variables')
+    return ','.join(mainvars)
+
+def split_expression(rqlstring):
+    for expr in rqlstring.split(','):
+        for word in expr.split():
+            yield word
+
+def normalize_expression(rqlstring):
+    """normalize an rql expression to ease schema synchronization (avoid
+    suppressing and reinserting an expression if only a space has been added/removed
+    for instance)
+    """
+    return u', '.join(' '.join(expr.split()) for expr in rqlstring.split(','))
+
+
+# Schema objects definition ###################################################
+
 def ERSchema_display_name(self, req, form=''):
     """return a internationalized string for the entity/relation type name in
     a given form
@@ -543,19 +574,8 @@
 
 # Possible constraints ########################################################
 
-class RQLVocabularyConstraint(BaseConstraint):
-    """the rql vocabulary constraint :
-
-    limit the proposed values to a set of entities returned by a rql query,
-    but this is not enforced at the repository level
-
-     restriction is additional rql restriction that will be added to
-     a predefined query, where the S and O variables respectivly represent
-     the subject and the object of the relation
-
-     mainvars is a string that should be used as selection variable (eg
-     `'Any %s WHERE ...' % mainvars`). If not specified, an attempt will be
-     done to guess it according to variable used in the expression.
+class BaseRQLConstraint(BaseConstraint):
+    """base class for rql constraints
     """
 
     def __init__(self, restriction, mainvars=None):
@@ -603,14 +623,26 @@
         return '<%s @%#x>' % (self.__str__(), id(self))
 
 
-class RQLConstraint(RQLVocabularyConstraint):
-    """the rql constraint is similar to the RQLVocabularyConstraint but
-    are also enforced at the repository level
+class RQLVocabularyConstraint(BaseRQLConstraint):
+    """the rql vocabulary constraint :
+
+    limit the proposed values to a set of entities returned by a rql query,
+    but this is not enforced at the repository level
+
+     restriction is additional rql restriction that will be added to
+     a predefined query, where the S and O variables respectivly represent
+     the subject and the object of the relation
+
+     mainvars is a string that should be used as selection variable (eg
+     `'Any %s WHERE ...' % mainvars`). If not specified, an attempt will be
+     done to guess it according to variable used in the expression.
     """
-    distinct_query = False
+
+
+class RepoEnforcedRQLConstraintMixIn(object):
 
     def __init__(self, restriction, mainvars=None, msg=None):
-        super(RQLConstraint, self).__init__(restriction, mainvars)
+        super(RepoEnforcedRQLConstraintMixIn, self).__init__(restriction, mainvars)
         self.msg = msg
 
     def serialize(self):
@@ -627,23 +659,10 @@
         return cls(restriction, mainvars, msg)
     deserialize = classmethod(deserialize)
 
-    def exec_query(self, session, eidfrom, eidto):
-        if eidto is None:
-            # checking constraint for an attribute relation
-            restriction = 'S eid %(s)s, ' + self.restriction
-            args, ck = {'s': eidfrom}, 's'
-        else:
-            restriction = 'S eid %(s)s, O eid %(o)s, ' + self.restriction
-            args, ck = {'s': eidfrom, 'o': eidto}, ('s', 'o')
-        rql = 'Any %s WHERE %s' % (self.mainvars,  restriction)
-        if self.distinct_query:
-            rql = 'DISTINCT ' + rql
-        return session.unsafe_execute(rql, args, ck, build_descr=False)
-
     def repo_check(self, session, eidfrom, rtype, eidto=None):
         """raise ValidationError if the relation doesn't satisfy the constraint
         """
-        if not self.exec_query(session, eidfrom, eidto):
+        if not self.match_condition(session, eidfrom, eidto):
             # XXX at this point if both or neither of S and O are in mainvar we
             # dunno if the validation error `occured` on eidfrom or eidto (from
             # user interface point of view)
@@ -659,29 +678,38 @@
                     'restriction': self.restriction}
             raise ValidationError(maineid, {rtype: msg})
 
+    def exec_query(self, session, eidfrom, eidto):
+        if eidto is None:
+            # checking constraint for an attribute relation
+            restriction = 'S eid %(s)s, ' + self.restriction
+            args, ck = {'s': eidfrom}, 's'
+        else:
+            restriction = 'S eid %(s)s, O eid %(o)s, ' + self.restriction
+            args, ck = {'s': eidfrom, 'o': eidto}, ('s', 'o')
+        rql = 'Any %s WHERE %s' % (self.mainvars,  restriction)
+        if self.distinct_query:
+            rql = 'DISTINCT ' + rql
+        return session.unsafe_execute(rql, args, ck, build_descr=False)
 
-class RQLUniqueConstraint(RQLConstraint):
+
+class RQLConstraint(RepoEnforcedRQLConstraintMixIn, RQLVocabularyConstraint):
+    """the rql constraint is similar to the RQLVocabularyConstraint but
+    are also enforced at the repository level
+    """
+    distinct_query = False
+
+    def match_condition(self, session, eidfrom, eidto):
+        return self.exec_query(session, eidfrom, eidto)
+
+
+class RQLUniqueConstraint(RepoEnforcedRQLConstraintMixIn, BaseRQLConstraint):
     """the unique rql constraint check that the result of the query isn't
     greater than one
     """
     distinct_query = True
 
-    def exec_query(self, session, eidfrom, eidto):
-        rset = super(RQLUniqueConstraint, self).exec_query(session, eidfrom, eidto)
-        return len(rset) <= 1
-
-
-def split_expression(rqlstring):
-    for expr in rqlstring.split(','):
-        for word in expr.split():
-            yield word
-
-def normalize_expression(rqlstring):
-    """normalize an rql expression to ease schema synchronization (avoid
-    suppressing and reinserting an expression if only a space has been added/removed
-    for instance)
-    """
-    return u', '.join(' '.join(expr.split()) for expr in rqlstring.split(','))
+    def match_condition(self, session, eidfrom, eidto):
+        return len(self.exec_query(session, eidfrom, eidto)) <= 1
 
 
 class RQLExpression(object):
@@ -848,20 +876,6 @@
             return self._check(session, x=eid)
         return self._check(session)
 
-PyFileReader.context['ERQLExpression'] = yobsolete(ERQLExpression)
-
-def guess_rrqlexpr_mainvars(expression):
-    defined = set(split_expression(expression))
-    mainvars = []
-    if 'S' in defined:
-        mainvars.append('S')
-    if 'O' in defined:
-        mainvars.append('O')
-    if 'U' in defined:
-        mainvars.append('U')
-    if not mainvars:
-        raise Exception('unable to guess selection variables')
-    return ','.join(mainvars)
 
 class RRQLExpression(RQLExpression):
     def __init__(self, expression, mainvars=None, eid=None):
@@ -909,7 +923,6 @@
             kwargs['o'] = toeid
         return self._check(session, **kwargs)
 
-PyFileReader.context['RRQLExpression'] = yobsolete(RRQLExpression)
 
 # workflow extensions #########################################################
 
@@ -946,7 +959,6 @@
     __metaclass__ = workflowable_definition
     __abstract__ = True
 
-PyFileReader.context['WorkflowableEntityType'] = WorkflowableEntityType
 
 # schema loading ##############################################################
 
@@ -1067,6 +1079,11 @@
 stmts.Select.set_statement_type = bw_set_statement_type
 
 # XXX deprecated
+
 from yams.constraints import format_constraint
 from yams.buildobjs import RichString
+
+PyFileReader.context['ERQLExpression'] = yobsolete(ERQLExpression)
+PyFileReader.context['RRQLExpression'] = yobsolete(RRQLExpression)
+PyFileReader.context['WorkflowableEntityType'] = WorkflowableEntityType
 PyFileReader.context['format_constraint'] = format_constraint
--- a/server/hooks.py	Wed Dec 02 18:36:16 2009 +0100
+++ b/server/hooks.py	Thu Dec 03 09:55:18 2009 +0100
@@ -11,7 +11,7 @@
 from datetime import datetime
 
 from cubicweb import UnknownProperty, ValidationError, BadConnectionId
-from cubicweb.schema import RQLVocabularyConstraint
+from cubicweb.schema import RQLConstraint, RQLUniqueConstraint
 from cubicweb.server.pool import Operation, LateOperation, PreCommitOperation
 from cubicweb.server.hookhelper import (check_internal_entity,
                                         get_user_sessions, rproperty)
@@ -236,6 +236,7 @@
         return
     constraints = rproperty(session, rtype, eidfrom, eidto, 'constraints')
     if constraints:
+        # XXX get only RQL[Unique]Constraints?
         CheckConstraintsOperation(session, constraints=constraints,
                                   rdef=(eidfrom, rtype, eidto))
 
@@ -254,6 +255,7 @@
                 msg = session._('the value "%s" is already used, use another one')
                 raise ValidationError(entity.eid, {attr: msg % val})
 
+
 def cstrcheck_after_update_attributes(session, entity):
     if session.is_super_session:
         return
@@ -261,7 +263,7 @@
     for attr in entity.edited_attributes:
         if schema.rschema(attr).final:
             constraints = [c for c in entity.e_schema.constraints(attr)
-                           if isinstance(c, RQLVocabularyConstraint)]
+                           if isinstance(c, (RQLConstraint, RQLUniqueConstraint))]
             if constraints:
                 CheckConstraintsOperation(session, rdef=(entity.eid, attr, None),
                                           constraints=constraints)
--- a/server/test/unittest_schemaserial.py	Wed Dec 02 18:36:16 2009 +0100
+++ b/server/test/unittest_schemaserial.py	Thu Dec 03 09:55:18 2009 +0100
@@ -55,13 +55,13 @@
              {'rt': 'relation_type', 'description': u'', 'composite': u'object', 'oe': 'CWRType',
               'ordernum': 1, 'cardinality': u'1*', 'se': 'CWRelation'}),
             ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT name %(ctname)s, EDEF relation_type ER, EDEF from_entity SE, EDEF to_entity OE, ER name %(rt)s, SE name %(se)s, OE name %(oe)s, EDEF is CWRelation',
-             {'rt': 'relation_type', 'oe': 'CWRType', 'ctname': u'RQLConstraint', 'se': 'CWRelation', 'value': u'O final FALSE'}),
+             {'rt': 'relation_type', 'oe': 'CWRType', 'ctname': u'RQLConstraint', 'se': 'CWRelation', 'value': u';O;O final FALSE\n'}),
 
             ('INSERT CWRelation X: X cardinality %(cardinality)s,X composite %(composite)s,X description %(description)s,X ordernum %(ordernum)s,X relation_type ER,X from_entity SE,X to_entity OE WHERE SE name %(se)s,ER name %(rt)s,OE name %(oe)s',
              {'rt': 'relation_type', 'description': u'', 'composite': u'object', 'oe': 'CWRType',
               'ordernum': 1, 'cardinality': u'1*', 'se': 'CWAttribute'}),
             ('INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE CT name %(ctname)s, EDEF relation_type ER, EDEF from_entity SE, EDEF to_entity OE, ER name %(rt)s, SE name %(se)s, OE name %(oe)s, EDEF is CWRelation',
-             {'rt': 'relation_type', 'oe': 'CWRType', 'ctname': u'RQLConstraint', 'se': 'CWAttribute', 'value': u'O final TRUE'}),
+             {'rt': 'relation_type', 'oe': 'CWRType', 'ctname': u'RQLConstraint', 'se': 'CWAttribute', 'value': u';O;O final TRUE\n'}),
             ])
 
     def test_rschema2rql2(self):
--- a/test/unittest_schema.py	Wed Dec 02 18:36:16 2009 +0100
+++ b/test/unittest_schema.py	Thu Dec 03 09:55:18 2009 +0100
@@ -18,9 +18,11 @@
 from yams.buildobjs import RelationDefinition, EntityType, RelationType
 from yams.reader import PyFileReader
 
-from cubicweb.schema import CubicWebSchema, CubicWebEntitySchema, \
-     RQLConstraint, CubicWebSchemaLoader, ERQLExpression, RRQLExpression, \
-     normalize_expression, order_eschemas
+from cubicweb.schema import (
+    CubicWebSchema, CubicWebEntitySchema, CubicWebSchemaLoader,
+    RQLConstraint, RQLUniqueConstraint, RQLVocabularyConstraint,
+    ERQLExpression, RRQLExpression,
+    normalize_expression, order_eschemas)
 from cubicweb.devtools import TestServerConfiguration as TestConfiguration
 
 DATADIR = join(dirname(__file__), 'data')
@@ -82,6 +84,18 @@
 
 class CubicWebSchemaTC(TestCase):
 
+    def test_rql_constraints_inheritance(self):
+        # isinstance(cstr, RQLVocabularyConstraint)
+        # -> expected to return RQLVocabularyConstraint and RQLConstraint
+        #   instances but not RQLUniqueConstraint
+        #
+        # isinstance(cstr, RQLConstraint)
+        # -> expected to return RQLConstraint instances but not
+        #    RRQLVocabularyConstraint and QLUniqueConstraint
+        self.failIf(issubclass(RQLUniqueConstraint, RQLVocabularyConstraint))
+        self.failIf(issubclass(RQLUniqueConstraint, RQLConstraint))
+        self.failUnless(issubclass(RQLConstraint, RQLVocabularyConstraint))
+
     def test_normalize(self):
         """test that entities, relations and attributes name are normalized
         """