Commit 9050743d authored by Adrien Di Mascio's avatar Adrien Di Mascio
Browse files


......@@ -4,6 +4,11 @@ ChangeLog for yams
2008-03-05 -- 0.16.1
* fix a bug in entity validation : should convert value to the correct
python type before checking constraints
* support for entities whose fulltext content should be indexed on a related
entity, using new fulltext_container attribute on RelationSchema instance
and new fulltext_relations and fulltext_containers methods on EntitySchema
* backported subjrproperty/objrproperty ESchema methods
* new has_[subject|object]_relation methods on ESchema
2008-02-15 -- 0.16.0
* nicer schema image view
......@@ -9,16 +9,20 @@ __docformat__ = "restructuredtext en"
class SchemaError(Exception):
"""base class for schema exceptions"""
def __str__(self):
return unicode(self).encode('utf8')
class UnknownType(SchemaError):
"""using an unknown entity type"""
msg = 'Unknown type %s'
def __unicode__(self):
return self.msg % self.args
class BadSchemaDefinition(SchemaError):
"""error in the schema definition"""
msg = '%s line %s: %s'
args = ()
def __str__(self):
def __unicode__(self):
if len(self.args) == 3:
return self.msg % self.args
return ' '.join(self.args)
......@@ -40,5 +44,9 @@ class ValidationError(SchemaError):
self.entity = entity
self.errors = explanation
def __str__(self):
return '\n'.join('%s: %s' % (k, v) for k, v in self.errors.items())
def __unicode__(self):
if len(self.errors) == 1:
attr, error = self.errors.items()[0]
return u'%s (%s): %s' % (self.entity, attr, error)
errors = '\n'.join('* %s: %s' % (k, v) for k, v in self.errors.items())
return u'%s:\n%s' % (self.entity, errors)
......@@ -24,7 +24,7 @@ __all__ = ('ObjectRelation', 'SubjectRelation', 'BothWayRelation',
ETYPE_PROPERTIES = ('meta', 'description', 'permissions')
# don't put description inside, handled "manually"
RTYPE_PROPERTIES = ('meta', 'symetric', 'inlined', 'permissions')
RTYPE_PROPERTIES = ('meta', 'symetric', 'inlined', 'fulltext_container', 'permissions')
RDEF_PROPERTIES = ('cardinality', 'constraints', 'composite',
'order', 'default', 'uid', 'indexed', 'uid',
'fulltextindexed', 'internationalizable')
......@@ -229,6 +229,7 @@ class EntityType(Definition):
class RelationType(Definition):
symetric = MARKER
inlined = MARKER
fulltext_container = MARKER
def __init__(self, name=None, **kwargs):
super(RelationType, self).__init__(name)
......@@ -253,6 +253,11 @@ class EntitySchema(ERSchema):
return self.type in BASE_TYPES
def subjrproperty(self, rschema, prop):
return rschema.rproperty(self.type, rschema.objects(self.type)[0], prop)
def objrproperty(self, rschema, prop):
return rschema.rproperty(rschema.subjects(self.type)[0], self.type, prop)
def is_subobject(self):
"""return True if this entity type is contained by another"""
for rschema in self.object_relations():
......@@ -277,11 +282,20 @@ class EntitySchema(ERSchema):
return self._subj_relations.values()
def has_subject_relation(self, rtype):
"""return True if this entity type as a `rtype` subject relation"""
return rtype in self._subj_relations
def object_relations(self):
"""return a list of relations that may have this type of entity as
return self._obj_relations.values()
def has_object_relation(self, rtype):
"""return True if this entity type as a `rtype` object relation"""
return rtype in self._obj_relations
def subject_relation(self, rtype):
"""return the relation schema for the rtype subject relation
......@@ -356,24 +370,42 @@ class EntitySchema(ERSchema):
for rschema in self.object_relations():
yield rschema, rschema.subjects(self), 'object'
def main_attribute(self):
"""convenience method that returns the *main* (i.e. the first non meta)
attribute defined in the entity schema
for rschema, _ in self.attribute_definitions():
if not rschema.meta:
# XXX return rschema.type for bw compat ?
return rschema
def indexable_attributes(self):
"""return the name of relations to index"""
"""return the (name, role) of relations to index"""
assert not self.is_final()
for rschema in self.subject_relations():
if rschema.is_final():
if self.rproperty(rschema, 'fulltextindexed'):
# XXX return rschema.type for bw compat ?
yield rschema
def fulltext_relations(self):
"""return the (name, role) of relations to index"""
assert not self.is_final()
for rschema in self.subject_relations():
if not rschema.is_final() and rschema.fulltext_container == 'subject':
yield rschema, 'subject'
for rschema in self.object_relations():
if rschema.fulltext_container == 'object':
yield rschema, 'object'
def fulltext_containers(self):
"""return relations whose extremity points to an entity that should
contains the full text index content of entities of this type
for rschema in self.subject_relations():
if rschema.fulltext_container == 'object':
yield rschema, 'object'
for rschema in self.object_relations():
if rschema.fulltext_container == 'subject':
yield rschema, 'subject'
def defaults(self):
"""return an iterator on (attribute name, default value)"""
......@@ -382,7 +414,6 @@ class EntitySchema(ERSchema):
if rschema.is_final():
value = self.default(rschema)
if value is not None:
# XXX return rschema.type for bw compat ?
yield rschema, value
def default(self, rtype):
......@@ -531,6 +562,9 @@ class RelationSchema(ERSchema):
# if this relation is symetric/inlined
self.symetric = rdef.symetric or False
self.inlined = rdef.inlined or False
# if full text content of subject/object entity should be added
# to other side entity (the container)
self.fulltext_container = rdef.fulltext_container or None
# if this relation is an attribute relation = False
# mapping to subject/object with schema as key
......@@ -685,7 +719,7 @@ class RelationSchema(ERSchema):
raise KeyError('%s %s %s' % (subject, self, object))
def rproperty(self, subject, object, property):
"""return the properties dictionary of a relation"""
"""return the property for a relation definition"""
return self.rproperties(subject, object).get(property)
def set_rproperty(self, subject, object, pname, value):
......@@ -696,7 +730,7 @@ class RelationSchema(ERSchema):
def init_rproperties(self, subject, object, rdef):
key = subject, object
if key in self._rproperties:
msg = '%s already defined for %s' % (key, self)
msg = '(%s, %s) already defined for %s' % (subject, object, self)
raise BadSchemaDefinition(msg)
self._rproperties[key] = {}
for prop, default in self.rproperty_defs(key[1]).iteritems():
......@@ -877,12 +911,12 @@ class Schema(object):
subjectschema = self.eschema(rdef.subject)
except KeyError, ex:
msg = 'using unknown type %s in relation %s' % (str(ex), rtype)
msg = 'using unknown type %r in relation %s' % (rdef.subject, rtype)
raise BadSchemaDefinition(msg)
objectschema = self.eschema(rdef.object)
except KeyError, ex:
msg = "using unknown type %s in relation %s" % (str(ex), rtype)
msg = "using unknown type %r in relation %s" % (rdef.object, rtype)
raise BadSchemaDefinition(msg)
rschema.update(subjectschema, objectschema, rdef)
......@@ -945,7 +979,10 @@ class Schema(object):
:rtype: `EntitySchema`
:raise `KeyError`: if the type is not defined as an entity
return self._entities[etype]
return self._entities[etype]
except KeyError:
raise KeyError('No entity named %s in schema' % etype)
def relations(self):
"""return the list of possible relation'types
......@@ -972,7 +1009,10 @@ class Schema(object):
:rtype: `RelationSchema`
return self._relations[rtype]
return self._relations[rtype]
except KeyError:
raise KeyError('No relation named %s in schema'%rtype)
def final_relations(self):
"""return the list of possible final relation'types
......@@ -7,3 +7,26 @@ class Division(EntityType):
class Employee(EntityType):
works_for = SubjectRelation(('Company', 'Division'))
class require_permission(RelationType):
"""link a permission to the entity. This permission should be used in the
security definition of the entity's type to be useful.
fulltext_container = 'subject'
permissions = {
'read': ('managers', 'users', 'guests'),
'add': ('managers',),
'delete': ('managers',),
class missing_require_permission(RelationDefinition):
name = 'require_permission'
subject = ('Company', 'Division')
object = 'EPermission'
class EPermission(MetaEntityType):
"""entity type that may be used to construct some advanced security configuration
name = String(required=True, indexed=True, internationalizable=True,
fulltextindexed=True, maxsize=100,
description=_('name or identifier of the permission'))
"""unittests for schema2dot"""
import os
from logilab.common.testlib import TestCase, unittest_main
from logilab.common.compat import set
......@@ -23,35 +25,27 @@ class DummyDefaultHandler:
schema = SchemaLoader().load([DATADIR], default_handler=DummyDefaultHandler())
DOT_SOURCE = """digraph "Schema" {
DOT_SOURCE = """digraph "toto" {
"Person" [label="Person"];
"Societe" [label="Societe"];
edge [label="travaille"];
"Person" [shape="box", fontname="Courier", style="filled", label="Person"];
"Societe" [shape="box", fontname="Courier", style="filled", label="Societe"];
edge [taillabel="0..n", style="filled", arrowhead="open", color="black", label="travaille", headlabel="0..n", arrowtail="none"];
"Person" -> "Societe"
class MyVisitor(schema2dot.SchemaVisitor):
"""customize drawing options for better control"""
def get_props_for_eschema(self, eschema):
return {'label' : eschema.type}
def get_props_for_rschema(self, rschema):
return {'label' : rschema.type}
class DotTC(TestCase):
def test_schema2dot(self):
"""tests dot conversion without attributes information"""
wanted_entities = set(('Person', 'Societe'))
skipped_entities = set(schema.entities()) - wanted_entities
visitor = MyVisitor()
visitor.visit(schema, skipped_entities=skipped_entities)
self.assertTextEquals(DOT_SOURCE, visitor.generator.source)
schema2dot.schema2dot(schema, '/tmp/', skipentities=skipped_entities)
generated = open('/tmp/').read()
self.assertTextEquals(DOT_SOURCE, generated)
if __name__ == '__main__':
......@@ -52,6 +52,11 @@ CREATE TABLE Division(
name text
name varchar(100) NOT NULL
CREATE INDEX epermission_name_idx ON EPermission(name);
name varchar(64) UNIQUE NOT NULL,
description text,
......@@ -158,6 +163,15 @@ CREATE TABLE obj_wildcard_relation (
CREATE INDEX obj_wildcard_relation_from_idx ON obj_wildcard_relation(eid_from);
CREATE INDEX obj_wildcard_relation_to_idx ON obj_wildcard_relation(eid_to);
CREATE TABLE require_permission_relation (
CONSTRAINT require_permission_relation_p_key PRIMARY KEY(eid_from, eid_to)
CREATE INDEX require_permission_relation_from_idx ON require_permission_relation(eid_from);
CREATE INDEX require_permission_relation_to_idx ON require_permission_relation(eid_to);
CREATE TABLE state_of_relation (
......@@ -54,7 +54,7 @@ class SchemaLoaderTC(TestCase):
self.assertEquals(, 'Test')
['Affaire', 'Boolean', 'Bytes', 'Company', 'Date', 'Datetest', 'Datetime',
'Division', 'Eetype', 'Employee', 'Float', 'Int', 'Interval',
'Division', 'EPermission', 'Eetype', 'Employee', 'Float', 'Int', 'Interval',
'Note', 'Password', 'Person', 'Societe', 'State', 'String', 'Time',
......@@ -67,7 +67,7 @@ class SchemaLoaderTC(TestCase):
'mailinglist', 'meta', 'modname',
'name', 'next_state', 'nom', 'obj_wildcard',
'para', 'prenom', 'promo', 'pyversions',
'ref', 'rncs',
'ref', 'require_permission', 'rncs',
'salary', 'sexe', 'short_desc', 'state_of', 'subj_wildcard', 'sujet', 'sym_rel',
't1', 't2', 'tel', 'test', 'titre', 'travaille', 'type',
......@@ -125,6 +125,23 @@ class SchemaLoaderTC(TestCase):
self.assert_(not eschema.rproperty('sexe', 'fulltextindexed'))
indexable = sorted(eschema.indexable_attributes())
self.assertEquals(['nom', 'prenom', 'titre'], indexable)
self.assertEquals(schema.rschema('works_for').fulltext_container, None)
eschema = schema.eschema('Company')
indexable = sorted(eschema.indexable_attributes())
self.assertEquals([], indexable)
indexable = sorted(eschema.fulltext_relations())
self.assertEquals([('require_permission', 'subject')], indexable)
containers = sorted(eschema.fulltext_containers())
self.assertEquals([], containers)
eschema = schema.eschema('EPermission')
indexable = sorted(eschema.indexable_attributes())
self.assertEquals(['name'], indexable)
indexable = sorted(eschema.fulltext_relations())
self.assertEquals([], indexable)
containers = sorted(eschema.fulltext_containers())
self.assertEquals([('require_permission', 'subject')], containers)
def test_internationalizable(self):
eschema = schema.eschema('Eetype')
......@@ -178,7 +195,7 @@ class SchemaLoaderTC(TestCase):
self.assertEquals(rschema.description, '')
self.assertEquals(rschema.meta, False)
self.assertEquals(rschema.is_final(), True)
self.assertListEquals(sorted(rschema.subjects()), ['Company', 'Division', 'Eetype', 'State'])
self.assertListEquals(sorted(rschema.subjects()), ['Company', 'Division', 'EPermission', 'Eetype', 'State'])
self.assertListEquals(sorted(rschema.objects()), ['String'])
def test_cardinality(self):
......@@ -250,6 +267,11 @@ class SchemaLoaderTC(TestCase):
'add': ('managers', 'users', 'guests'),
'delete': ('managers', 'users', 'guests')})
rschema = schema.rschema('require_permission')
self.assertEquals(rschema._groups, {'read': ('managers', 'users', 'guests'),
'add': ('managers', ),
'delete': ('managers',)})
def test_entity_permissions(self):
eschema = schema.eschema('State')
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment