schema.py
changeset 1808 aa09e20dd8c0
parent 1498 2c6eec0b46b9
child 1977 606923dff11b
--- a/schema.py	Tue May 05 17:18:49 2009 +0200
+++ b/schema.py	Thu May 14 12:48:11 2009 +0200
@@ -6,11 +6,11 @@
 """
 __docformat__ = "restructuredtext en"
 
-import warnings
 import re
 from logging import getLogger
+from warnings import warn
 
-from logilab.common.decorators import cached, clear_cache
+from logilab.common.decorators import cached, clear_cache, monkeypatch
 from logilab.common.compat import any
 
 from yams import BadSchemaDefinition, buildobjs as ybo
@@ -22,6 +22,12 @@
 from rql import parse, nodes, RQLSyntaxError, TypeResolverException
 
 from cubicweb import ETYPE_NAME_MAP, ValidationError, Unauthorized
+from cubicweb import set_log_methods
+
+# XXX <3.2 bw compat
+from yams import schema
+schema.use_py_datetime()
+nodes.use_py_datetime()
 
 _ = unicode
 
@@ -36,9 +42,8 @@
 
 def bw_normalize_etype(etype):
     if etype in ETYPE_NAME_MAP:
-        from warnings import warn
         msg = '%s has been renamed to %s, please update your code' % (
-            etype, ETYPE_NAME_MAP[etype])            
+            etype, ETYPE_NAME_MAP[etype])
         warn(msg, DeprecationWarning, stacklevel=4)
         etype = ETYPE_NAME_MAP[etype]
     return etype
@@ -68,6 +73,40 @@
     return (etype,)
 ybo.RelationDefinition._actual_types = _actual_types
 
+
+## cubicweb provides a RichString class for convenience
+class RichString(ybo.String):
+    """Convenience RichString attribute type
+    The following declaration::
+
+      class Card(EntityType):
+          content = RichString(fulltextindexed=True, default_format='text/rest')
+
+    is equivalent to::
+
+      class Card(EntityType):
+          content_format = String(meta=True, internationalizable=True,
+                                 default='text/rest', constraints=[format_constraint])
+          content  = String(fulltextindexed=True)
+    """
+    def __init__(self, default_format='text/plain', format_constraints=None, **kwargs):
+        self.default_format = default_format
+        self.format_constraints = format_constraints or [format_constraint]
+        super(RichString, self).__init__(**kwargs)
+
+PyFileReader.context['RichString'] = RichString
+
+## need to monkeypatch yams' _add_relation function to handle RichString
+yams_add_relation = ybo._add_relation
+@monkeypatch(ybo)
+def _add_relation(relations, rdef, name=None, insertidx=None):
+    if isinstance(rdef, RichString):
+        format_attrdef = ybo.String(meta=True, internationalizable=True,
+                                    default=rdef.default_format, maxsize=50,
+                                    constraints=rdef.format_constraints)
+        yams_add_relation(relations, format_attrdef, name+'_format', insertidx)
+    yams_add_relation(relations, rdef, name, insertidx)
+
 def display_name(req, key, form=''):
     """return a internationalized string for the key (schema entity or relation
     name) in a given form
@@ -219,19 +258,19 @@
             eid = getattr(edef, 'eid', None)
         self.eid = eid
         # take care: no _groups attribute when deep-copying
-        if getattr(self, '_groups', None): 
+        if getattr(self, '_groups', None):
             for groups in self._groups.itervalues():
                 for group_or_rqlexpr in groups:
                     if isinstance(group_or_rqlexpr, RRQLExpression):
                         msg = "can't use RRQLExpression on an entity type, use an ERQLExpression (%s)"
                         raise BadSchemaDefinition(msg % self.type)
