# HG changeset patch # User Sylvain Thénault # Date 1259830518 -3600 # Node ID 2c95e3033f64906aab0466401952b7729dea483e # Parent 5c84c6df679822ee0731db3481a6f1a9c74a10b8 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 diff -r 5c84c6df6798 -r 2c95e3033f64 devtools/devctl.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 diff -r 5c84c6df6798 -r 2c95e3033f64 schema.py --- 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 diff -r 5c84c6df6798 -r 2c95e3033f64 server/hooks.py --- 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) diff -r 5c84c6df6798 -r 2c95e3033f64 server/test/unittest_schemaserial.py --- 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): diff -r 5c84c6df6798 -r 2c95e3033f64 test/unittest_schema.py --- 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 """