first commit

This commit is contained in:
Benjamin Dauvergne 2012-03-26 08:57:37 +02:00
commit e7f7041dea
7 changed files with 496 additions and 0 deletions

View File

93
django_directory/admin.py Normal file
View File

@ -0,0 +1,93 @@
from django.contrib.admin import AdminSite, ModelAdmin, site, TabularInline
from django.db import models
from .schema import get_schemas, schemas_update_subscribe
from models import (Schema, View, ViewMember, Attribute, Principal,
Predicate, Relation, AttributeValue, PrincipalUser)
class DirectoryAdmin(AdminSite):
def __init__(self, *args, **kwargs):
super(DirectoryAdmin, self).__init__(*args, **kwargs)
self.directory_proxy_models = []
self.regenerate_proxy_models()
schemas_update_subscribe(id(self), self.regenerate_proxy_models)
def make_accessor(self, attribute):
def accessor(self, principal):
values = principal.attributes.filter(attribute=attribute)
if attribute.multivalued:
return ', '.join(values.values_list('text_value', flat=True))
else:
if values:
return values[0].text_value
else:
return ''
accessor.__name__ = str(attribute.name)
accessor.short_description = unicode(attribute)
return accessor
def make_proxy(self, schema, name, model):
class Meta:
proxy = True
verbose_name = unicode(schema)
return type(name, (model,), dict(Meta=Meta, __module__=__name__))
def make_model_admin(self, model_admin_name, schema):
model_admin = None
def queryset(self, request):
qs = super(model_admin, self).queryset(request)
qs = qs.filter(schema=schema)
qs.prefetch_related('attributes', 'child_relations', 'parent_relations')
return qs
model_admin = type(model_admin_name, (ModelAdmin,), dict(queryset=queryset))
return model_admin
def regenerate_proxy_models(self):
print 'regenerating proxy models'
for proxy_model, proxy_model_admin in self.directory_proxy_models:
self.unregister(proxy_model)
self.directory_proxy_models = []
for schema in get_schemas():
prefix = str(schema.name.capitalize())
name = '%sPrincipalProxy' % prefix
model_admin_name = '%sPrincipalModelAdmin' % prefix
proxy = self.make_proxy(schema, name, Principal)
model_admin = self.make_model_admin(model_admin_name, schema)
list_display = []
if schema.views.all()[:1]:
view = schema.views.all()[0]
qs = view.viewmember_set.order_by('order')[:5]
print qs
for i, viewmember in enumerate(qs):
attribute = viewmember.attribute
name = str(attribute.name)
print 'setattre', model_admin, name
setattr(model_admin, name,
self.make_accessor(attribute))
list_display.append(name)
print 'setting list display', list_display, 'for', proxy
self.register(proxy, model_admin, list_display=list_display)
self.directory_proxy_models.append((proxy, model_admin))
site = DirectoryAdmin()
class AttributeValueInlineAdmin(TabularInline):
model = AttributeValue
class PrincipalModelAdmin(ModelAdmin):
inlines = [ AttributeValueInlineAdmin ]
class ViewMemberInlineAdmin(TabularInline):
model = ViewMember
class ViewModelAdmin(ModelAdmin):
inlines = [ ViewMemberInlineAdmin]
for model in (Schema, Attribute, Predicate, Relation,
AttributeValue, PrincipalUser):
site.register(model)
site.register(Principal, PrincipalModelAdmin)
site.register(View, ViewModelAdmin)

View File

@ -0,0 +1,29 @@
import uuid
from django.db import models
class UUIDField(models.Field):
__metaclass__ = models.SubfieldBase
def __init__(self, *args, **kwargs):
kwargs['max_length'] = 32
kwargs['editable'] = False
kwargs['blank'] = False
kwargs['unique'] = True
super(UUIDField, self).__init__(*args, **kwargs)
def pre_save(self, model_instance, add):
value = getattr(model_instance, self.attname, None)
if not value and add:
setattr(model_instance, self.attname, uuid.uuid4().hex)
return super(UUIDField, self).pre_save(model_instance, add)
def db_type(self, connection=None):
return 'char(%s)' % (self.max_length,)
def south_field_triple(self):
"Returns a suitable description of this field for South."
from south.modelsinspector import introspector
field_class = "%s.UUIDField" % __name__
args, kwargs = introspector(self)
return (field_class, args, kwargs)

View File

@ -0,0 +1,2 @@

299
django_directory/models.py Normal file
View File