-            
+
     def attribute_definitions(self):
         """return an iterator on attribute definitions
-        
+
         attribute relations are a subset of subject relations where the
         object's type is a final entity
-        
+
         an attribute definition is a 2-uple :
         * name of the relation
         * schema of the destination entity type
@@ -241,7 +280,7 @@
             if rschema.type == 'has_text':
                 continue
             yield rschema, attrschema
-            
+
     def add_subject_relation(self, rschema):
         """register the relation schema as possible subject relation"""
         super(CubicWebEntitySchema, self).add_subject_relation(rschema)
@@ -250,7 +289,7 @@
     def del_subject_relation(self, rtype):
         super(CubicWebEntitySchema, self).del_subject_relation(rtype)
         self._update_has_text(False)
-        
+
     def _update_has_text(self, need_has_text=None):
         may_need_has_text, has_has_text = False, False
         for rschema in self.subject_relations():
@@ -278,23 +317,11 @@
             self.schema.add_relation_def(rdef)
         elif not need_has_text and has_has_text:
             self.schema.del_relation_def(self.type, 'has_text', 'String')
-            
+
     def schema_entity(self):
         """return True if this entity type is used to build the schema"""
         return self.type in self.schema.schema_entity_types()
 
-    def rich_text_fields(self):
-        """return an iterator on (attribute, format attribute) of rich text field
-
-        (the first tuple element containing the text and the second the text format)
-        """
-        for rschema, _ in self.attribute_definitions():
-            if rschema.type.endswith('_format'):
-                for constraint in self.constraints(rschema):
-                    if isinstance(constraint, FormatConstraint):
-                        yield self.subject_relation(rschema.type[:-7]), rschema
-                        break
-                    
     def check_perm(self, session, action, eid=None):
         # NB: session may be a server session or a request object
         user = session.user
@@ -310,17 +337,17 @@
         # else if there is some rql expressions, check them
         if any(rqlexpr.check(session, eid)
                for rqlexpr in self.get_rqlexprs(action)):
-            return        
+            return
         raise Unauthorized(action, str(self))
 
     def rql_expression(self, expression, mainvars=None, eid=None):
         """rql expression factory"""
         return ERQLExpression(expression, mainvars, eid)
-    
+
 class CubicWebRelationSchema(RelationSchema):
     RelationSchema._RPROPERTIES['eid'] = None
     _perms_checked = False
-    
+
     def __init__(self, schema=None, rdef=None, eid=None, **kwargs):
         if rdef is not None:
             # if this relation is inlined
@@ -329,8 +356,8 @@
         if eid is None and rdef is not None:
             eid = getattr(rdef, 'eid', None)
         self.eid = eid
-                    
-        
+
+
     def update(self, subjschema, objschema, rdef):
         super(CubicWebRelationSchema, self).update(subjschema, objschema, rdef)
         if not self._perms_checked and self._groups:
@@ -350,7 +377,7 @@
                             newrqlexprs.append(ERQLExpression(rqlexpr.expression,
                                                               rqlexpr.mainvars,
                                                               rqlexpr.eid))
-                            self.set_rqlexprs(action, newrqlexprs) 
+                            self.set_rqlexprs(action, newrqlexprs)
                         else:
                             msg = "can't use RRQLExpression on a final relation "\
                                   "type (eg attribute relation), use an ERQLExpression (%s)"
@@ -361,16 +388,16 @@
                               "a RRQLExpression (%s)"
                         raise BadSchemaDefinition(msg % self.type)
             self._perms_checked = True
-            
+
     def cardinality(self, subjtype, objtype, target):
         card = self.rproperty(subjtype, objtype, 'cardinality')
         return (target == 'subject' and card[0]) or \
                (target == 'object' and card[1])
-    
+
     def schema_relation(self):
         return self.type in ('relation_type', 'from_entity', 'to_entity',
                              'constrained_by', 'cstrtype')
-    
+
     def physical_mode(self):
         """return an appropriate mode for physical storage of this relation type:
         * 'subjectinline' if every possible subject cardinalities are 1 or ?
@@ -386,7 +413,7 @@
         # in an allowed group, if so that's enough internal sessions should
         # always stop there
         if session.user.matching_groups(self.get_groups(action)):
-            return 
+            return
         # else if there is some rql expressions, check them
         if any(rqlexpr.check(session, *args, **kwargs)
                for rqlexpr in self.get_rqlexprs(action)):
@@ -399,7 +426,7 @@
             return ERQLExpression(expression, mainvars, eid)
         return RRQLExpression(expression, mainvars, eid)
 
-    
+
 class CubicWebSchema(Schema):
     """set of entities and relations schema defining the possible data sets
     used in an application
@@ -407,11 +434,11 @@
 
     :type name: str
     :ivar name: name of the schema, usually the application identifier
-    
+
     :type base: str
     :ivar base: path of the directory where the schema is defined
     """
