from django.contrib import admin
from django.contrib.contenttypes import generic
-from philo.models import Tag, Attribute, Relationship
+from philo.models import Tag, Attribute
+from philo.forms import AttributeForm
COLLAPSE_CLASSES = ('collapse', 'collapse-closed', 'closed',)
ct_fk_field = 'entity_object_id'
model = Attribute
extra = 1
- template = 'admin/philo/edit_inline/tabular_collapse.html'
- allow_add = True
- classes = COLLAPSE_CLASSES
-
-
-class RelationshipInline(generic.GenericTabularInline):
- ct_field = 'entity_content_type'
- ct_fk_field = 'entity_object_id'
- model = Relationship
- extra = 1
- template = 'admin/philo/edit_inline/tabular_collapse.html'
+ template = 'admin/philo/edit_inline/tabular_attribute.html'
allow_add = True
classes = COLLAPSE_CLASSES
+ form = AttributeForm
+ exclude = ['value_object_id']
class EntityAdmin(admin.ModelAdmin):
- inlines = [AttributeInline, RelationshipInline]
+ inlines = [AttributeInline]
save_on_top = True
from django.contrib import admin
from django import forms
-from philo.admin import widgets
from philo.admin.base import COLLAPSE_CLASSES
from philo.admin.nodes import ViewAdmin
from philo.models.pages import Page, Template, Contentlet, ContentReference
from django.template import loader, loader_tags, TemplateDoesNotExist, Context, Template as DjangoTemplate
from django.utils.datastructures import SortedDict
from philo.admin.widgets import ModelLookupWidget
-from philo.models import Entity, Template, Contentlet, ContentReference
-from philo.models.fields import RelationshipField
+from philo.models import Entity, Template, Contentlet, ContentReference, Attribute
from philo.utils import fattr
return instance
+class AttributeForm(ModelForm):
+ def __init__(self, *args, **kwargs):
+ super(AttributeForm, self).__init__(*args, **kwargs)
+
+ # This is necessary because model forms store changes to self.instance in their clean method.
+ # Mutter mutter.
+ self._cached_value_ct = self.instance.value_content_type
+ self._cached_value = self.instance.value
+
+ if self.instance.value is not None:
+ value_field = self.instance.value.value_formfield()
+ if value_field:
+ self.fields['value'] = value_field
+ if hasattr(self.instance.value, 'content_type'):
+ self.fields['content_type'] = self.instance.value._meta.get_field('content_type').formfield(initial=getattr(self.instance.value.content_type, 'pk', None))
+
+ def save(self, *args, **kwargs):
+ # At this point, the cleaned_data has already been stored on self.instance.
+ if self.instance.value_content_type != self._cached_value_ct:
+ if self.instance.value is not None:
+ self._cached_value.delete()
+ if 'value' in self.cleaned_data:
+ del(self.cleaned_data['value'])
+
+ if self.instance.value_content_type is not None:
+ # Make a blank value of the new type! Run special code for content_type attributes.
+ if hasattr(self.instance.value_content_type.model_class(), 'content_type'):
+ if self._cached_value and hasattr(self._cached_value, 'content_type'):
+ new_ct = self._cached_value.content_type
+ else:
+ new_ct = None
+ new_value = self.instance.value_content_type.model_class().objects.create(content_type=new_ct)
+ else:
+ new_value = self.instance.value_content_type.model_class().objects.create()
+
+ new_value.apply_data(self.cleaned_data)
+ new_value.save()
+ self.instance.value = new_value
+ else:
+ # The value type is the same, but one of the fields has changed.
+ # Check to see if the changed value was the content type. We have to check the
+ # cleaned_data because self.instance.value.content_type was overridden.
+ if hasattr(self.instance.value, 'content_type') and 'content_type' in self.cleaned_data and 'value' in self.cleaned_data and (not hasattr(self._cached_value, 'content_type') or self._cached_value.content_type != self.cleaned_data['content_type']):
+ self.cleaned_data['value'] = None
+
+ self.instance.value.apply_data(self.cleaned_data)
+ self.instance.value.save()
+
+ super(AttributeForm, self).save(*args, **kwargs)
+ return self.instance
+
+ class Meta:
+ model = Attribute
+
+
class ContainerForm(ModelForm):
def __init__(self, *args, **kwargs):
super(ContainerForm, self).__init__(*args, **kwargs)
+from django import forms
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic
from django.core.exceptions import ObjectDoesNotExist
from philo.exceptions import AncestorDoesNotExist
from philo.models.fields import JSONField
-from philo.utils import ContentTypeRegistryLimiter
+from philo.utils import ContentTypeRegistryLimiter, ContentTypeSubclassLimiter
from philo.signals import entity_class_prepared
from philo.validators import json_validator
from UserDict import DictMixin
abstract = True
-class Attribute(models.Model):
- entity_content_type = models.ForeignKey(ContentType, verbose_name='Entity type')
- entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
- entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
- key = models.CharField(max_length=255)
- value = JSONField() #verbose_name='Value (JSON)', help_text='This value must be valid JSON.')
+value_content_type_limiter = ContentTypeRegistryLimiter()
+
+
+def register_value_model(model):
+ value_content_type_limiter.register_class(model)
+
+
+def unregister_value_model(model):
+ value_content_type_limiter.unregister_class(model)
+
+
+class AttributeValue(models.Model):
+ def apply_data(self, data):
+ raise NotImplementedError
+
+ def value_formfield(self, **kwargs):
+ raise NotImplementedError
def __unicode__(self):
- return u'"%s": %s' % (self.key, self.value)
+ return unicode(self.value)
class Meta:
- app_label = 'philo'
- unique_together = ('key', 'entity_content_type', 'entity_object_id')
+ abstract = True
-value_content_type_limiter = ContentTypeRegistryLimiter()
+attribute_value_limiter = ContentTypeSubclassLimiter(AttributeValue)
-def register_value_model(model):
- value_content_type_limiter.register_class(model)
+class JSONValue(AttributeValue):
+ value = JSONField() #verbose_name='Value (JSON)', help_text='This value must be valid JSON.')
+
+ def __unicode__(self):
+ return self.value_json
+
+ def value_formfield(self, **kwargs):
+ kwargs['initial'] = self.value_json
+ return self._meta.get_field('value').formfield(**kwargs)
+
+ def apply_data(self, cleaned_data):
+ self.value = cleaned_data.get('value', None)
+
+ class Meta:
+ app_label = 'philo'
-def unregister_value_model(model):
- value_content_type_limiter.unregister_class(model)
+class ForeignKeyValue(AttributeValue):
+ content_type = models.ForeignKey(ContentType, related_name='foreign_key_value_set', limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
+ object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True)
+ value = generic.GenericForeignKey()
+
+ def value_formfield(self, form_class=forms.ModelChoiceField, **kwargs):
+ if self.content_type is None:
+ return None
+ kwargs.update({'initial': self.object_id, 'required': False})
+ return form_class(self.content_type.model_class()._default_manager.all(), **kwargs)
+
+ def apply_data(self, cleaned_data):
+ if 'value' in cleaned_data and cleaned_data['value'] is not None:
+ self.value = cleaned_data['value']
+ else:
+ self.content_type = cleaned_data.get('content_type', None)
+ # If there is no value set in the cleaned data, clear the stored value.
+ self.object_id = None
+
+ class Meta:
+ app_label = 'philo'
+class ManyToManyValue(AttributeValue):
+ content_type = models.ForeignKey(ContentType, related_name='many_to_many_value_set', limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
+ object_ids = models.CommaSeparatedIntegerField(max_length=300, verbose_name='Value IDs', null=True, blank=True)
+
+ def get_object_id_list(self):
+ if not self.object_ids:
+ return []
+ else:
+ return self.object_ids.split(',')
+
+ def get_value(self):
+ if self.content_type is None:
+ return None
+
+ return self.content_type.model_class()._default_manager.filter(id__in=self.get_object_id_list())
+
+ def set_value(self, value):
+ if value is None:
+ self.object_ids = ""
+ return
+ if not isinstance(value, models.query.QuerySet):
+ raise TypeError("Value must be a QuerySet.")
+ self.content_type = ContentType.objects.get_for_model(value.model)
+ self.object_ids = ','.join([`value` for value in value.values_list('id', flat=True)])
+
+ value = property(get_value, set_value)
+
+ def value_formfield(self, form_class=forms.ModelMultipleChoiceField, **kwargs):
+ if self.content_type is None:
+ return None
+ kwargs.update({'initial': self.get_object_id_list(), 'required': False})
+ return form_class(self.content_type.model_class()._default_manager.all(), **kwargs)
+
+ def apply_data(self, cleaned_data):
+ self.value = cleaned_data.get('value', None)
+
+ class Meta:
+ app_label = 'philo'
+
-class Relationship(models.Model):
- entity_content_type = models.ForeignKey(ContentType, related_name='relationship_entity_set', verbose_name='Entity type')
+class Attribute(models.Model):
+ entity_content_type = models.ForeignKey(ContentType, related_name='attribute_entity_set', verbose_name='Entity type')
entity_object_id = models.PositiveIntegerField(verbose_name='Entity ID')
entity = generic.GenericForeignKey('entity_content_type', 'entity_object_id')
- key = models.CharField(max_length=255)
- value_content_type = models.ForeignKey(ContentType, related_name='relationship_value_set', limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True)
+
+ value_content_type = models.ForeignKey(ContentType, related_name='attribute_value_set', limit_choices_to=attribute_value_limiter, verbose_name='Value type', null=True, blank=True)
value_object_id = models.PositiveIntegerField(verbose_name='Value ID', null=True, blank=True)
value = generic.GenericForeignKey('value_content_type', 'value_object_id')
+ key = models.CharField(max_length=255)
+
+ def get_value_class(self, value):
+ if isinstance(value, models.query.QuerySet):
+ return ManyToManyValue
+ elif isinstance(value, models.Model) or (value is None and self.value_content_type.model_class() is ForeignKeyValue):
+ return ForeignKeyValue
+ else:
+ return JSONValue
+
+ def set_value(self, value):
+ # is this useful? The best way of doing it?
+ value_class = self.get_value_class(value)
+
+ if self.value is None or value_class != self.value_content_type.model_class():
+ if self.value is not None:
+ self.value.delete()
+ new_value = value_class()
+ new_value.value = value
+ new_value.save()
+ self.value = new_value
+ else:
+ self.value.value = value
+ self.value.save()
+
def __unicode__(self):
return u'"%s": %s' % (self.key, self.value)
class Meta:
app_label = 'philo'
- unique_together = ('key', 'entity_content_type', 'entity_object_id')
+ unique_together = (('key', 'entity_content_type', 'entity_object_id'), ('value_content_type', 'value_object_id'))
class QuerySetMapper(object, DictMixin):
__metaclass__ = EntityBase
attribute_set = generic.GenericRelation(Attribute, content_type_field='entity_content_type', object_id_field='entity_object_id')
- relationship_set = generic.GenericRelation(Relationship, content_type_field='entity_content_type', object_id_field='entity_object_id')
@property
def attributes(self):
return QuerySetMapper(self.attribute_set)
- @property
- def relationships(self):
- return QuerySetMapper(self.relationship_set)
-
@property
def _added_attribute_registry(self):
if not hasattr(self, '_real_added_attribute_registry'):
self._real_removed_attribute_registry = []
return self._real_removed_attribute_registry
- @property
- def _added_relationship_registry(self):
- if not hasattr(self, '_real_added_relationship_registry'):
- self._real_added_relationship_registry = {}
- return self._real_added_relationship_registry
-
- @property
- def _removed_relationship_registry(self):
- if not hasattr(self, '_real_removed_relationship_registry'):
- self._real_removed_relationship_registry = []
- return self._real_removed_relationship_registry
-
def save(self, *args, **kwargs):
super(Entity, self).save(*args, **kwargs)
attribute = Attribute()
attribute.entity = self
attribute.key = key
- attribute.value = value
+ attribute.set_value(value)
attribute.save()
self._added_attribute_registry.clear()
-
- for key in self._removed_relationship_registry:
- self.relationship_set.filter(key__exact=key).delete()
- del self._removed_relationship_registry[:]
-
- for key, value in self._added_relationship_registry.items():
- try:
- relationship = self.relationship_set.get(key__exact=key)
- except Relationship.DoesNotExist:
- relationship = Relationship()
- relationship.entity = self
- relationship.key = key
- relationship.value = value
- relationship.save()
- self._added_relationship_registry.clear()
class Meta:
abstract = True
from philo.validators import TemplateValidator, json_validator
-__all__ = ('AttributeField', 'RelationshipField')
+__all__ = ('JSONAttribute', 'ForeignKeyAttribute', 'ManyToManyAttribute')
class EntityProxyField(object):
raise AttributeError('The \'%s\' attribute can only be accessed from %s instances.' % (self.field.name, owner.__name__))
def __set__(self, instance, value):
- if self.field.key in instance._removed_attribute_registry:
- instance._removed_attribute_registry.remove(self.field.key)
- instance._added_attribute_registry[self.field.key] = value
+ raise NotImplementedError('AttributeFieldDescriptor subclasses must implement a __set__ method.')
def __delete__(self, instance):
if self.field.key in instance._added_attribute_registry:
instance._removed_attribute_registry.append(self.field.key)
+class JSONAttributeDescriptor(AttributeFieldDescriptor):
+ def __set__(self, instance, value):
+ if self.field.key in instance._removed_attribute_registry:
+ instance._removed_attribute_registry.remove(self.field.key)
+ instance._added_attribute_registry[self.field.key] = value
+
+
+class ForeignKeyAttributeDescriptor(AttributeFieldDescriptor):
+ def __set__(self, instance, value):
+ if isinstance(value, (models.Model, type(None))):
+ if self.field.key in instance._removed_attribute_registry:
+ instance._removed_attribute_registry.remove(self.field.key)
+ instance._added_attribute_registry[self.field.key] = value
+ else:
+ raise AttributeError('The \'%s\' attribute can only be set using existing Model objects.' % self.field.name)
+
+
+class ManyToManyAttributeDescriptor(AttributeFieldDescriptor):
+ def __set__(self, instance, value):
+ if isinstance(value, models.QuerySet):
+ if self.field.key in instance._removed_attribute_registry:
+ instance._removed_attribute_registry.remove(self.field.key)
+ instance._added_attribute_registry[self.field.key] = value
+ else:
+ raise AttributeError('The \'%s\' attribute can only be set to a QuerySet.' % self.field.name)
+
+
class AttributeField(EntityProxyField):
- descriptor_class = AttributeFieldDescriptor
+ def contribute_to_class(self, cls, name):
+ super(AttributeField, self).contribute_to_class(cls, name)
+ if self.key is None:
+ self.key = name
+
+
+class JSONAttribute(AttributeField):
+ descriptor_class = JSONAttributeDescriptor
def __init__(self, field_template=None, key=None, **kwargs):
super(AttributeField, self).__init__(**kwargs)
field_template = models.CharField(max_length=255)
self.field_template = field_template
- def contribute_to_class(self, cls, name):
- super(AttributeField, self).contribute_to_class(cls, name)
- if self.key is None:
- self.key = name
-
def formfield(self, **kwargs):
defaults = {'required': False, 'label': capfirst(self.verbose_name), 'help_text': self.help_text}
defaults.update(kwargs)
return self.field_template.formfield(**defaults)
-
-
-class RelationshipFieldDescriptor(object):
- def __init__(self, field):
- self.field = field
- def __get__(self, instance, owner):
- if instance:
- if self.field.key in instance._added_relationship_registry:
- return instance._added_relationship_registry[self.field.key]
- if self.field.key in instance._removed_relationship_registry:
- return None
- try:
- return instance.relationships[self.field.key]
- except KeyError:
- return None
- else:
- raise AttributeError('The \'%s\' attribute can only be accessed from %s instances.' % (self.field.name, owner.__name__))
-
- def __set__(self, instance, value):
- if isinstance(value, (models.Model, type(None))):
- if self.field.key in instance._removed_relationship_registry:
- instance._removed_relationship_registry.remove(self.field.key)
- instance._added_relationship_registry[self.field.key] = value
- else:
- raise AttributeError('The \'%s\' attribute can only be set using existing Model objects.' % self.field.name)
-
- def __delete__(self, instance):
- if self.field.key in instance._added_relationship_registry:
- del instance._added_relationship_registry[self.field.key]
- instance._removed_relationship_registry.append(self.field.key)
+ def value_from_object(self, obj):
+ return getattr(obj, self.attname).value
-class RelationshipField(EntityProxyField):
- descriptor_class = RelationshipFieldDescriptor
+class ForeignKeyAttribute(AttributeField):
+ descriptor_class = ForeignKeyAttributeDescriptor
def __init__(self, model, limit_choices_to=None, key=None, **kwargs):
- super(RelationshipField, self).__init__(**kwargs)
+ super(ForeignKeyAttribute, self).__init__(**kwargs)
self.key = key
self.model = model
if limit_choices_to is None:
limit_choices_to = {}
self.limit_choices_to = limit_choices_to
- def contribute_to_class(self, cls, name):
- super(RelationshipField, self).contribute_to_class(cls, name)
- if self.key is None:
- self.key = name
-
def formfield(self, form_class=forms.ModelChoiceField, **kwargs):
defaults = {'required': False, 'label': capfirst(self.verbose_name), 'help_text': self.help_text}
defaults.update(kwargs)
return form_class(self.model._default_manager.complex_filter(self.limit_choices_to), **defaults)
def value_from_object(self, obj):
- relobj = super(RelationshipField, self).value_from_object(obj)
+ relobj = super(ForeignKeyAttribute, self).value_from_object(obj).value
return getattr(relobj, 'pk', None)
+class ManyToManyAttribute(AttributeField):
+ descriptor_class = ManyToManyAttributeDescriptor
+ #FIXME: Add __init__ and formfield methods
+
+
class TemplateField(models.TextField):
def __init__(self, allow=None, disallow=None, secure=True, *args, **kwargs):
super(TemplateField, self).__init__(*args, **kwargs)
def attributes_with_node(self, node):
return QuerySetMapper(self.attribute_set, passthrough=node.attributes)
- def relationships_with_node(self, node):
- return QuerySetMapper(self.relationship_set, passthrough=node.relationships)
-
def render_to_response(self, node, request, path=None, subpath=None, extra_context=None):
extra_context = extra_context or {}
view_about_to_render.send(sender=self, node=node, request=request, path=path, subpath=subpath, extra_context=extra_context)
def render_to_string(self, node=None, request=None, path=None, subpath=None, extra_context=None):
context = {}
context.update(extra_context or {})
- context.update({'page': self, 'attributes': self.attributes, 'relationships': self.relationships})
+ context.update({'page': self, 'attributes': self.attributes})
if node and request:
- context.update({'node': node, 'attributes': self.attributes_with_node(node), 'relationships': self.relationships_with_node(node)})
+ context.update({'node': node, 'attributes': self.attributes_with_node(node)})
page_about_to_render_to_string.send(sender=self, node=node, request=request, extra_context=context)
string = self.template.django_template.render(RequestContext(request, context))
else:
<th{% if forloop.first %} colspan="2"{% endif %}{% if field.required %} class="required"{% endif %}>{{ field.label|capfirst }}</th>
{% endif %}
{% endfor %}
+ <th>Content Type</th>
+ <th>Value</th>
{% if inline_admin_formset.formset.can_delete %}<th>{% trans "Delete?" %}</th>{% endif %}
</tr></thead>
{% endfor %}
{% endfor %}
{% endfor %}
+ <td>{% with inline_admin_form.form.content_type as field %}{{ field.errors.as_ul }}{{ field }}{% endwith %}</td>
+ <td>{% with inline_admin_form.form.value as field %}{{ field.errors.as_ul }}{{ field }}{% endwith %}</td>
{% if inline_admin_formset.formset.can_delete %}
<td class="delete">{% if inline_admin_form.original %}{{ inline_admin_form.deletion_field.field }}{% endif %}</td>
{% endif %}