Commit 68f965c5 authored by Sylvain Thénault's avatar Sylvain Thénault
Browse files

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

--HG--
branch : stable
parent 5c84c6df6798
......@@ -21,9 +21,10 @@ from logilab.common.textutils import splitstrip
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 @@ def _generate_schema_pot(w, vreg, schema, libconfig=None, cube=None):
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
......
......@@ -119,6 +119,37 @@ def display_name(req, key, form='', context=None):
__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 @@ class CubicWebSchema(Schema):
# 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 @@ class RQLVocabularyConstraint(BaseConstraint):
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 @@ class RQLConstraint(RQLVocabularyConstraint):
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 @@ class RQLConstraint(RQLVocabularyConstraint):
'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):
"""the unique rql constraint check that the result of the query isn't
greater than one
class RQLConstraint(RepoEnforcedRQLConstraintMixIn, RQLVocabularyConstraint):
"""the rql constraint is similar to the RQLVocabularyConstraint but
are also enforced at the repository level
"""
distinct_query = True
distinct_query = False
def exec_query(self, session, eidfrom, eidto):
rset = super(RQLUniqueConstraint, self).exec_query(session, eidfrom, eidto)
return len(rset) <= 1
def match_condition(self, session, eidfrom, eidto):
return self.exec_query(session, eidfrom, eidto)
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)
class RQLUniqueConstraint(RepoEnforcedRQLConstraintMixIn, BaseRQLConstraint):
"""the unique rql constraint check that the result of the query isn't
greater than one
"""
return u', '.join(' '.join(expr.split()) for expr in rqlstring.split(','))
distinct_query = True
def match_condition(self, session, eidfrom, eidto):
return len(self.exec_query(session, eidfrom, eidto)) <= 1
class RQLExpression(object):
......@@ -848,20 +876,6 @@ class ERQLExpression(RQLExpression):
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 @@ class RRQLExpression(RQLExpression):
kwargs['o'] = toeid
return self._check(session, **kwargs)
PyFileReader.context['RRQLExpression'] = yobsolete(RRQLExpression)
# workflow extensions #########################################################
......@@ -946,7 +959,6 @@ class WorkflowableEntityType(ybo.EntityType):
__metaclass__ = workflowable_definition
__abstract__ = True
PyFileReader.context['WorkflowableEntityType'] = WorkflowableEntityType
# schema loading ##############################################################
......@@ -1067,6 +1079,11 @@ def bw_set_statement_type(self, etype):
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
......@@ -11,7 +11,7 @@ __docformat__ = "restructuredtext en"
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 @@ def cstrcheck_after_add_relation(session, eidfrom, rtype, eidto):
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 @@ def uniquecstrcheck_before_modification(session, entity):
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 @@ def cstrcheck_after_update_attributes(session, entity):
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)
......
......@@ -55,13 +55,13 @@ class Schema2RQLTC(TestCase):
{'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):
......
......@@ -18,9 +18,11 @@ from yams.constraints import SizeConstraint, StaticVocabularyConstraint
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 @@ for rel in RELS:
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
"""
......
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