-    reading_from_database = False    
+    reading_from_database = False
     entity_class = CubicWebEntitySchema
     relation_class = CubicWebRelationSchema
 
@@ -428,15 +455,15 @@
         rschema = self.add_relation_type(ybo.RelationType('identity', meta=True))
         rschema.final = False
         rschema.set_default_groups()
-        
+
     def schema_entity_types(self):
         """return the list of entity types used to build the schema"""
-        return frozenset(('EEType', 'ERType', 'EFRDef', 'ENFRDef',
-                          'EConstraint', 'EConstraintType', 'RQLExpression',
+        return frozenset(('CWEType', 'CWRType', 'CWAttribute', 'CWRelation',
+                          'CWConstraint', 'CWConstraintType', 'RQLExpression',
                           # XXX those are not really "schema" entity types
                           #     but we usually don't want them as @* targets
-                          'EProperty', 'EPermission', 'State', 'Transition'))
-        
+                          'CWProperty', 'CWPermission', 'State', 'Transition'))
+
     def add_entity_type(self, edef):
         edef.name = edef.name.encode()
         edef.name = bw_normalize_etype(edef.name)
@@ -451,13 +478,13 @@
             self.add_relation_def(rdef)
         self._eid_index[eschema.eid] = eschema
         return eschema
-        
+
     def add_relation_type(self, rdef):
         rdef.name = rdef.name.lower().encode()
         rschema = super(CubicWebSchema, self).add_relation_type(rdef)
         self._eid_index[rschema.eid] = rschema
         return rschema
-    
+
     def add_relation_def(self, rdef):
         """build a part of a relation schema
         (i.e. add a relation between two specific entity's types)
@@ -477,25 +504,25 @@
         rdef.name = rdef.name.lower()
         rdef.subject = bw_normalize_etype(rdef.subject)
         rdef.object = bw_normalize_etype(rdef.object)
-        super(CubicWebSchema, self).add_relation_def(rdef)
-        try:
-            self._eid_index[rdef.eid] = (self.eschema(rdef.subject),
-                                         self.rschema(rdef.name),
-                                         self.eschema(rdef.object))
-        except AttributeError:
-            pass # not a serialized schema
-    
+        if super(CubicWebSchema, self).add_relation_def(rdef):
+            try:
+                self._eid_index[rdef.eid] = (self.eschema(rdef.subject),
+                                             self.rschema(rdef.name),
+                                             self.eschema(rdef.object))
+            except AttributeError:
+                pass # not a serialized schema
+
     def del_relation_type(self, rtype):
         rschema = self.rschema(rtype)
         self._eid_index.pop(rschema.eid, None)
         super(CubicWebSchema, self).del_relation_type(rtype)
-    
+
     def del_relation_def(self, subjtype, rtype, objtype):
         for k, v in self._eid_index.items():
             if v == (subjtype, rtype, objtype):
                 del self._eid_index[k]
         super(CubicWebSchema, self).del_relation_def(subjtype, rtype, objtype)
-        
+
     def del_entity_type(self, etype):
         eschema = self.eschema(etype)
         self._eid_index.pop(eschema.eid, None)
@@ -504,7 +531,7 @@
         if 'has_text' in eschema.subject_relations():
             self.del_relation_def(etype, 'has_text', 'String')
         super(CubicWebSchema, self).del_entity_type(etype)
-        
+
     def schema_by_eid(self, eid):
         return self._eid_index[eid]
 
@@ -516,22 +543,22 @@
 
     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
     """
-    
+
     def __init__(self, restriction):
         self.restriction = restriction
 
     def serialize(self):
         return self.restriction
-    
+
     def deserialize(cls, value):
         return cls(value)
     deserialize = classmethod(deserialize)
-    
+
     def check(self, entity, rtype, value):
         """return true if the value satisfy the constraint, else false"""
         # implemented as a hook in the repository
@@ -541,7 +568,7 @@
         """raise ValidationError if the relation doesn't satisfy the constraint
         """
         pass # this is a vocabulary constraint, not enforce
-    
+
     def __str__(self):
         return self.restriction
 
@@ -559,7 +586,7 @@
                                       ('s', 'o'), build_descr=False)
     def error(self, eid, rtype, msg):
         raise ValidationError(eid, {rtype: msg})
-        
+
     def repo_check(self, session, eidfrom, rtype, eidto):
         """raise ValidationError if the relation doesn't satisfy the constraint
         """
