first commit
This commit is contained in:
commit
e7f7041dea
|
@ -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)
|
|
@ -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)
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
|
|
@ -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')
|
||||
|
|
@ -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
|
|
@ -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/')),
|
||||
)
|
Reference in New Issue