diff -r 49075f57cf2c -r aa09e20dd8c0 schema.py --- 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 """ 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 """ @@ -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