@@ -581,12 +608,12 @@
             #     eidfrom or eidto (from user interface point of view)
             self.error(eidfrom, rtype, 'unique constraint %s failed' % self)
 
-    
+
 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
@@ -610,19 +637,19 @@
             if len(self.rqlst.defined_vars[mainvar].references()) <= 2:
                 LOGGER.warn('You did not use the %s variable in your RQL expression %s',
                             mainvar, self)
-    
+
     def __str__(self):
         return self.full_rql
     def __repr__(self):
         return '%s(%s)' % (self.__class__.__name__, self.full_rql)
-        
+
     def __deepcopy__(self, memo):
         return self.__class__(self.expression, self.mainvars)
     def __getstate__(self):
         return (self.expression, self.mainvars)
     def __setstate__(self, state):
         self.__init__(*state)
-        
+
     @cached
     def transform_has_permission(self):
         found = None
@@ -666,7 +693,7 @@
             rqlst.recover()
             return rql, found, keyarg
         return rqlst.as_string(), None, None
-        
+
     def _check(self, session, **kwargs):
         """return True if the rql expression is matching the given relation
         between fromeid and toeid
@@ -726,7 +753,7 @@
         if self.eid is not None:
             session.local_perm_cache[key] = False
         return False
-    
+
     @property
     def minimal_rql(self):
         return 'Any %s WHERE %s' % (self.mainvars, self.expression)
@@ -751,16 +778,16 @@
         if 'U' in defined:
             rql += ', U eid %(u)s'
         return rql
-    
+
     def check(self, session, eid=None):
         if 'X' in self.rqlst.defined_vars:
             if eid is None:
                 return False
             return self._check(session, x=eid)
         return self._check(session)
-    
+
 PyFileReader.context['ERQLExpression'] = ERQLExpression
-        
+
 class RRQLExpression(RQLExpression):
     def __init__(self, expression, mainvars=None, eid=None):
         if mainvars is None:
@@ -790,7 +817,7 @@
         if 'U' in defined:
             rql += ', U eid %(u)s'
         return rql
-    
+
     def check(self, session, fromeid=None, toeid=None):
         kwargs = {}
         if 'S' in self.rqlst.defined_vars:
@@ -802,17 +829,44 @@
                 return False
             kwargs['o'] = toeid
         return self._check(session, **kwargs)
-        
+
 PyFileReader.context['RRQLExpression'] = RRQLExpression
 
-        
+# workflow extensions #########################################################
+
+class workflowable_definition(ybo.metadefinition):
+    """extends default EntityType's metaclass to add workflow relations
+    (i.e. in_state and wf_info_for).
+    This is the default metaclass for WorkflowableEntityType
+    """
+    def __new__(mcs, name, bases, classdict):
+        abstract = classdict.pop('abstract', False)
+        defclass = super(workflowable_definition, mcs).__new__(mcs, name, bases, classdict)
+        if not abstract:
+            existing_rels = set(rdef.name for rdef in defclass.__relations__)
+            if 'in_state' not in existing_rels and 'wf_info_for' not in existing_rels:
+                in_state = ybo.SubjectRelation('State', cardinality='1*',
+                                               # XXX automatize this
+                                               constraints=[RQLConstraint('S is ET, O state_of ET')],
+                                               description=_('account state'))
+                yams_add_relation(defclass.__relations__, in_state, 'in_state')
+                wf_info_for = ybo.ObjectRelation('TrInfo', cardinality='1*', composite='object')
+                yams_add_relation(defclass.__relations__, wf_info_for, 'wf_info_for')
+        return defclass
+
+class WorkflowableEntityType(ybo.EntityType):
+    __metaclass__ = workflowable_definition
+    abstract = True
+
+PyFileReader.context['WorkflowableEntityType'] = WorkflowableEntityType
+
 # schema loading ##############################################################
 
 class CubicWebRelationFileReader(RelationFileReader):
     """cubicweb specific relation file reader, handling additional RQL
     constraints on a relation definition
     """
-    
+
     def handle_constraint(self, rdef, constraint_text):
         """arbitrary constraint is an rql expression for cubicweb"""
         if not rdef.constraints:
@@ -824,7 +878,7 @@
             rdef.inlined = True
         RelationFileReader.process_properties(self, rdef, relation_def)
 
-        
+
 CONSTRAINTS['RQLConstraint'] = RQLConstraint
 CONSTRAINTS['RQLUniqueConstraint'] = RQLUniqueConstraint
 CONSTRAINTS['RQLVocabularyConstraint'] = RQLVocabularyConstraint
@@ -839,20 +893,20 @@
     SchemaLoader.file_handlers.update({'.rel' : CubicWebRelationFileReader,
                                        })
 
-    def load(self, config, path=()):
+    def load(self, config, path=(), **kwargs):
         """return a Schema instance from the schema definition read
         from <directory>
         """
         self.lib_directory = config.schemas_lib_dir()
         return super(BootstrapSchemaLoader, self).load(
-            path, config.appid, register_base_types=False)
-    
+            path, config.appid, register_base_types=False, **kwargs)
+
     def _load_definition_files(self, cubes=None):
         # bootstraping, ignore cubes
         for filepath in self.include_schema_files('bootstrap'):
             self.info('loading %s', filepath)
             self.handle_file(filepath)
-        
+
     def unhandled_file(self, filepath):
         """called when a file without handler associated has been found"""
         self.warning('ignoring file %r', filepath)
@@ -863,7 +917,7 @@
     application's schema
     """
 