@ -0,0 +1,299 @@
from decimal import Decimal
from datetime import time, datetime, date
from django.db import models
from django.contrib.auth.models import User
from django.forms import (IntegerField, CharField, DateField, DateTimeField,
TimeField, DecimalField, TypedChoiceField, FloatField)
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from django.db.models.signals import post_save, post_delete
from .fields import UUIDField
class decimal(Decimal):
pass
def parse_iso8601_datetime(iso_time):
return datetime.strptime(iso_time.replace("-", ""), "%Y%m%dT%H:%M:%S")
def parse_iso8601_date(iso_time):
return datetime.strptime(iso_time.replace("-", ""), "%Y%m%d").date()
def parse_iso8601_time(iso_time):
return datetime.strptime(iso_time, "%Y%m%d").time()
ATTRIBUTE_TYPES = {
str: dict(field=CharField),
int: dict(field=IntegerField),
float: dict(field=FloatField),
decimal: dict(field=DecimalField),
date: dict(field=DateField, coerce=parse_iso8601_date),
time: dict(field=TimeField, coerce=parse_iso8601_time),
datetime: dict(field=DateTimeField, coerce=parse_iso8601_datetime),
}
ATTRIBUTE_TYPES_MAP = dict(((t.__name__, t) for t in ATTRIBUTE_TYPES))
ATTRIBUTE_TYPE_CHOICES = [(t.__name__, t.__name__) for t in ATTRIBUTE_TYPES]
class Schema(models.Model):
name = models.CharField(max_length=128, unique=True)
display_name = models.CharField(max_length=256)
views = models.ManyToManyField('django_directory.View', blank=True)
object_of = models.ManyToManyField('django_directory.Predicate',
related_name='object_schemas', blank=True)
value_of = models.ManyToManyField('django_directory.Predicate',
related_name='value_schemas', blank=True)
def __unicode__(self):
return self.display_name or self.name
class Principal(models.Model):
uuid = UUIDField()
schema = models.ForeignKey(Schema)
@property
def attributes(self):
return Attributes(self)
@property
def user(self):
'''Return the user linked to this principal, if none exists, create
one.
'''
try:
return self.principaluser.user
except PrincipalUser.DoesNotExist:
if not self.id:
raise ValueError('Principal must have been saved to have an user')
user = User.get_or_create(username='principal#%u' % self.uuid)
principal_user = PrincipalUser(user=user, principal_ptr_id=self.pk)
principal_user.__dict__.update(self.__dict__)
principal_user.save()
return user
def __unicode__(self):
try:
user = self.principaluser.user
return self.uuid + " " + user
except PrincipalUser.DoesNotExist:
return self.uuid
class PrincipalUser(Principal):
user = models.OneToOneField(User, related_name='principal')
class Attribute(models.Model):
'''An attribute MUST have a globally unique name,
you can use URI for example.
'''
name = models.CharField(max_length=128, unique=True)
display_name = models.CharField(max_length=256)
multivalued = models.BooleanField(blank=True)
type_name = models.CharField(max_length=10, choices=ATTRIBUTE_TYPE_CHOICES)
possible_choices = models.TextField(blank=True, null=True)
required = models.BooleanField(blank=True)
@property
def type(self):
return ATTRIBUTE_TYPES_MAP.get(self.type_name)
@property
def coerce(self):
t = ATTRIBUTE_TYPES_MAP.get(self.type_name)
return ATTRIBUTE_TYPES[t].get('coerce', t)
@property
def choices(self):
result = []
t = self.type()
if self.possible_choices:
for choice in self.possible_choices.strip(','):
result.append(t(choice.strip()))
return result
@property
def field(self, *args, **kwargs):
t = self.type
if self.choices:
coerce = ATTRIBUTE_TYPES[t].get('coerce', t)
return TypedChoiceField( choices=self.choices, coerce=coerce)
else:
return ATTRIBUTE_TYPES[t]['field'](*args, required=self.required,
**kwargs)
def __unicode__(self):
return self.display_name or self.name
class Meta:
ordering = ('display_name',)
class ViewMember(models.Model):
'''Try to put some order upon all those attributes.
Attribute should be displayed in UI with this ordering, attribute with
the same order can be displayed on the same line for example.
'''
attribute = models.ForeignKey(Attribute)
view = models.ForeignKey('django_directory.View')
order = models.IntegerField(default=0)
def __unicode__(self):
return u'%s.%s %s' % (self.view, self.attribute, self.order)
class Meta:
ordering = ('order','attribute',)
class View(models.Model):
'''Group attributes together.
Attributes without a group should not be displayed or editable.
'''
name = models.CharField(max_length=128, primary_key=True)
display_name = models.CharField(max_length=128)
order = models.IntegerField(default=0)
attributes = models.ManyToManyField(Attribute,
blank=True, null=True, related_name='views',
through=ViewMember)
def __unicode__(self):
return self.display_name or self.name
def natural_key(self):
return (self.name,)
class Meta:
ordering = ('order', 'display_name', 'name')
class AttributeValue(models.Model):
'''Value for an attribute of a principal'''
user = models.ForeignKey(Principal, related_name='attributes')
attribute = models.ForeignKey(Attribute, related_name='values')
text_value = models.TextField()
@property
def value(self):
'''Coerce the stored text value into a Python value'''
return self.attribute.coerce(self.text_value)
def natural_key(self):
return (self.value,) + self.attribute.natural_key() + self.principal.natural_key()
def clean(self):
if self.attribute.multivalued and self.objects.filter(user=self.user, attribute=self.attribute) \
.exclude(pk=self.pk):
raise ValidationError(_(u'Attribute %u is not multi-valued') % self.attribute)
class Meta:
unique_together = (('user', 'attribute', 'text_value'),)
class AttributeValuesAsSet(object):
def __init__(self, attribute, **kwargs):
self.attribute = attribute
self.kwargs = kwargs
def qs(self, value=None):
qs = AttributeValue.objects.filter(*self.kwargs)
if value:
qs = qs.filter(text_value=unicode(value))
return qs
def add(self, value):
value = unicode(value)
if value not in self:
Attribute.get_or_create(text_value=value, **self.kwargs)
def update(self, values):
values = map(unicode, values)
values = set(values) - set(iter(self))
# since Django 1.4
if hasattr(AttributeValue.objects, 'bulk_create'):
AttributeValue.objects.bulk_create((
AttributeValue(text_value=value, **self.kwargs) for value in values))
else:
for value in values:
self.add(value)
def remove(self, value):
if value not in self:
raise KeyError()
self.discard(value)
def discard(self, value):
self.qs(value).delete()
def clear(self):
self.qs().delete()
def __contains__(self, value):
return bool(self.qs(value))
def __len__(self):
return self.qs().count()
def __iter__(self):
return iter(self.qs().values_list('text_value', flat=True))
class Attributes(object):
def __init__(self, principal):
self.principal = principal
def keys(self):
return Attribute.objects.filter(values__principal=self.principal)
def __getitem__(self, attribute):
if not hasattr(attribute, '_meta'):
attribute = Attribute.objects.get(id=attribute)
return AttributeValuesAsSet(attribute, principal=self.principal)
def __setitem__(self, attribute, values):
s = self[attribute]
s.update(values)
def __contains__(self, attribute):
if not hasattr(attribute, '_meta'):
attribute = Attribute.objects.get(id=attribute)
return bool(AttributeValue.filter(attribute=attribute))
class DirectoryUser(User):
@property
def principal(self):
return PrincipalUser.get_or_create(user=self)
@property
def attributes(self):
self.principal.attributes
class Meta:
proxy = True
class Predicate(models.Model):
name = models.CharField(max_length=128, unique=True)
display_name = models.CharField(max_length=256)
def __unicode__(self):
return self.display_name or self.name
class Relation(models.Model):
'''State a relation between two principals'''
object = models.ForeignKey(Principal, related_name='child_relations')
predicate = models.ForeignKey(Predicate, related_name='relations')
value = models.ForeignKey(Principal, related_name='parent_relations')
def update_schema(sender, **kwargs):
from . import schema
print 'update schema', kwargs
schema.get_schemas(True)
for model in (Schema, View, ViewMember, Attribute, Predicate, Relation):
post_save.connect(update_schema, sender=model,
dispatch_uid='schema-rebuild')
post_delete.connect(update_schema, sender=model,
dispatch_uid='schema-rebuild')

