schema.py
changeset 3998 94cc7cad3d2d
parent 3890 d7a270f50f54
parent 3985 d5bf894fcf02
child 4003 b9436fe77c9e
equal deleted inserted replaced
3895:92ead039d3d0 3998:94cc7cad3d2d
   117         return unicode(req.pgettext(context, key)).lower()
   117         return unicode(req.pgettext(context, key)).lower()
   118     else:
   118     else:
   119         return unicode(req._(key)).lower()
   119         return unicode(req._(key)).lower()
   120 
   120 
   121 __builtins__['display_name'] = deprecated('[3.4] display_name should be imported from cubicweb.schema')(display_name)
   121 __builtins__['display_name'] = deprecated('[3.4] display_name should be imported from cubicweb.schema')(display_name)
       
   122 
       
   123 
       
   124 # rql expression utilities function ############################################
       
   125 
       
   126 def guess_rrqlexpr_mainvars(expression):
       
   127     defined = set(split_expression(expression))
       
   128     mainvars = []
       
   129     if 'S' in defined:
       
   130         mainvars.append('S')
       
   131     if 'O' in defined:
       
   132         mainvars.append('O')
       
   133     if 'U' in defined:
       
   134         mainvars.append('U')
       
   135     if not mainvars:
       
   136         raise Exception('unable to guess selection variables')
       
   137     return ','.join(mainvars)
       
   138 
       
   139 def split_expression(rqlstring):
       
   140     for expr in rqlstring.split(','):
       
   141         for word in expr.split():
       
   142             yield word
       
   143 
       
   144 def normalize_expression(rqlstring):
       
   145     """normalize an rql expression to ease schema synchronization (avoid
       
   146     suppressing and reinserting an expression if only a space has been added/removed
       
   147     for instance)
       
   148     """
       
   149     return u', '.join(' '.join(expr.split()) for expr in rqlstring.split(','))
       
   150 
       
   151 
       
   152 # Schema objects definition ###################################################
   122 
   153 
   123 def ERSchema_display_name(self, req, form='', context=None):
   154 def ERSchema_display_name(self, req, form='', context=None):
   124     """return a internationalized string for the entity/relation type name in
   155     """return a internationalized string for the entity/relation type name in
   125     a given form
   156     a given form
   126     """
   157     """
   269                 msg = "can't use ERQLExpression on %s, use a RRQLExpression"
   300                 msg = "can't use ERQLExpression on %s, use a RRQLExpression"
   270                 raise BadSchemaDefinition(msg % self)
   301                 raise BadSchemaDefinition(msg % self)
   271 RelationDefinitionSchema.check_permission_definitions = check_permission_definitions
   302 RelationDefinitionSchema.check_permission_definitions = check_permission_definitions
   272 
   303 
   273 
   304 
   274 def system_etypes(schema):
       
   275     """return system entity types only: skip final, schema and application entities
       
   276     """
       
   277     for eschema in schema.entities():
       
   278         if eschema.final or eschema.schema_entity():
       
   279             continue
       
   280         yield eschema.type
       
   281 
       
   282 # Schema objects definition ###################################################
       
   283 
       
   284 class CubicWebEntitySchema(EntitySchema):
   305 class CubicWebEntitySchema(EntitySchema):
   285     """a entity has a type, a set of subject and or object relations
   306     """a entity has a type, a set of subject and or object relations
   286     the entity schema defines the possible relations for a given type and some
   307     the entity schema defines the possible relations for a given type and some
   287     constraints on those relations
   308     constraints on those relations
   288     """
   309     """
   534         return self._eid_index[eid]
   555         return self._eid_index[eid]
   535 
   556 
   536 
   557 
   537 # Possible constraints ########################################################
   558 # Possible constraints ########################################################
   538 
   559 
   539 class RQLVocabularyConstraint(BaseConstraint):
   560 class BaseRQLConstraint(BaseConstraint):
   540     """the rql vocabulary constraint :
   561     """base class for rql constraints
   541 
   562     """
   542     limit the proposed values to a set of entities returned by a rql query,
   563 
   543     but this is not enforced at the repository level
   564     def __init__(self, restriction, mainvars=None):
   544 
   565         self.restriction = normalize_expression(restriction)
   545      restriction is additional rql restriction that will be added to
   566         if mainvars is None:
   546      a predefined query, where the S and O variables respectivly represent
   567             mainvars = guess_rrqlexpr_mainvars(restriction)
   547      the subject and the object of the relation
   568         else:
   548     """
   569             normmainvars = []
   549 
   570             for mainvar in mainvars.split(','):
   550     def __init__(self, restriction):
   571                 mainvar = mainvar.strip()
   551         self.restriction = restriction
   572                 if not mainvar.isalpha():
       
   573                     raise Exception('bad mainvars %s' % mainvars)
       
   574                 normmainvars.append(mainvar)
       
   575             assert mainvars, 'bad mainvars %s' % mainvars
       
   576             mainvars = ','.join(sorted(normmainvars))
       
   577         self.mainvars = mainvars
   552 
   578 
   553     def serialize(self):
   579     def serialize(self):
   554         return self.restriction
   580         # start with a comma for bw compat, see below
       
   581         return ';' + self.mainvars + ';' + self.restriction
   555 
   582 
   556     def deserialize(cls, value):
   583     def deserialize(cls, value):
   557         return cls(value)
   584         # XXX < 3.5.10 bw compat
       
   585         if not value.startswith(';'):
       
   586             return cls(value)
       
   587         _, mainvars, restriction = value.split(';', 2)
       
   588         return cls(restriction, mainvars)
   558     deserialize = classmethod(deserialize)
   589     deserialize = classmethod(deserialize)
   559 
   590 
   560     def check(self, entity, rtype, value):
   591     def check(self, entity, rtype, value):
   561         """return true if the value satisfy the constraint, else false"""
   592         """return true if the value satisfy the constraint, else false"""
   562         # implemented as a hook in the repository
   593         # implemented as a hook in the repository
   566         """raise ValidationError if the relation doesn't satisfy the constraint
   597         """raise ValidationError if the relation doesn't satisfy the constraint
   567         """
   598         """
   568         pass # this is a vocabulary constraint, not enforce XXX why?
   599         pass # this is a vocabulary constraint, not enforce XXX why?
   569 
   600 
   570     def __str__(self):
   601     def __str__(self):
   571         return self.restriction
   602         return '%s(Any %s WHERE %s)' % (self.__class__.__name__, self.mainvars,
       
   603                                         self.restriction)
   572 
   604 
   573     def __repr__(self):
   605     def __repr__(self):
   574         return '<%s : %s>' % (self.__class__.__name__, repr(self.restriction))
   606         return '<%s @%#x>' % (self.__str__(), id(self))
   575 
   607 
   576 
   608 
   577 class RQLConstraint(RQLVocabularyConstraint):
   609 class RQLVocabularyConstraint(BaseRQLConstraint):
   578     """the rql constraint is similar to the RQLVocabularyConstraint but
   610     """the rql vocabulary constraint :
   579     are also enforced at the repository level
   611 
   580     """
   612     limit the proposed values to a set of entities returned by a rql query,
   581     def exec_query(self, session, eidfrom, eidto):
   613     but this is not enforced at the repository level
   582         if eidto is None:
   614 
   583             rql = 'Any S WHERE S eid %(s)s, ' + self.restriction
   615      restriction is additional rql restriction that will be added to
   584             return session.unsafe_execute(rql, {'s': eidfrom}, 's',
   616      a predefined query, where the S and O variables respectivly represent
   585                                           build_descr=False)
   617      the subject and the object of the relation
   586         rql = 'Any S,O WHERE S eid %(s)s, O eid %(o)s, ' + self.restriction
   618 
   587         return session.unsafe_execute(rql, {'s': eidfrom, 'o': eidto},
   619      mainvars is a string that should be used as selection variable (eg
   588                                       ('s', 'o'), build_descr=False)
   620      `'Any %s WHERE ...' % mainvars`). If not specified, an attempt will be
   589     def error(self, eid, rtype, msg):
   621      done to guess it according to variable used in the expression.
   590         raise ValidationError(eid, {rtype: msg})
   622     """
       
   623 
       
   624 
       
   625 class RepoEnforcedRQLConstraintMixIn(object):
       
   626 
       
   627     def __init__(self, restriction, mainvars=None, msg=None):
       
   628         super(RepoEnforcedRQLConstraintMixIn, self).__init__(restriction, mainvars)
       
   629         self.msg = msg
       
   630 
       
   631     def serialize(self):
       
   632         # start with a semicolon for bw compat, see below
       
   633         return ';%s;%s\n%s' % (self.mainvars, self.restriction,
       
   634                                self.msg or '')
       
   635 
       
   636     def deserialize(cls, value):
       
   637         # XXX < 3.5.10 bw compat
       
   638         if not value.startswith(';'):
       
   639             return cls(value)
       
   640         value, msg = value.split('\n', 1)
       
   641         _, mainvars, restriction = value.split(';', 2)
       
   642         return cls(restriction, mainvars, msg)
       
   643     deserialize = classmethod(deserialize)
   591 
   644 
   592     def repo_check(self, session, eidfrom, rtype, eidto=None):
   645     def repo_check(self, session, eidfrom, rtype, eidto=None):
   593         """raise ValidationError if the relation doesn't satisfy the constraint
   646         """raise ValidationError if the relation doesn't satisfy the constraint
   594         """
   647         """
   595         if not self.exec_query(session, eidfrom, eidto):
   648         if not self.match_condition(session, eidfrom, eidto):
   596             # XXX at this point dunno if the validation error `occured` on
   649             # XXX at this point if both or neither of S and O are in mainvar we
   597             #     eidfrom or eidto (from user interface point of view)
   650             # dunno if the validation error `occured` on eidfrom or eidto (from
   598             self.error(eidfrom, rtype, 'constraint %s failed' % self)
   651             # user interface point of view)
   599 
   652             if eidto is None or 'S' in self.mainvars or not 'O' in self.mainvars:
   600 
   653                 maineid = eidfrom
   601 class RQLUniqueConstraint(RQLConstraint):
   654             else:
       
   655                 maineid = eidto
       
   656             if self.msg:
       
   657                 msg = session._(self.msg)
       
   658             else:
       
   659                 msg = '%(constraint)s %(restriction)s failed' % {
       
   660                     'constraint':  session._(self.type()),
       
   661                     'restriction': self.restriction}
       
   662             raise ValidationError(maineid, {rtype: msg})
       
   663 
       
   664     def exec_query(self, session, eidfrom, eidto):
       
   665         if eidto is None:
       
   666             # checking constraint for an attribute relation
       
   667             restriction = 'S eid %(s)s, ' + self.restriction
       
   668             args, ck = {'s': eidfrom}, 's'
       
   669         else:
       
   670             restriction = 'S eid %(s)s, O eid %(o)s, ' + self.restriction
       
   671             args, ck = {'s': eidfrom, 'o': eidto}, ('s', 'o')
       
   672         rql = 'Any %s WHERE %s' % (self.mainvars,  restriction)
       
   673         if self.distinct_query:
       
   674             rql = 'DISTINCT ' + rql
       
   675         return session.unsafe_execute(rql, args, ck, build_descr=False)
       
   676 
       
   677 
       
   678 class RQLConstraint(RepoEnforcedRQLConstraintMixIn, RQLVocabularyConstraint):
       
   679     """the rql constraint is similar to the RQLVocabularyConstraint but
       
   680     are also enforced at the repository level
       
   681     """
       
   682     distinct_query = False
       
   683 
       
   684     def match_condition(self, session, eidfrom, eidto):
       
   685         return self.exec_query(session, eidfrom, eidto)
       
   686 
       
   687 
       
   688 class RQLUniqueConstraint(RepoEnforcedRQLConstraintMixIn, BaseRQLConstraint):
   602     """the unique rql constraint check that the result of the query isn't
   689     """the unique rql constraint check that the result of the query isn't
   603     greater than one
   690     greater than one
   604     """
   691     """
   605     def repo_check(self, session, eidfrom, rtype, eidto=None):
   692     distinct_query = True
   606         """raise ValidationError if the relation doesn't satisfy the constraint
   693 
   607         """
   694     # XXX turns mainvars into a required argument in __init__, since we've no
   608         if len(self.exec_query(session, eidfrom, eidto)) > 1:
   695     #     way to guess it correctly (eg if using S,O or U the constraint will
   609             # XXX at this point dunno if the validation error `occured` on
   696     #     always be satisfied since we've to use a DISTINCT query)
   610             #     eidfrom or eidto (from user interface point of view)
   697 
   611             self.error(eidfrom, rtype, 'unique constraint %s failed' % self)
   698     def match_condition(self, session, eidfrom, eidto):
   612 
   699         return len(self.exec_query(session, eidfrom, eidto)) <= 1
   613 
       
   614 def split_expression(rqlstring):
       
   615     for expr in rqlstring.split(','):
       
   616         for word in expr.split():
       
   617             yield word
       
   618 
       
   619 def normalize_expression(rqlstring):
       
   620     """normalize an rql expression to ease schema synchronization (avoid
       
   621     suppressing and reinserting an expression if only a space has been added/removed
       
   622     for instance)
       
   623     """
       
   624     return u', '.join(' '.join(expr.split()) for expr in rqlstring.split(','))
       
   625 
   700 
   626 
   701 
   627 class RQLExpression(object):
   702 class RQLExpression(object):
   628     def __init__(self, expression, mainvars, eid):
   703     def __init__(self, expression, mainvars, eid):
   629         self.eid = eid # eid of the entity representing this rql expression
   704         self.eid = eid # eid of the entity representing this rql expression
   791             if eid is None:
   866             if eid is None:
   792                 return False
   867                 return False
   793             return self._check(session, x=eid)
   868             return self._check(session, x=eid)
   794         return self._check(session)
   869         return self._check(session)
   795 
   870 
   796 PyFileReader.context['ERQLExpression'] = yobsolete(ERQLExpression)
       
   797 
   871 
   798 class RRQLExpression(RQLExpression):
   872 class RRQLExpression(RQLExpression):
   799     def __init__(self, expression, mainvars=None, eid=None):
   873     def __init__(self, expression, mainvars=None, eid=None):
   800         if mainvars is None:
   874         if mainvars is None:
   801             defined = set(split_expression(expression))
   875             mainvars = guess_rrqlexpr_mainvars(expression)
   802             mainvars = []
       
   803             if 'S' in defined:
       
   804                 mainvars.append('S')
       
   805             if 'O' in defined:
       
   806                 mainvars.append('O')
       
   807             if 'U' in defined:
       
   808                 mainvars.append('U')
       
   809             if not mainvars:
       
   810                 raise Exception('unable to guess selection variables')
       
   811             mainvars = ','.join(mainvars)
       
   812         RQLExpression.__init__(self, expression, mainvars, eid)
   876         RQLExpression.__init__(self, expression, mainvars, eid)
   813         # graph of links between variable, used by rql rewriter
   877         # graph of links between variable, used by rql rewriter
   814         self.vargraph = {}
   878         self.vargraph = {}
   815         for relation in self.rqlst.get_nodes(nodes.Relation):
   879         for relation in self.rqlst.get_nodes(nodes.Relation):
   816             try:
   880             try:
   849             if toeid is None:
   913             if toeid is None:
   850                 return False
   914                 return False
   851             kwargs['o'] = toeid
   915             kwargs['o'] = toeid
   852         return self._check(session, **kwargs)
   916         return self._check(session, **kwargs)
   853 
   917 
   854 PyFileReader.context['RRQLExpression'] = yobsolete(RRQLExpression)
       
   855 
   918 
   856 # workflow extensions #########################################################
   919 # workflow extensions #########################################################
   857 
   920 
   858 from yams.buildobjs import _add_relation as yams_add_relation
   921 from yams.buildobjs import _add_relation as yams_add_relation
   859 
   922 
   886 
   949 
   887 class WorkflowableEntityType(ybo.EntityType):
   950 class WorkflowableEntityType(ybo.EntityType):
   888     __metaclass__ = workflowable_definition
   951     __metaclass__ = workflowable_definition
   889     __abstract__ = True
   952     __abstract__ = True
   890 
   953 
   891 PyFileReader.context['WorkflowableEntityType'] = WorkflowableEntityType
       
   892 
   954 
   893 # schema loading ##############################################################
   955 # schema loading ##############################################################
   894 
   956 
   895 CONSTRAINTS['RQLConstraint'] = RQLConstraint
   957 CONSTRAINTS['RQLConstraint'] = RQLConstraint
   896 CONSTRAINTS['RQLUniqueConstraint'] = RQLUniqueConstraint
   958 CONSTRAINTS['RQLUniqueConstraint'] = RQLUniqueConstraint
   897 CONSTRAINTS['RQLVocabularyConstraint'] = RQLVocabularyConstraint
   959 CONSTRAINTS['RQLVocabularyConstraint'] = RQLVocabularyConstraint
       
   960 CONSTRAINTS.pop('MultipleStaticVocabularyConstraint', None) # don't want this in cw yams schema
   898 PyFileReader.context.update(CONSTRAINTS)
   961 PyFileReader.context.update(CONSTRAINTS)
   899 
   962 
   900 
   963 
   901 class BootstrapSchemaLoader(SchemaLoader):
   964 class BootstrapSchemaLoader(SchemaLoader):
   902     """cubicweb specific schema loader, loading only schema necessary to read
   965     """cubicweb specific schema loader, loading only schema necessary to read
  1007 def bw_set_statement_type(self, etype):
  1070 def bw_set_statement_type(self, etype):
  1008     return orig_set_statement_type(self, bw_normalize_etype(etype))
  1071     return orig_set_statement_type(self, bw_normalize_etype(etype))
  1009 stmts.Select.set_statement_type = bw_set_statement_type
  1072 stmts.Select.set_statement_type = bw_set_statement_type
  1010 
  1073 
  1011 # XXX deprecated
  1074 # XXX deprecated
       
  1075 
  1012 from yams.constraints import format_constraint
  1076 from yams.constraints import format_constraint
  1013 format_constraint = deprecated('[3.4] use RichString instead of format_constraint')(format_constraint)
  1077 format_constraint = deprecated('[3.4] use RichString instead of format_constraint')(format_constraint)
  1014 from yams.buildobjs import RichString
  1078 from yams.buildobjs import RichString
       
  1079 
       
  1080 PyFileReader.context['ERQLExpression'] = yobsolete(ERQLExpression)
       
  1081 PyFileReader.context['RRQLExpression'] = yobsolete(RRQLExpression)
       
  1082 PyFileReader.context['WorkflowableEntityType'] = WorkflowableEntityType
  1015 PyFileReader.context['format_constraint'] = format_constraint
  1083 PyFileReader.context['format_constraint'] = format_constraint