-    def load(self, config):
+    def load(self, config, **kwargs):
         """return a Schema instance from the schema definition read
         from <directory>
         """
@@ -872,13 +926,13 @@
             path = reversed([config.apphome] + config.cubes_path())
         else:
             path = reversed(config.cubes_path())
-        return super(CubicWebSchemaLoader, self).load(config, path=path)
+        return super(CubicWebSchemaLoader, self).load(config, path=path, **kwargs)
 
     def _load_definition_files(self, cubes):
         for filepath in (self.include_schema_files('bootstrap')
                          + self.include_schema_files('base')
+                         + self.include_schema_files('workflow')
                          + self.include_schema_files('Bookmark')):
-#                         + self.include_schema_files('Card')):
             self.info('loading %s', filepath)
             self.handle_file(filepath)
         for cube in cubes:
@@ -892,14 +946,15 @@
 PERM_USE_TEMPLATE_FORMAT = _('use_template_format')
 
 class FormatConstraint(StaticVocabularyConstraint):
-    need_perm_formats = (_('text/cubicweb-page-template'),
-                         )
+    need_perm_formats = [_('text/cubicweb-page-template')]
+
     regular_formats = (_('text/rest'),
                        _('text/html'),
                        _('text/plain'),
                        )
     def __init__(self):
         pass
+
     def serialize(self):
         """called to make persistent valuable data of a constraint"""
         return None
@@ -910,22 +965,22 @@
         a `cls` instance
         """
         return cls()
-    
-    def vocabulary(self, entity=None):
-        if entity and entity.req.user.has_permission(PERM_USE_TEMPLATE_FORMAT):
-            return self.regular_formats + self.need_perm_formats
+
+    def vocabulary(self, entity=None, req=None):
+        if req is None and entity is not None:
+            req = entity.req
+        if req is not None and req.user.has_permission(PERM_USE_TEMPLATE_FORMAT):
+            return self.regular_formats + tuple(self.need_perm_formats)
         return self.regular_formats
-    
+
     def __str__(self):
         return 'value in (%s)' % u', '.join(repr(unicode(word)) for word in self.vocabulary())
-    
-    
+
+
 format_constraint = FormatConstraint()
 CONSTRAINTS['FormatConstraint'] = FormatConstraint
 PyFileReader.context['format_constraint'] = format_constraint
 
-from logging import getLogger
-from cubicweb import set_log_methods
 set_log_methods(CubicWebSchemaLoader, getLogger('cubicweb.schemaloader'))
 set_log_methods(BootstrapSchemaLoader, getLogger('cubicweb.bootstrapschemaloader'))
 set_log_methods(RQLExpression, getLogger('cubicweb.schema'))
@@ -936,7 +991,7 @@
 def bw_import_erschema(self, ertype, schemamod=None, instantiate=True):
     return orig_import_erschema(self, bw_normalize_etype(ertype), schemamod, instantiate)
 PyFileReader.import_erschema = bw_import_erschema
-    
+
 # XXX itou for some Statement methods
 from rql import stmts
 orig_get_etype = stmts.ScopeNode.get_etype