View File

@ -0,0 +1,19 @@
from . import models
__cached_schema = None
__notify = {}
def schemas_update_subscribe(who, what):
'''Register a function to be notified of schema changes'''
__notify[who] = what
def get_schemas(force=False):
'''Get and cache schema objects'''
global __cached_schema
if __cached_schema is None or force:
__cached_schema = list(models.Schema.objects.prefetch_related('views', 'object_of', 'value_of', 'views__attributes'))
if force:
for what in __notify.itervalues():
what()
return __cached_schema

54
setup.py Executable file
View File

@ -0,0 +1,54 @@
#! /usr/bin/env python
#
from distutils.core import setup, Command
from unittest import TextTestRunner, TestLoader
from glob import glob
from os.path import splitext, basename, join as pjoin
import os
class TestCommand(Command):
user_options = [ ]
def initialize_options(self):
self._dir = os.getcwd()
def finalize_options(self):
pass
def run(self):
'''
Finds all the tests modules in tests/, and runs them.
'''
testfiles = [ ]
for t in glob(pjoin(self._dir, 'tests', '*.py')):
if not t.endswith('__init__.py'):
testfiles.append('.'.join(
['tests', splitext(basename(t))[0]])
)
tests = TestLoader().loadTestsFromNames(testfiles)
t = TextTestRunner(verbosity = 4)
t.run(tests)
def ls_r(directory, target):
'''Recursively list files in @directory'''
path = os.path.join(os.path.dirname(__file__), directory)
to_remove = os.path.dirname(path)
for root, _, files in os.walk(path):
root = root.replace(to_remove + '/', '')
file_list = [ os.path.join(root, filename) for filename in files]
yield (os.path.join(target, root), file_list)
setup(name="django-directory",
version="0.1",
license="AGPLv3 or later",
description="A generic directory data model for Django",
url="http://dev.entrouvert.org/projects/django-directory/",
author="Entr'ouvert",
maintainer="Benjamin Dauvergne",
maintainer_email="bdauvergne@entrouvert.com",
packages=[ 'django_directory' ],
package_data={ '': ['fixtures/*.json',
'templates/*.html','templates/*/*.html','js/*.js'] },
data_files=list(ls_r('static', 'share/authentic2/')),
)