schema.py
changeset 1808 aa09e20dd8c0
parent 1498 2c6eec0b46b9
child 1977 606923dff11b
equal deleted inserted replaced
1693:49075f57cf2c 1808:aa09e20dd8c0
     4 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     4 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
     5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
     6 """
     6 """
     7 __docformat__ = "restructuredtext en"
     7 __docformat__ = "restructuredtext en"
     8 
     8 
     9 import warnings
       
    10 import re
     9 import re
    11 from logging import getLogger
    10 from logging import getLogger
    12 
    11 from warnings import warn
    13 from logilab.common.decorators import cached, clear_cache
    12 
       
    13 from logilab.common.decorators import cached, clear_cache, monkeypatch
    14 from logilab.common.compat import any
    14 from logilab.common.compat import any
    15 
    15 
    16 from yams import BadSchemaDefinition, buildobjs as ybo
    16 from yams import BadSchemaDefinition, buildobjs as ybo
    17 from yams.schema import Schema, ERSchema, EntitySchema, RelationSchema
    17 from yams.schema import Schema, ERSchema, EntitySchema, RelationSchema
    18 from yams.constraints import BaseConstraint, StaticVocabularyConstraint
    18 from yams.constraints import BaseConstraint, StaticVocabularyConstraint
    20                          SchemaLoader)
    20                          SchemaLoader)
    21 
    21 
    22 from rql import parse, nodes, RQLSyntaxError, TypeResolverException
    22 from rql import parse, nodes, RQLSyntaxError, TypeResolverException
    23 
    23 
    24 from cubicweb import ETYPE_NAME_MAP, ValidationError, Unauthorized
    24 from cubicweb import ETYPE_NAME_MAP, ValidationError, Unauthorized
       
    25 from cubicweb import set_log_methods
       
    26 
       
    27 # XXX <3.2 bw compat
       
    28 from yams import schema
       
    29 schema.use_py_datetime()
       
    30 nodes.use_py_datetime()
    25 
    31 
    26 _ = unicode
    32 _ = unicode
    27 
    33 
    28 BASEGROUPS = ('managers', 'users', 'guests', 'owners')
    34 BASEGROUPS = ('managers', 'users', 'guests', 'owners')
    29 
    35 
    34 ybo.RTYPE_PROPERTIES += ('eid',)
    40 ybo.RTYPE_PROPERTIES += ('eid',)
    35 ybo.RDEF_PROPERTIES += ('eid',)
    41 ybo.RDEF_PROPERTIES += ('eid',)
    36 
    42 
    37 def bw_normalize_etype(etype):
    43 def bw_normalize_etype(etype):
    38     if etype in ETYPE_NAME_MAP:
    44     if etype in ETYPE_NAME_MAP:
    39         from warnings import warn
       
    40         msg = '%s has been renamed to %s, please update your code' % (
    45         msg = '%s has been renamed to %s, please update your code' % (
    41             etype, ETYPE_NAME_MAP[etype])            
    46             etype, ETYPE_NAME_MAP[etype])
    42         warn(msg, DeprecationWarning, stacklevel=4)
    47         warn(msg, DeprecationWarning, stacklevel=4)
    43         etype = ETYPE_NAME_MAP[etype]
    48         etype = ETYPE_NAME_MAP[etype]
    44     return etype
    49     return etype
    45 
    50 
    46 # monkey path yams.builder.RelationDefinition to support a new wildcard type '@'
    51 # monkey path yams.builder.RelationDefinition to support a new wildcard type '@'
    66             etypes += tuple(system_etypes(schema))
    71             etypes += tuple(system_etypes(schema))
    67         return etypes
    72         return etypes
    68     return (etype,)
    73     return (etype,)
    69 ybo.RelationDefinition._actual_types = _actual_types
    74 ybo.RelationDefinition._actual_types = _actual_types
    70 
    75 
       
    76 
       
    77 ## cubicweb provides a RichString class for convenience
       
    78 class RichString(ybo.String):
       
    79     """Convenience RichString attribute type
       
    80     The following declaration::
       
    81 
       
    82       class Card(EntityType):
       
    83           content = RichString(fulltextindexed=True, default_format='text/rest')
       
    84 
       
    85     is equivalent to::
       
    86 
       
    87       class Card(EntityType):
       
    88           content_format = String(meta=True, internationalizable=True,
       
    89                                  default='text/rest', constraints=[format_constraint])
       
    90           content  = String(fulltextindexed=True)
       
    91     """
       
    92     def __init__(self, default_format='text/plain', format_constraints=None, **kwargs):
       
    93         self.default_format = default_format
       
    94         self.format_constraints = format_constraints or [format_constraint]
       
    95         super(RichString, self).__init__(**kwargs)
       
    96 
       
    97 PyFileReader.context['RichString'] = RichString
       
    98 
       
    99 ## need to monkeypatch yams' _add_relation function to handle RichString
       
   100 yams_add_relation = ybo._add_relation
       
   101 @monkeypatch(ybo)
       
   102 def _add_relation(relations, rdef, name=None, insertidx=None):
       
   103     if isinstance(rdef, RichString):
       
   104         format_attrdef = ybo.String(meta=True, internationalizable=True,
       
   105                                     default=rdef.default_format, maxsize=50,
       
   106                                     constraints=rdef.format_constraints)
       
   107         yams_add_relation(relations, format_attrdef, name+'_format', insertidx)
       
   108     yams_add_relation(relations, rdef, name, insertidx)
       
   109 
    71 def display_name(req, key, form=''):
   110 def display_name(req, key, form=''):
    72     """return a internationalized string for the key (schema entity or relation
   111     """return a internationalized string for the key (schema entity or relation
    73     name) in a given form
   112     name) in a given form
    74     """
   113     """
    75     assert form in ('', 'plural', 'subject', 'object')
   114     assert form in ('', 'plural', 'subject', 'object')
   217         super(CubicWebEntitySchema, self).__init__(schema, edef, **kwargs)
   256         super(CubicWebEntitySchema, self).__init__(schema, edef, **kwargs)
   218         if eid is None and edef is not None:
   257         if eid is None and edef is not None:
   219             eid = getattr(edef, 'eid', None)
   258             eid = getattr(edef, 'eid', None)
   220         self.eid = eid
   259         self.eid = eid
   221         # take care: no _groups attribute when deep-copying
   260         # take care: no _groups attribute when deep-copying
   222         if getattr(self, '_groups', None): 
   261         if getattr(self, '_groups', None):
   223             for groups in self._groups.itervalues():
   262             for groups in self._groups.itervalues():
   224                 for group_or_rqlexpr in groups:
   263                 for group_or_rqlexpr in groups:
   225                     if isinstance(group_or_rqlexpr, RRQLExpression):
   264                     if isinstance(group_or_rqlexpr, RRQLExpression):
   226                         msg = "can't use RRQLExpression on an entity type, use an ERQLExpression (%s)"
   265                         msg = "can't use RRQLExpression on an entity type, use an ERQLExpression (%s)"
   227                         raise BadSchemaDefinition(msg % self.type)
   266                         raise BadSchemaDefinition(msg % self.type)
   228             
   267 
   229     def attribute_definitions(self):
   268     def attribute_definitions(self):
   230         """return an iterator on attribute definitions
   269         """return an iterator on attribute definitions
   231         
   270 
   232         attribute relations are a subset of subject relations where the
   271         attribute relations are a subset of subject relations where the
   233         object's type is a final entity
   272         object's type is a final entity
   234         
   273 
   235         an attribute definition is a 2-uple :
   274         an attribute definition is a 2-uple :
   236         * name of the relation
   275         * name of the relation
   237         * schema of the destination entity type
   276         * schema of the destination entity type
   238         """
   277         """
   239         iter = super(CubicWebEntitySchema, self).attribute_definitions()
   278         iter = super(CubicWebEntitySchema, self).attribute_definitions()
   240         for rschema, attrschema in iter:
   279         for rschema, attrschema in iter:
   241             if rschema.type == 'has_text':
   280             if rschema.type == 'has_text':
   242                 continue
   281                 continue
   243             yield rschema, attrschema
   282             yield rschema, attrschema
   244             
   283 
   245     def add_subject_relation(self, rschema):
   284     def add_subject_relation(self, rschema):
   246         """register the relation schema as possible subject relation"""
   285         """register the relation schema as possible subject relation"""
   247         super(CubicWebEntitySchema, self).add_subject_relation(rschema)
   286         super(CubicWebEntitySchema, self).add_subject_relation(rschema)
   248         self._update_has_text()
   287         self._update_has_text()
   249 
   288 
   250     def del_subject_relation(self, rtype):
   289     def del_subject_relation(self, rtype):
   251         super(CubicWebEntitySchema, self).del_subject_relation(rtype)
   290         super(CubicWebEntitySchema, self).del_subject_relation(rtype)
   252         self._update_has_text(False)
   291         self._update_has_text(False)
   253         
   292 
   254     def _update_has_text(self, need_has_text=None):
   293     def _update_has_text(self, need_has_text=None):
   255         may_need_has_text, has_has_text = False, False
   294         may_need_has_text, has_has_text = False, False
   256         for rschema in self.subject_relations():
   295         for rschema in self.subject_relations():
   257             if rschema.is_final():
   296             if rschema.is_final():
   258                 if rschema == 'has_text':
   297                 if rschema == 'has_text':
   276         if need_has_text and not has_has_text:
   315         if need_has_text and not has_has_text:
   277             rdef = ybo.RelationDefinition(self.type, 'has_text', 'String')
   316             rdef = ybo.RelationDefinition(self.type, 'has_text', 'String')
   278             self.schema.add_relation_def(rdef)
   317             self.schema.add_relation_def(rdef)
   279         elif not need_has_text and has_has_text:
   318         elif not need_has_text and has_has_text:
   280             self.schema.del_relation_def(self.type, 'has_text', 'String')
   319             self.schema.del_relation_def(self.type, 'has_text', 'String')
   281             
   320 
   282     def schema_entity(self):
   321     def schema_entity(self):
   283         """return True if this entity type is used to build the schema"""
   322         """return True if this entity type is used to build the schema"""
   284         return self.type in self.schema.schema_entity_types()
   323         return self.type in self.schema.schema_entity_types()
   285 
   324 
   286     def rich_text_fields(self):
       
   287         """return an iterator on (attribute, format attribute) of rich text field
       
   288 
       
   289         (the first tuple element containing the text and the second the text format)
       
   290         """
       
   291         for rschema, _ in self.attribute_definitions():
       
   292             if rschema.type.endswith('_format'):
       
   293                 for constraint in self.constraints(rschema):
       
   294                     if isinstance(constraint, FormatConstraint):
       
   295                         yield self.subject_relation(rschema.type[:-7]), rschema
       
   296                         break
       
   297                     
       
   298     def check_perm(self, session, action, eid=None):
   325     def check_perm(self, session, action, eid=None):
   299         # NB: session may be a server session or a request object
   326         # NB: session may be a server session or a request object
   300         user = session.user
   327         user = session.user
   301         # check user is in an allowed group, if so that's enough
   328         # check user is in an allowed group, if so that's enough
   302         # internal sessions should always stop there
   329         # internal sessions should always stop there
   308                user.owns(eid):
   335                user.owns(eid):
   309             return
   336             return
   310         # else if there is some rql expressions, check them
   337         # else if there is some rql expressions, check them
   311         if any(rqlexpr.check(session, eid)
   338         if any(rqlexpr.check(session, eid)
   312                for rqlexpr in self.get_rqlexprs(action)):
   339                for rqlexpr in self.get_rqlexprs(action)):
   313             return        
   340             return
   314         raise Unauthorized(action, str(self))
   341         raise Unauthorized(action, str(self))
   315 
   342 
   316     def rql_expression(self, expression, mainvars=None, eid=None):
   343     def rql_expression(self, expression, mainvars=None, eid=None):
   317         """rql expression factory"""
   344         """rql expression factory"""
   318         return ERQLExpression(expression, mainvars, eid)
   345         return ERQLExpression(expression, mainvars, eid)
   319     
   346 
   320 class CubicWebRelationSchema(RelationSchema):
   347 class CubicWebRelationSchema(RelationSchema):
   321     RelationSchema._RPROPERTIES['eid'] = None
   348     RelationSchema._RPROPERTIES['eid'] = None
   322     _perms_checked = False
   349     _perms_checked = False
   323     
   350 
   324     def __init__(self, schema=None, rdef=None, eid=None, **kwargs):
   351     def __init__(self, schema=None, rdef=None, eid=None, **kwargs):
   325         if rdef is not None:
   352         if rdef is not None:
   326             # if this relation is inlined
   353             # if this relation is inlined
   327             self.inlined = rdef.inlined
   354             self.inlined = rdef.inlined
   328         super(CubicWebRelationSchema, self).__init__(schema, rdef, **kwargs)
   355         super(CubicWebRelationSchema, self).__init__(schema, rdef, **kwargs)
   329         if eid is None and rdef is not None:
   356         if eid is None and rdef is not None:
   330             eid = getattr(rdef, 'eid', None)
   357             eid = getattr(rdef, 'eid', None)
   331         self.eid = eid
   358         self.eid = eid
   332                     
   359 
   333         
   360 
   334     def update(self, subjschema, objschema, rdef):
   361     def update(self, subjschema, objschema, rdef):
   335         super(CubicWebRelationSchema, self).update(subjschema, objschema, rdef)
   362         super(CubicWebRelationSchema, self).update(subjschema, objschema, rdef)
   336         if not self._perms_checked and self._groups:
   363         if not self._perms_checked and self._groups:
   337             for action, groups in self._groups.iteritems():
   364             for action, groups in self._groups.iteritems():
   338                 for group_or_rqlexpr in groups:
   365                 for group_or_rqlexpr in groups:
   348                             rqlexpr = group_or_rqlexpr
   375                             rqlexpr = group_or_rqlexpr
   349                             newrqlexprs = [x for x in self.get_rqlexprs(action) if not x is rqlexpr]
   376                             newrqlexprs = [x for x in self.get_rqlexprs(action) if not x is rqlexpr]
   350                             newrqlexprs.append(ERQLExpression(rqlexpr.expression,
   377                             newrqlexprs.append(ERQLExpression(rqlexpr.expression,
   351                                                               rqlexpr.mainvars,
   378                                                               rqlexpr.mainvars,
   352                                                               rqlexpr.eid))
   379                                                               rqlexpr.eid))
   353                             self.set_rqlexprs(action, newrqlexprs) 
   380                             self.set_rqlexprs(action, newrqlexprs)
   354                         else:
   381                         else:
   355                             msg = "can't use RRQLExpression on a final relation "\
   382                             msg = "can't use RRQLExpression on a final relation "\
   356                                   "type (eg attribute relation), use an ERQLExpression (%s)"
   383                                   "type (eg attribute relation), use an ERQLExpression (%s)"
   357                             raise BadSchemaDefinition(msg % self.type)
   384                             raise BadSchemaDefinition(msg % self.type)
   358                     elif not self.final and \
   385                     elif not self.final and \
   359                              isinstance(group_or_rqlexpr, ERQLExpression):
   386                              isinstance(group_or_rqlexpr, ERQLExpression):
   360                         msg = "can't use ERQLExpression on a relation type, use "\
   387                         msg = "can't use ERQLExpression on a relation type, use "\
   361                               "a RRQLExpression (%s)"
   388                               "a RRQLExpression (%s)"
   362                         raise BadSchemaDefinition(msg % self.type)
   389                         raise BadSchemaDefinition(msg % self.type)
   363             self._perms_checked = True
   390             self._perms_checked = True
   364             
   391 
   365     def cardinality(self, subjtype, objtype, target):
   392     def cardinality(self, subjtype, objtype, target):
   366         card = self.rproperty(subjtype, objtype, 'cardinality')
   393         card = self.rproperty(subjtype, objtype, 'cardinality')
   367         return (target == 'subject' and card[0]) or \
   394         return (target == 'subject' and card[0]) or \
   368                (target == 'object' and card[1])
   395                (target == 'object' and card[1])
   369     
   396 
   370     def schema_relation(self):
   397     def schema_relation(self):
   371         return self.type in ('relation_type', 'from_entity', 'to_entity',
   398         return self.type in ('relation_type', 'from_entity', 'to_entity',
   372                              'constrained_by', 'cstrtype')
   399                              'constrained_by', 'cstrtype')
   373     
   400 
   374     def physical_mode(self):
   401     def physical_mode(self):
   375         """return an appropriate mode for physical storage of this relation type:
   402         """return an appropriate mode for physical storage of this relation type:
   376         * 'subjectinline' if every possible subject cardinalities are 1 or ?
   403         * 'subjectinline' if every possible subject cardinalities are 1 or ?
   377         * 'objectinline' if 'subjectinline' mode is not possible but every
   404         * 'objectinline' if 'subjectinline' mode is not possible but every
   378           possible object cardinalities are 1 or ?
   405           possible object cardinalities are 1 or ?
   384     def check_perm(self, session, action, *args, **kwargs):
   411     def check_perm(self, session, action, *args, **kwargs):
   385         # NB: session may be a server session or a request object check user is
   412         # NB: session may be a server session or a request object check user is
   386         # in an allowed group, if so that's enough internal sessions should
   413         # in an allowed group, if so that's enough internal sessions should
   387         # always stop there
   414         # always stop there
   388         if session.user.matching_groups(self.get_groups(action)):
   415         if session.user.matching_groups(self.get_groups(action)):
   389             return 
   416             return
   390         # else if there is some rql expressions, check them
   417         # else if there is some rql expressions, check them
   391         if any(rqlexpr.check(session, *args, **kwargs)
   418         if any(rqlexpr.check(session, *args, **kwargs)
   392                for rqlexpr in self.get_rqlexprs(action)):
   419                for rqlexpr in self.get_rqlexprs(action)):
   393             return
   420             return
   394         raise Unauthorized(action, str(self))
   421         raise Unauthorized(action, str(self))
   397         """rql expression factory"""
   424         """rql expression factory"""
   398         if self.is_final():
   425         if self.is_final():
   399             return ERQLExpression(expression, mainvars, eid)
   426             return ERQLExpression(expression, mainvars, eid)
   400         return RRQLExpression(expression, mainvars, eid)
   427         return RRQLExpression(expression, mainvars, eid)
   401 
   428 
   402     
   429 
   403 class CubicWebSchema(Schema):
   430 class CubicWebSchema(Schema):
   404     """set of entities and relations schema defining the possible data sets
   431     """set of entities and relations schema defining the possible data sets
   405     used in an application
   432     used in an application
   406 
   433 
   407 
   434 
   408     :type name: str
   435     :type name: str
   409     :ivar name: name of the schema, usually the application identifier
   436     :ivar name: name of the schema, usually the application identifier
   410     
   437 
   411     :type base: str
   438     :type base: str
   412     :ivar base: path of the directory where the schema is defined
   439     :ivar base: path of the directory where the schema is defined
   413     """
   440     """
   414     reading_from_database = False    
   441     reading_from_database = False
   415     entity_class = CubicWebEntitySchema
   442     entity_class = CubicWebEntitySchema
   416     relation_class = CubicWebRelationSchema
   443     relation_class = CubicWebRelationSchema
   417 
   444 
   418     def __init__(self, *args, **kwargs):
   445     def __init__(self, *args, **kwargs):
   419         self._eid_index = {}
   446         self._eid_index = {}
   426         rschema.final = True
   453         rschema.final = True
   427         rschema.set_default_groups()
   454         rschema.set_default_groups()
   428         rschema = self.add_relation_type(ybo.RelationType('identity', meta=True))
   455         rschema = self.add_relation_type(ybo.RelationType('identity', meta=True))
   429         rschema.final = False
   456         rschema.final = False
   430         rschema.set_default_groups()
   457         rschema.set_default_groups()
   431         
   458 
   432     def schema_entity_types(self):
   459     def schema_entity_types(self):
   433         """return the list of entity types used to build the schema"""
   460         """return the list of entity types used to build the schema"""
   434         return frozenset(('EEType', 'ERType', 'EFRDef', 'ENFRDef',
   461         return frozenset(('CWEType', 'CWRType', 'CWAttribute', 'CWRelation',
   435                           'EConstraint', 'EConstraintType', 'RQLExpression',
   462                           'CWConstraint', 'CWConstraintType', 'RQLExpression',
   436                           # XXX those are not really "schema" entity types
   463                           # XXX those are not really "schema" entity types
   437                           #     but we usually don't want them as @* targets
   464                           #     but we usually don't want them as @* targets
   438                           'EProperty', 'EPermission', 'State', 'Transition'))
   465                           'CWProperty', 'CWPermission', 'State', 'Transition'))
   439         
   466 
   440     def add_entity_type(self, edef):
   467     def add_entity_type(self, edef):
   441         edef.name = edef.name.encode()
   468         edef.name = edef.name.encode()
   442         edef.name = bw_normalize_etype(edef.name)
   469         edef.name = bw_normalize_etype(edef.name)
   443         assert re.match(r'[A-Z][A-Za-z0-9]*[a-z]+[0-9]*$', edef.name), repr(edef.name)
   470         assert re.match(r'[A-Z][A-Za-z0-9]*[a-z]+[0-9]*$', edef.name), repr(edef.name)
   444         eschema = super(CubicWebSchema, self).add_entity_type(edef)
   471         eschema = super(CubicWebSchema, self).add_entity_type(edef)
   449             self.add_relation_def(rdef)
   476             self.add_relation_def(rdef)
   450             rdef = ybo.RelationDefinition(eschema.type, 'identity', eschema.type)
   477             rdef = ybo.RelationDefinition(eschema.type, 'identity', eschema.type)
   451             self.add_relation_def(rdef)
   478             self.add_relation_def(rdef)
   452         self._eid_index[eschema.eid] = eschema
   479         self._eid_index[eschema.eid] = eschema
   453         return eschema
   480         return eschema
   454         
   481 
   455     def add_relation_type(self, rdef):
   482     def add_relation_type(self, rdef):
   456         rdef.name = rdef.name.lower().encode()
   483         rdef.name = rdef.name.lower().encode()
   457         rschema = super(CubicWebSchema, self).add_relation_type(rdef)
   484         rschema = super(CubicWebSchema, self).add_relation_type(rdef)
   458         self._eid_index[rschema.eid] = rschema
   485         self._eid_index[rschema.eid] = rschema
   459         return rschema
   486         return rschema
   460     
   487 
   461     def add_relation_def(self, rdef):
   488     def add_relation_def(self, rdef):
   462         """build a part of a relation schema
   489         """build a part of a relation schema
   463         (i.e. add a relation between two specific entity's types)
   490         (i.e. add a relation between two specific entity's types)
   464 
   491 
   465         :type subject: str
   492         :type subject: str
   475         :param: the newly created or just completed relation schema
   502         :param: the newly created or just completed relation schema
   476         """
   503         """
   477         rdef.name = rdef.name.lower()
   504         rdef.name = rdef.name.lower()
   478         rdef.subject = bw_normalize_etype(rdef.subject)
   505         rdef.subject = bw_normalize_etype(rdef.subject)
   479         rdef.object = bw_normalize_etype(rdef.object)
   506         rdef.object = bw_normalize_etype(rdef.object)
   480         super(CubicWebSchema, self).add_relation_def(rdef)
   507         if super(CubicWebSchema, self).add_relation_def(rdef):
   481         try:
   508             try:
   482             self._eid_index[rdef.eid] = (self.eschema(rdef.subject),
   509                 self._eid_index[rdef.eid] = (self.eschema(rdef.subject),
   483                                          self.rschema(rdef.name),
   510                                              self.rschema(rdef.name),
   484                                          self.eschema(rdef.object))
   511                                              self.eschema(rdef.object))
   485         except AttributeError:
   512             except AttributeError:
   486             pass # not a serialized schema
   513                 pass # not a serialized schema
   487     
   514 
   488     def del_relation_type(self, rtype):
   515     def del_relation_type(self, rtype):
   489         rschema = self.rschema(rtype)
   516         rschema = self.rschema(rtype)
   490         self._eid_index.pop(rschema.eid, None)
   517         self._eid_index.pop(rschema.eid, None)
   491         super(CubicWebSchema, self).del_relation_type(rtype)
   518         super(CubicWebSchema, self).del_relation_type(rtype)
   492     
   519 
   493     def del_relation_def(self, subjtype, rtype, objtype):
   520     def del_relation_def(self, subjtype, rtype, objtype):
   494         for k, v in self._eid_index.items():
   521         for k, v in self._eid_index.items():
   495             if v == (subjtype, rtype, objtype):
   522             if v == (subjtype, rtype, objtype):
   496                 del self._eid_index[k]
   523                 del self._eid_index[k]
   497         super(CubicWebSchema, self).del_relation_def(subjtype, rtype, objtype)
   524         super(CubicWebSchema, self).del_relation_def(subjtype, rtype, objtype)
   498         
   525 
   499     def del_entity_type(self, etype):
   526     def del_entity_type(self, etype):
   500         eschema = self.eschema(etype)
   527         eschema = self.eschema(etype)
   501         self._eid_index.pop(eschema.eid, None)
   528         self._eid_index.pop(eschema.eid, None)
   502         # deal with has_text first, else its automatic deletion (see above)
   529         # deal with has_text first, else its automatic deletion (see above)
   503         # may trigger an error in ancestor's del_entity_type method
   530         # may trigger an error in ancestor's del_entity_type method
   504         if 'has_text' in eschema.subject_relations():
   531         if 'has_text' in eschema.subject_relations():
   505             self.del_relation_def(etype, 'has_text', 'String')
   532             self.del_relation_def(etype, 'has_text', 'String')
   506         super(CubicWebSchema, self).del_entity_type(etype)
   533         super(CubicWebSchema, self).del_entity_type(etype)
   507         
   534 
   508     def schema_by_eid(self, eid):
   535     def schema_by_eid(self, eid):
   509         return self._eid_index[eid]
   536         return self._eid_index[eid]
   510 
   537 
   511 
   538 
   512 # Possible constraints ########################################################
   539 # Possible constraints ########################################################
   514 class RQLVocabularyConstraint(BaseConstraint):
   541 class RQLVocabularyConstraint(BaseConstraint):
   515     """the rql vocabulary constraint :
   542     """the rql vocabulary constraint :
   516 
   543 
   517     limit the proposed values to a set of entities returned by a rql query,
   544     limit the proposed values to a set of entities returned by a rql query,
   518     but this is not enforced at the repository level
   545     but this is not enforced at the repository level
   519     
   546 
   520      restriction is additional rql restriction that will be added to
   547      restriction is additional rql restriction that will be added to
   521      a predefined query, where the S and O variables respectivly represent
   548      a predefined query, where the S and O variables respectivly represent
   522      the subject and the object of the relation
   549      the subject and the object of the relation
   523     """
   550     """
   524     
   551 
   525     def __init__(self, restriction):
   552     def __init__(self, restriction):
   526         self.restriction = restriction
   553         self.restriction = restriction
   527 
   554 
   528     def serialize(self):
   555     def serialize(self):
   529         return self.restriction
   556         return self.restriction
   530     
   557 
   531     def deserialize(cls, value):
   558     def deserialize(cls, value):
   532         return cls(value)
   559         return cls(value)
   533     deserialize = classmethod(deserialize)
   560     deserialize = classmethod(deserialize)
   534     
   561 
   535     def check(self, entity, rtype, value):
   562     def check(self, entity, rtype, value):
   536         """return true if the value satisfy the constraint, else false"""
   563         """return true if the value satisfy the constraint, else false"""
   537         # implemented as a hook in the repository
   564         # implemented as a hook in the repository
   538         return 1
   565         return 1
   539 
   566 
   540     def repo_check(self, session, eidfrom, rtype, eidto):
   567     def repo_check(self, session, eidfrom, rtype, eidto):
   541         """raise ValidationError if the relation doesn't satisfy the constraint
   568         """raise ValidationError if the relation doesn't satisfy the constraint
   542         """
   569         """
   543         pass # this is a vocabulary constraint, not enforce
   570         pass # this is a vocabulary constraint, not enforce
   544     
   571 
   545     def __str__(self):
   572     def __str__(self):
   546         return self.restriction
   573         return self.restriction
   547 
   574 
   548     def __repr__(self):
   575     def __repr__(self):
   549         return '<%s : %s>' % (self.__class__.__name__, repr(self.restriction))
   576         return '<%s : %s>' % (self.__class__.__name__, repr(self.restriction))
   557         rql = 'Any S,O WHERE S eid %(s)s, O eid %(o)s, ' + self.restriction
   584         rql = 'Any S,O WHERE S eid %(s)s, O eid %(o)s, ' + self.restriction
   558         return session.unsafe_execute(rql, {'s': eidfrom, 'o': eidto},
   585         return session.unsafe_execute(rql, {'s': eidfrom, 'o': eidto},
   559                                       ('s', 'o'), build_descr=False)
   586                                       ('s', 'o'), build_descr=False)
   560     def error(self, eid, rtype, msg):
   587     def error(self, eid, rtype, msg):
   561         raise ValidationError(eid, {rtype: msg})
   588         raise ValidationError(eid, {rtype: msg})
   562         
   589 
   563     def repo_check(self, session, eidfrom, rtype, eidto):
   590     def repo_check(self, session, eidfrom, rtype, eidto):
   564         """raise ValidationError if the relation doesn't satisfy the constraint
   591         """raise ValidationError if the relation doesn't satisfy the constraint
   565         """
   592         """
   566         if not self.exec_query(session, eidfrom, eidto):
   593         if not self.exec_query(session, eidfrom, eidto):
   567             # XXX at this point dunno if the validation error `occured` on
   594             # XXX at this point dunno if the validation error `occured` on
   579         if len(self.exec_query(session, eidfrom, eidto)) > 1:
   606         if len(self.exec_query(session, eidfrom, eidto)) > 1:
   580             # XXX at this point dunno if the validation error `occured` on
   607             # XXX at this point dunno if the validation error `occured` on
   581             #     eidfrom or eidto (from user interface point of view)
   608             #     eidfrom or eidto (from user interface point of view)
   582             self.error(eidfrom, rtype, 'unique constraint %s failed' % self)
   609             self.error(eidfrom, rtype, 'unique constraint %s failed' % self)
   583 
   610 
   584     
   611 
   585 def split_expression(rqlstring):
   612 def split_expression(rqlstring):
   586     for expr in rqlstring.split(','):
   613     for expr in rqlstring.split(','):
   587         for word in expr.split():
   614         for word in expr.split():
   588             yield word
   615             yield word
   589             
   616 
   590 def normalize_expression(rqlstring):
   617 def normalize_expression(rqlstring):
   591     """normalize an rql expression to ease schema synchronization (avoid
   618     """normalize an rql expression to ease schema synchronization (avoid
   592     suppressing and reinserting an expression if only a space has been added/removed
   619     suppressing and reinserting an expression if only a space has been added/removed
   593     for instance)
   620     for instance)
   594     """
   621     """
   608             raise RQLSyntaxError(expression)
   635             raise RQLSyntaxError(expression)
   609         for mainvar in mainvars.split(','):
   636         for mainvar in mainvars.split(','):
   610             if len(self.rqlst.defined_vars[mainvar].references()) <= 2:
   637             if len(self.rqlst.defined_vars[mainvar].references()) <= 2:
   611                 LOGGER.warn('You did not use the %s variable in your RQL expression %s',
   638                 LOGGER.warn('You did not use the %s variable in your RQL expression %s',
   612                             mainvar, self)
   639                             mainvar, self)
   613     
   640 
   614     def __str__(self):
   641     def __str__(self):
   615         return self.full_rql
   642         return self.full_rql
   616     def __repr__(self):
   643     def __repr__(self):
   617         return '%s(%s)' % (self.__class__.__name__, self.full_rql)
   644         return '%s(%s)' % (self.__class__.__name__, self.full_rql)
   618         
   645 
   619     def __deepcopy__(self, memo):
   646     def __deepcopy__(self, memo):
   620         return self.__class__(self.expression, self.mainvars)
   647         return self.__class__(self.expression, self.mainvars)
   621     def __getstate__(self):
   648     def __getstate__(self):
   622         return (self.expression, self.mainvars)
   649         return (self.expression, self.mainvars)
   623     def __setstate__(self, state):
   650     def __setstate__(self, state):
   624         self.__init__(*state)
   651         self.__init__(*state)
   625         
   652 
   626     @cached
   653     @cached
   627     def transform_has_permission(self):
   654     def transform_has_permission(self):
   628         found = None
   655         found = None
   629         rqlst = self.rqlst
   656         rqlst = self.rqlst
   630         for var in rqlst.defined_vars.itervalues():
   657         for var in rqlst.defined_vars.itervalues():
   664             else:
   691             else:
   665                 keyarg = None
   692                 keyarg = None
   666             rqlst.recover()
   693             rqlst.recover()
   667             return rql, found, keyarg
   694             return rql, found, keyarg
   668         return rqlst.as_string(), None, None
   695         return rqlst.as_string(), None, None
   669         
   696 
   670     def _check(self, session, **kwargs):
   697     def _check(self, session, **kwargs):
   671         """return True if the rql expression is matching the given relation
   698         """return True if the rql expression is matching the given relation
   672         between fromeid and toeid
   699         between fromeid and toeid
   673 
   700 
   674         session may actually be a request as well
   701         session may actually be a request as well
   724             except Unauthorized:
   751             except Unauthorized:
   725                 pass
   752                 pass
   726         if self.eid is not None:
   753         if self.eid is not None:
   727             session.local_perm_cache[key] = False
   754             session.local_perm_cache[key] = False
   728         return False
   755         return False
   729     
   756 
   730     @property
   757     @property
   731     def minimal_rql(self):
   758     def minimal_rql(self):
   732         return 'Any %s WHERE %s' % (self.mainvars, self.expression)
   759         return 'Any %s WHERE %s' % (self.mainvars, self.expression)
   733 
   760 
   734 
   761 
   749         if 'X' in defined:
   776         if 'X' in defined:
   750             rql += ', X eid %(x)s'
   777             rql += ', X eid %(x)s'
   751         if 'U' in defined:
   778         if 'U' in defined:
   752             rql += ', U eid %(u)s'
   779             rql += ', U eid %(u)s'
   753         return rql
   780         return rql
   754     
   781 
   755     def check(self, session, eid=None):
   782     def check(self, session, eid=None):
   756         if 'X' in self.rqlst.defined_vars:
   783         if 'X' in self.rqlst.defined_vars:
   757             if eid is None:
   784             if eid is None:
   758                 return False
   785                 return False
   759             return self._check(session, x=eid)
   786             return self._check(session, x=eid)
   760         return self._check(session)
   787         return self._check(session)
   761     
   788 
   762 PyFileReader.context['ERQLExpression'] = ERQLExpression
   789 PyFileReader.context['ERQLExpression'] = ERQLExpression
   763         
   790 
   764 class RRQLExpression(RQLExpression):
   791 class RRQLExpression(RQLExpression):
   765     def __init__(self, expression, mainvars=None, eid=None):
   792     def __init__(self, expression, mainvars=None, eid=None):
   766         if mainvars is None:
   793         if mainvars is None:
   767             defined = set(split_expression(expression))
   794             defined = set(split_expression(expression))
   768             mainvars = []
   795             mainvars = []
   788         if 'O' in defined:
   815         if 'O' in defined:
   789             rql += ', O eid %(o)s'
   816             rql += ', O eid %(o)s'
   790         if 'U' in defined:
   817         if 'U' in defined:
   791             rql += ', U eid %(u)s'
   818             rql += ', U eid %(u)s'
   792         return rql
   819         return rql
   793     
   820 
   794     def check(self, session, fromeid=None, toeid=None):
   821     def check(self, session, fromeid=None, toeid=None):
   795         kwargs = {}
   822         kwargs = {}
   796         if 'S' in self.rqlst.defined_vars:
   823         if 'S' in self.rqlst.defined_vars:
   797             if fromeid is None:
   824             if fromeid is None:
   798                 return False
   825                 return False
   800         if 'O' in self.rqlst.defined_vars:
   827         if 'O' in self.rqlst.defined_vars:
   801             if toeid is None:
   828             if toeid is None:
   802                 return False
   829                 return False
   803             kwargs['o'] = toeid
   830             kwargs['o'] = toeid
   804         return self._check(session, **kwargs)
   831         return self._check(session, **kwargs)
   805         
   832 
   806 PyFileReader.context['RRQLExpression'] = RRQLExpression
   833 PyFileReader.context['RRQLExpression'] = RRQLExpression
   807 
   834 
   808         
   835 # workflow extensions #########################################################
       
   836 
       
   837 class workflowable_definition(ybo.metadefinition):
       
   838     """extends default EntityType's metaclass to add workflow relations
       
   839     (i.e. in_state and wf_info_for).
       
   840     This is the default metaclass for WorkflowableEntityType
       
   841     """
       
   842     def __new__(mcs, name, bases, classdict):
       
   843         abstract = classdict.pop('abstract', False)
       
   844         defclass = super(workflowable_definition, mcs).__new__(mcs, name, bases, classdict)
       
   845         if not abstract:
       
   846             existing_rels = set(rdef.name for rdef in defclass.__relations__)
       
   847             if 'in_state' not in existing_rels and 'wf_info_for' not in existing_rels:
       
   848                 in_state = ybo.SubjectRelation('State', cardinality='1*',
       
   849                                                # XXX automatize this
       
   850                                                constraints=[RQLConstraint('S is ET, O state_of ET')],
       
   851                                                description=_('account state'))
       
   852                 yams_add_relation(defclass.__relations__, in_state, 'in_state')
       
   853                 wf_info_for = ybo.ObjectRelation('TrInfo', cardinality='1*', composite='object')
       
   854                 yams_add_relation(defclass.__relations__, wf_info_for, 'wf_info_for')
       
   855         return defclass
       
   856 
       
   857 class WorkflowableEntityType(ybo.EntityType):
       
   858     __metaclass__ = workflowable_definition
       
   859     abstract = True
       
   860 
       
   861 PyFileReader.context['WorkflowableEntityType'] = WorkflowableEntityType
       
   862 
   809 # schema loading ##############################################################
   863 # schema loading ##############################################################
   810 
   864 
   811 class CubicWebRelationFileReader(RelationFileReader):
   865 class CubicWebRelationFileReader(RelationFileReader):
   812     """cubicweb specific relation file reader, handling additional RQL
   866     """cubicweb specific relation file reader, handling additional RQL
   813     constraints on a relation definition
   867     constraints on a relation definition
   814     """
   868     """
   815     
   869 
   816     def handle_constraint(self, rdef, constraint_text):
   870     def handle_constraint(self, rdef, constraint_text):
   817         """arbitrary constraint is an rql expression for cubicweb"""
   871         """arbitrary constraint is an rql expression for cubicweb"""
   818         if not rdef.constraints:
   872         if not rdef.constraints:
   819             rdef.constraints = []
   873             rdef.constraints = []
   820         rdef.constraints.append(RQLVocabularyConstraint(constraint_text))
   874         rdef.constraints.append(RQLVocabularyConstraint(constraint_text))
   822     def process_properties(self, rdef, relation_def):
   876     def process_properties(self, rdef, relation_def):
   823         if 'inline' in relation_def:
   877         if 'inline' in relation_def:
   824             rdef.inlined = True
   878             rdef.inlined = True
   825         RelationFileReader.process_properties(self, rdef, relation_def)
   879         RelationFileReader.process_properties(self, rdef, relation_def)
   826 
   880 
   827         
   881 
   828 CONSTRAINTS['RQLConstraint'] = RQLConstraint
   882 CONSTRAINTS['RQLConstraint'] = RQLConstraint
   829 CONSTRAINTS['RQLUniqueConstraint'] = RQLUniqueConstraint
   883 CONSTRAINTS['RQLUniqueConstraint'] = RQLUniqueConstraint
   830 CONSTRAINTS['RQLVocabularyConstraint'] = RQLVocabularyConstraint
   884 CONSTRAINTS['RQLVocabularyConstraint'] = RQLVocabularyConstraint
   831 PyFileReader.context.update(CONSTRAINTS)
   885 PyFileReader.context.update(CONSTRAINTS)
   832 
   886 
   837     """
   891     """
   838     schemacls = CubicWebSchema
   892     schemacls = CubicWebSchema
   839     SchemaLoader.file_handlers.update({'.rel' : CubicWebRelationFileReader,
   893     SchemaLoader.file_handlers.update({'.rel' : CubicWebRelationFileReader,
   840                                        })
   894                                        })
   841 
   895 
   842     def load(self, config, path=()):
   896     def load(self, config, path=(), **kwargs):
   843         """return a Schema instance from the schema definition read
   897         """return a Schema instance from the schema definition read
   844         from <directory>
   898         from <directory>
   845         """
   899         """
   846         self.lib_directory = config.schemas_lib_dir()
   900         self.lib_directory = config.schemas_lib_dir()
   847         return super(BootstrapSchemaLoader, self).load(
   901         return super(BootstrapSchemaLoader, self).load(
   848             path, config.appid, register_base_types=False)
   902             path, config.appid, register_base_types=False, **kwargs)
   849     
   903 
   850     def _load_definition_files(self, cubes=None):
   904     def _load_definition_files(self, cubes=None):
   851         # bootstraping, ignore cubes
   905         # bootstraping, ignore cubes
   852         for filepath in self.include_schema_files('bootstrap'):
   906         for filepath in self.include_schema_files('bootstrap'):
   853             self.info('loading %s', filepath)
   907             self.info('loading %s', filepath)
   854             self.handle_file(filepath)
   908             self.handle_file(filepath)
   855         
   909 
   856     def unhandled_file(self, filepath):
   910     def unhandled_file(self, filepath):
   857         """called when a file without handler associated has been found"""
   911         """called when a file without handler associated has been found"""
   858         self.warning('ignoring file %r', filepath)
   912         self.warning('ignoring file %r', filepath)
   859 
   913 
   860 
   914 
   861 class CubicWebSchemaLoader(BootstrapSchemaLoader):
   915 class CubicWebSchemaLoader(BootstrapSchemaLoader):
   862     """cubicweb specific schema loader, automatically adding metadata to the
   916     """cubicweb specific schema loader, automatically adding metadata to the
   863     application's schema
   917     application's schema
   864     """
   918     """
   865 
   919 
   866     def load(self, config):
   920     def load(self, config, **kwargs):
   867         """return a Schema instance from the schema definition read
   921         """return a Schema instance from the schema definition read
   868         from <directory>
   922         from <directory>
   869         """
   923         """
   870         self.info('loading %s schemas', ', '.join(config.cubes()))
   924         self.info('loading %s schemas', ', '.join(config.cubes()))
   871         if config.apphome:
   925         if config.apphome:
   872             path = reversed([config.apphome] + config.cubes_path())
   926             path = reversed([config.apphome] + config.cubes_path())
   873         else:
   927         else:
   874             path = reversed(config.cubes_path())
   928             path = reversed(config.cubes_path())
   875         return super(CubicWebSchemaLoader, self).load(config, path=path)
   929         return super(CubicWebSchemaLoader, self).load(config, path=path, **kwargs)
   876 
   930 
   877     def _load_definition_files(self, cubes):
   931     def _load_definition_files(self, cubes):
   878         for filepath in (self.include_schema_files('bootstrap')
   932         for filepath in (self.include_schema_files('bootstrap')
   879                          + self.include_schema_files('base')
   933                          + self.include_schema_files('base')
       
   934                          + self.include_schema_files('workflow')
   880                          + self.include_schema_files('Bookmark')):
   935                          + self.include_schema_files('Bookmark')):
   881 #                         + self.include_schema_files('Card')):
       
   882             self.info('loading %s', filepath)
   936             self.info('loading %s', filepath)
   883             self.handle_file(filepath)
   937             self.handle_file(filepath)
   884         for cube in cubes:
   938         for cube in cubes:
   885             for filepath in self.get_schema_files(cube):
   939             for filepath in self.get_schema_files(cube):
   886                 self.info('loading %s', filepath)
   940                 self.info('loading %s', filepath)
   890 # _() is just there to add messages to the catalog, don't care about actual
   944 # _() is just there to add messages to the catalog, don't care about actual
   891 # translation
   945 # translation
   892 PERM_USE_TEMPLATE_FORMAT = _('use_template_format')
   946 PERM_USE_TEMPLATE_FORMAT = _('use_template_format')
   893 
   947 
   894 class FormatConstraint(StaticVocabularyConstraint):
   948 class FormatConstraint(StaticVocabularyConstraint):
   895     need_perm_formats = (_('text/cubicweb-page-template'),
   949     need_perm_formats = [_('text/cubicweb-page-template')]
   896                          )
   950 
   897     regular_formats = (_('text/rest'),
   951     regular_formats = (_('text/rest'),
   898                        _('text/html'),
   952                        _('text/html'),
   899                        _('text/plain'),
   953                        _('text/plain'),
   900                        )
   954                        )
   901     def __init__(self):
   955     def __init__(self):
   902         pass
   956         pass
       
   957 
   903     def serialize(self):
   958     def serialize(self):
   904         """called to make persistent valuable data of a constraint"""
   959         """called to make persistent valuable data of a constraint"""
   905         return None
   960         return None
   906 
   961 
   907     @classmethod
   962     @classmethod
   908     def deserialize(cls, value):
   963     def deserialize(cls, value):
   909         """called to restore serialized data of a constraint. Should return
   964         """called to restore serialized data of a constraint. Should return
   910         a `cls` instance
   965         a `cls` instance
   911         """
   966         """
   912         return cls()
   967         return cls()
   913     
   968 
   914     def vocabulary(self, entity=None):
   969     def vocabulary(self, entity=None, req=None):
   915         if entity and entity.req.user.has_permission(PERM_USE_TEMPLATE_FORMAT):
   970         if req is None and entity is not None:
   916             return self.regular_formats + self.need_perm_formats
   971             req = entity.req
       
   972         if req is not None and req.user.has_permission(PERM_USE_TEMPLATE_FORMAT):
       
   973             return self.regular_formats + tuple(self.need_perm_formats)
   917         return self.regular_formats
   974         return self.regular_formats
   918     
   975 
   919     def __str__(self):
   976     def __str__(self):
   920         return 'value in (%s)' % u', '.join(repr(unicode(word)) for word in self.vocabulary())
   977         return 'value in (%s)' % u', '.join(repr(unicode(word)) for word in self.vocabulary())
   921     
   978 
   922     
   979 
   923 format_constraint = FormatConstraint()
   980 format_constraint = FormatConstraint()
   924 CONSTRAINTS['FormatConstraint'] = FormatConstraint
   981 CONSTRAINTS['FormatConstraint'] = FormatConstraint
   925 PyFileReader.context['format_constraint'] = format_constraint
   982 PyFileReader.context['format_constraint'] = format_constraint
   926 
   983 
   927 from logging import getLogger
       
   928 from cubicweb import set_log_methods
       
   929 set_log_methods(CubicWebSchemaLoader, getLogger('cubicweb.schemaloader'))
   984 set_log_methods(CubicWebSchemaLoader, getLogger('cubicweb.schemaloader'))
   930 set_log_methods(BootstrapSchemaLoader, getLogger('cubicweb.bootstrapschemaloader'))
   985 set_log_methods(BootstrapSchemaLoader, getLogger('cubicweb.bootstrapschemaloader'))
   931 set_log_methods(RQLExpression, getLogger('cubicweb.schema'))
   986 set_log_methods(RQLExpression, getLogger('cubicweb.schema'))
   932 
   987 
   933 # XXX monkey patch PyFileReader.import_erschema until bw_normalize_etype is
   988 # XXX monkey patch PyFileReader.import_erschema until bw_normalize_etype is
   934 # necessary
   989 # necessary
   935 orig_import_erschema = PyFileReader.import_erschema
   990 orig_import_erschema = PyFileReader.import_erschema
   936 def bw_import_erschema(self, ertype, schemamod=None, instantiate=True):
   991 def bw_import_erschema(self, ertype, schemamod=None, instantiate=True):
   937     return orig_import_erschema(self, bw_normalize_etype(ertype), schemamod, instantiate)
   992     return orig_import_erschema(self, bw_normalize_etype(ertype), schemamod, instantiate)
   938 PyFileReader.import_erschema = bw_import_erschema
   993 PyFileReader.import_erschema = bw_import_erschema
   939     
   994 
   940 # XXX itou for some Statement methods
   995 # XXX itou for some Statement methods
   941 from rql import stmts
   996 from rql import stmts
   942 orig_get_etype = stmts.ScopeNode.get_etype
   997 orig_get_etype = stmts.ScopeNode.get_etype
   943 def bw_get_etype(self, name):
   998 def bw_get_etype(self, name):
   944     return orig_get_etype(self, bw_normalize_etype(name))
   999     return orig_get_etype(self, bw_normalize_etype(name))