From 1422f0d2c8325b2c4d6781bd3c7b21a3d5873b90 Mon Sep 17 00:00:00 2001 From: Stephen Burrows Date: Fri, 21 Jan 2011 13:09:58 -0500 Subject: [PATCH] Split forms into containers, entities, and fields. Split attribute fields out from other model fields. Revamped the interaction between AttributeForm and the AttributeValue subclasses to more clearly define their roles. Moved all code related to AttributeFields into models.fields.attributes. Genericized AttributeFieldDescriptor and removed the other descriptor options. --- admin/base.py | 2 +- admin/pages.py | 2 +- forms.py | 340 ------------------------------------ forms/__init__.py | 0 forms/containers.py | 190 ++++++++++++++++++++ forms/entities.py | 175 +++++++++++++++++++ forms/fields.py | 18 ++ models/base.py | 129 +++++++------- models/fields.py | 263 ---------------------------- models/fields/__init__.py | 64 +++++++ models/fields/attributes.py | 242 +++++++++++++++++++++++++ 11 files changed, 757 insertions(+), 668 deletions(-) delete mode 100644 forms.py create mode 100644 forms/__init__.py create mode 100644 forms/containers.py create mode 100644 forms/entities.py create mode 100644 forms/fields.py delete mode 100644 models/fields.py create mode 100644 models/fields/__init__.py create mode 100644 models/fields/attributes.py diff --git a/admin/base.py b/admin/base.py index 0d35cf6..100eb31 100644 --- a/admin/base.py +++ b/admin/base.py @@ -5,7 +5,7 @@ from django.http import HttpResponse from django.utils import simplejson as json from django.utils.html import escape from philo.models import Tag, Attribute -from philo.forms import AttributeForm, AttributeInlineFormSet +from philo.forms.entities import AttributeForm, AttributeInlineFormSet from philo.admin.widgets import TagFilteredSelectMultiple from mptt.admin import MPTTModelAdmin diff --git a/admin/pages.py b/admin/pages.py index caeee05..0a09c03 100644 --- a/admin/pages.py +++ b/admin/pages.py @@ -4,7 +4,7 @@ from django import forms from philo.admin.base import COLLAPSE_CLASSES, TreeAdmin from philo.admin.nodes import ViewAdmin from philo.models.pages import Page, Template, Contentlet, ContentReference -from philo.forms import ContentletInlineFormSet, ContentReferenceInlineFormSet, ContentletForm, ContentReferenceForm +from philo.forms.containers import * class ContentletInline(admin.StackedInline): diff --git a/forms.py b/forms.py deleted file mode 100644 index a1785fb..0000000 --- a/forms.py +++ /dev/null @@ -1,340 +0,0 @@ -from django import forms -from django.contrib.admin.widgets import AdminTextareaWidget -from django.contrib.contenttypes.generic import BaseGenericInlineFormSet -from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import ValidationError, ObjectDoesNotExist -from django.db.models import Q -from django.forms.models import model_to_dict, fields_for_model, ModelFormMetaclass, ModelForm, BaseInlineFormSet -from django.forms.formsets import TOTAL_FORM_COUNT -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, Attribute -from philo.utils import fattr - - -__all__ = ('EntityForm', ) - - -def proxy_fields_for_entity_model(entity_model, fields=None, exclude=None, widgets=None, formfield_callback=lambda f, **kwargs: f.formfield(**kwargs)): - field_list = [] - ignored = [] - opts = entity_model._entity_meta - for f in opts.proxy_fields: - if not f.editable: - continue - if fields and not f.name in fields: - continue - if exclude and f.name in exclude: - continue - if widgets and f.name in widgets: - kwargs = {'widget': widgets[f.name]} - else: - kwargs = {} - formfield = formfield_callback(f, **kwargs) - if formfield: - field_list.append((f.name, formfield)) - else: - ignored.append(f.name) - field_dict = SortedDict(field_list) - if fields: - field_dict = SortedDict( - [(f, field_dict.get(f)) for f in fields - if ((not exclude) or (exclude and f not in exclude)) and (f not in ignored) and (f in field_dict)] - ) - return field_dict - - -# BEGIN HACK - This will not be required after http://code.djangoproject.com/ticket/14082 has been resolved - -class EntityFormBase(ModelForm): - pass - -_old_metaclass_new = ModelFormMetaclass.__new__ - -def _new_metaclass_new(cls, name, bases, attrs): - new_class = _old_metaclass_new(cls, name, bases, attrs) - if issubclass(new_class, EntityFormBase) and new_class._meta.model: - new_class.base_fields.update(proxy_fields_for_entity_model(new_class._meta.model, new_class._meta.fields, new_class._meta.exclude, new_class._meta.widgets)) # don't pass in formfield_callback - return new_class - -ModelFormMetaclass.__new__ = staticmethod(_new_metaclass_new) - -# END HACK - - -class EntityForm(EntityFormBase): # Would inherit from ModelForm directly if it weren't for the above HACK - def __init__(self, *args, **kwargs): - initial = kwargs.pop('initial', None) - instance = kwargs.get('instance', None) - if instance is not None: - new_initial = {} - for f in instance._entity_meta.proxy_fields: - if self._meta.fields and not f.name in self._meta.fields: - continue - if self._meta.exclude and f.name in self._meta.exclude: - continue - new_initial[f.name] = f.value_from_object(instance) - else: - new_initial = {} - if initial is not None: - new_initial.update(initial) - kwargs['initial'] = new_initial - super(EntityForm, self).__init__(*args, **kwargs) - - @fattr(alters_data=True) - def save(self, commit=True): - cleaned_data = self.cleaned_data - instance = super(EntityForm, self).save(commit=False) - - for f in instance._entity_meta.proxy_fields: - if not f.editable or not f.name in cleaned_data: - continue - if self._meta.fields and f.name not in self._meta.fields: - continue - if self._meta.exclude and f.name in self._meta.exclude: - continue - setattr(instance, f.attname, cleaned_data[f.name]) - - if commit: - instance.save() - self.save_m2m() - - 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 AttributeInlineFormSet(BaseGenericInlineFormSet): - "Necessary to force the GenericInlineFormset to use the form's save method for new objects." - def save_new(self, form, commit): - setattr(form.instance, self.ct_field.get_attname(), ContentType.objects.get_for_model(self.instance).pk) - setattr(form.instance, self.ct_fk_field.get_attname(), self.instance.pk) - return form.save() - - -class ContainerForm(ModelForm): - def __init__(self, *args, **kwargs): - super(ContainerForm, self).__init__(*args, **kwargs) - self.verbose_name = self.instance.name.replace('_', ' ') - - -class ContentletForm(ContainerForm): - content = forms.CharField(required=False, widget=AdminTextareaWidget, label='Content') - - def should_delete(self): - return not bool(self.cleaned_data['content']) - - class Meta: - model = Contentlet - fields = ['name', 'content'] - - -class ContentReferenceForm(ContainerForm): - def __init__(self, *args, **kwargs): - super(ContentReferenceForm, self).__init__(*args, **kwargs) - try: - self.fields['content_id'].widget = ModelLookupWidget(self.instance.content_type) - except ObjectDoesNotExist: - # This will happen when an empty form (which we will never use) gets instantiated. - pass - - def should_delete(self): - return (self.cleaned_data['content_id'] is None) - - class Meta: - model = ContentReference - fields = ['name', 'content_id'] - - -class ContainerInlineFormSet(BaseInlineFormSet): - def __init__(self, containers, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=None): - # Unfortunately, I need to add some things to BaseInline between its __init__ and its - # super call, so a lot of this is repetition. - - # Start cribbed from BaseInline - from django.db.models.fields.related import RelatedObject - self.save_as_new = save_as_new - # is there a better way to get the object descriptor? - self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name() - if self.fk.rel.field_name == self.fk.rel.to._meta.pk.name: - backlink_value = self.instance - else: - backlink_value = getattr(self.instance, self.fk.rel.field_name) - if queryset is None: - queryset = self.model._default_manager - qs = queryset.filter(**{self.fk.name: backlink_value}) - # End cribbed from BaseInline - - self.container_instances, qs = self.get_container_instances(containers, qs) - self.extra_containers = containers - self.extra = len(self.extra_containers) - super(BaseInlineFormSet, self).__init__(data, files, prefix=prefix, queryset=qs) - - def get_container_instances(self, containers, qs): - raise NotImplementedError - - def total_form_count(self): - if self.data or self.files: - return self.management_form.cleaned_data[TOTAL_FORM_COUNT] - else: - return self.initial_form_count() + self.extra - - def save_existing_objects(self, commit=True): - self.changed_objects = [] - self.deleted_objects = [] - if not self.get_queryset(): - return [] - - saved_instances = [] - for form in self.initial_forms: - pk_name = self._pk_field.name - raw_pk_value = form._raw_value(pk_name) - - # clean() for different types of PK fields can sometimes return - # the model instance, and sometimes the PK. Handle either. - pk_value = form.fields[pk_name].clean(raw_pk_value) - pk_value = getattr(pk_value, 'pk', pk_value) - - obj = self._existing_object(pk_value) - if form.should_delete(): - self.deleted_objects.append(obj) - obj.delete() - continue - if form.has_changed(): - self.changed_objects.append((obj, form.changed_data)) - saved_instances.append(self.save_existing(form, obj, commit=commit)) - if not commit: - self.saved_forms.append(form) - return saved_instances - - def save_new_objects(self, commit=True): - self.new_objects = [] - for form in self.extra_forms: - if not form.has_changed(): - continue - # If someone has marked an add form for deletion, don't save the - # object. - if form.should_delete(): - continue - self.new_objects.append(self.save_new(form, commit=commit)) - if not commit: - self.saved_forms.append(form) - return self.new_objects - - -class ContentletInlineFormSet(ContainerInlineFormSet): - def __init__(self, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=None): - if instance is None: - self.instance = self.fk.rel.to() - else: - self.instance = instance - - try: - containers = list(self.instance.containers[0]) - except ObjectDoesNotExist: - containers = [] - - super(ContentletInlineFormSet, self).__init__(containers, data, files, instance, save_as_new, prefix, queryset) - - def get_container_instances(self, containers, qs): - qs = qs.filter(name__in=containers) - container_instances = [] - for container in qs: - container_instances.append(container) - containers.remove(container.name) - return container_instances, qs - - def _construct_form(self, i, **kwargs): - if i >= self.initial_form_count(): # and not kwargs.get('instance'): - kwargs['instance'] = self.model(name=self.extra_containers[i - self.initial_form_count() - 1]) - - return super(ContentletInlineFormSet, self)._construct_form(i, **kwargs) - - -class ContentReferenceInlineFormSet(ContainerInlineFormSet): - def __init__(self, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=None): - if instance is None: - self.instance = self.fk.rel.to() - else: - self.instance = instance - - try: - containers = list(self.instance.containers[1]) - except ObjectDoesNotExist: - containers = [] - - super(ContentReferenceInlineFormSet, self).__init__(containers, data, files, instance, save_as_new, prefix, queryset) - - def get_container_instances(self, containers, qs): - filter = Q() - - for name, ct in containers: - filter |= Q(name=name, content_type=ct) - - qs = qs.filter(filter) - container_instances = [] - for container in qs: - container_instances.append(container) - containers.remove((container.name, container.content_type)) - return container_instances, qs - - def _construct_form(self, i, **kwargs): - if i >= self.initial_form_count(): # and not kwargs.get('instance'): - name, content_type = self.extra_containers[i - self.initial_form_count() - 1] - kwargs['instance'] = self.model(name=name, content_type=content_type) - - return super(ContentReferenceInlineFormSet, self)._construct_form(i, **kwargs) \ No newline at end of file diff --git a/forms/__init__.py b/forms/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/forms/containers.py b/forms/containers.py new file mode 100644 index 0000000..5991dfa --- /dev/null +++ b/forms/containers.py @@ -0,0 +1,190 @@ +from django import forms +from django.contrib.admin.widgets import AdminTextareaWidget +from django.core.exceptions import ObjectDoesNotExist +from django.db.models import Q +from django.forms.models import ModelForm, BaseInlineFormSet +from django.forms.formsets import TOTAL_FORM_COUNT +from philo.admin.widgets import ModelLookupWidget +from philo.models import Contentlet, ContentReference + + +__all__ = ( + 'ContentletForm', + 'ContentletInlineFormSet', + 'ContentReferenceForm', + 'ContentReferenceInlineFormSet' +) + + +class ContainerForm(ModelForm): + def __init__(self, *args, **kwargs): + super(ContainerForm, self).__init__(*args, **kwargs) + self.verbose_name = self.instance.name.replace('_', ' ') + + +class ContentletForm(ContainerForm): + content = forms.CharField(required=False, widget=AdminTextareaWidget, label='Content') + + def should_delete(self): + return not bool(self.cleaned_data['content']) + + class Meta: + model = Contentlet + fields = ['name', 'content'] + + +class ContentReferenceForm(ContainerForm): + def __init__(self, *args, **kwargs): + super(ContentReferenceForm, self).__init__(*args, **kwargs) + try: + self.fields['content_id'].widget = ModelLookupWidget(self.instance.content_type) + except ObjectDoesNotExist: + # This will happen when an empty form (which we will never use) gets instantiated. + pass + + def should_delete(self): + return (self.cleaned_data['content_id'] is None) + + class Meta: + model = ContentReference + fields = ['name', 'content_id'] + + +class ContainerInlineFormSet(BaseInlineFormSet): + def __init__(self, containers, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=None): + # Unfortunately, I need to add some things to BaseInline between its __init__ and its + # super call, so a lot of this is repetition. + + # Start cribbed from BaseInline + from django.db.models.fields.related import RelatedObject + self.save_as_new = save_as_new + # is there a better way to get the object descriptor? + self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name() + if self.fk.rel.field_name == self.fk.rel.to._meta.pk.name: + backlink_value = self.instance + else: + backlink_value = getattr(self.instance, self.fk.rel.field_name) + if queryset is None: + queryset = self.model._default_manager + qs = queryset.filter(**{self.fk.name: backlink_value}) + # End cribbed from BaseInline + + self.container_instances, qs = self.get_container_instances(containers, qs) + self.extra_containers = containers + self.extra = len(self.extra_containers) + super(BaseInlineFormSet, self).__init__(data, files, prefix=prefix, queryset=qs) + + def get_container_instances(self, containers, qs): + raise NotImplementedError + + def total_form_count(self): + if self.data or self.files: + return self.management_form.cleaned_data[TOTAL_FORM_COUNT] + else: + return self.initial_form_count() + self.extra + + def save_existing_objects(self, commit=True): + self.changed_objects = [] + self.deleted_objects = [] + if not self.get_queryset(): + return [] + + saved_instances = [] + for form in self.initial_forms: + pk_name = self._pk_field.name + raw_pk_value = form._raw_value(pk_name) + + # clean() for different types of PK fields can sometimes return + # the model instance, and sometimes the PK. Handle either. + pk_value = form.fields[pk_name].clean(raw_pk_value) + pk_value = getattr(pk_value, 'pk', pk_value) + + obj = self._existing_object(pk_value) + if form.should_delete(): + self.deleted_objects.append(obj) + obj.delete() + continue + if form.has_changed(): + self.changed_objects.append((obj, form.changed_data)) + saved_instances.append(self.save_existing(form, obj, commit=commit)) + if not commit: + self.saved_forms.append(form) + return saved_instances + + def save_new_objects(self, commit=True): + self.new_objects = [] + for form in self.extra_forms: + if not form.has_changed(): + continue + # If someone has marked an add form for deletion, don't save the + # object. + if form.should_delete(): + continue + self.new_objects.append(self.save_new(form, commit=commit)) + if not commit: + self.saved_forms.append(form) + return self.new_objects + + +class ContentletInlineFormSet(ContainerInlineFormSet): + def __init__(self, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=None): + if instance is None: + self.instance = self.fk.rel.to() + else: + self.instance = instance + + try: + containers = list(self.instance.containers[0]) + except ObjectDoesNotExist: + containers = [] + + super(ContentletInlineFormSet, self).__init__(containers, data, files, instance, save_as_new, prefix, queryset) + + def get_container_instances(self, containers, qs): + qs = qs.filter(name__in=containers) + container_instances = [] + for container in qs: + container_instances.append(container) + containers.remove(container.name) + return container_instances, qs + + def _construct_form(self, i, **kwargs): + if i >= self.initial_form_count(): # and not kwargs.get('instance'): + kwargs['instance'] = self.model(name=self.extra_containers[i - self.initial_form_count() - 1]) + + return super(ContentletInlineFormSet, self)._construct_form(i, **kwargs) + + +class ContentReferenceInlineFormSet(ContainerInlineFormSet): + def __init__(self, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=None): + if instance is None: + self.instance = self.fk.rel.to() + else: + self.instance = instance + + try: + containers = list(self.instance.containers[1]) + except ObjectDoesNotExist: + containers = [] + + super(ContentReferenceInlineFormSet, self).__init__(containers, data, files, instance, save_as_new, prefix, queryset) + + def get_container_instances(self, containers, qs): + filter = Q() + + for name, ct in containers: + filter |= Q(name=name, content_type=ct) + + qs = qs.filter(filter) + container_instances = [] + for container in qs: + container_instances.append(container) + containers.remove((container.name, container.content_type)) + return container_instances, qs + + def _construct_form(self, i, **kwargs): + if i >= self.initial_form_count(): # and not kwargs.get('instance'): + name, content_type = self.extra_containers[i - self.initial_form_count() - 1] + kwargs['instance'] = self.model(name=name, content_type=content_type) + + return super(ContentReferenceInlineFormSet, self)._construct_form(i, **kwargs) \ No newline at end of file diff --git a/forms/entities.py b/forms/entities.py new file mode 100644 index 0000000..a392e45 --- /dev/null +++ b/forms/entities.py @@ -0,0 +1,175 @@ +from django import forms +from django.contrib.contenttypes.generic import BaseGenericInlineFormSet +from django.contrib.contenttypes.models import ContentType +from django.forms.models import ModelFormMetaclass, ModelForm +from django.utils.datastructures import SortedDict +from philo.models import Attribute +from philo.utils import fattr + + +__all__ = ('EntityForm', 'AttributeForm', 'AttributeInlineFormSet') + + +def proxy_fields_for_entity_model(entity_model, fields=None, exclude=None, widgets=None, formfield_callback=lambda f, **kwargs: f.formfield(**kwargs)): + field_list = [] + ignored = [] + opts = entity_model._entity_meta + for f in opts.proxy_fields: + if not f.editable: + continue + if fields and not f.name in fields: + continue + if exclude and f.name in exclude: + continue + if widgets and f.name in widgets: + kwargs = {'widget': widgets[f.name]} + else: + kwargs = {} + formfield = formfield_callback(f, **kwargs) + if formfield: + field_list.append((f.name, formfield)) + else: + ignored.append(f.name) + field_dict = SortedDict(field_list) + if fields: + field_dict = SortedDict( + [(f, field_dict.get(f)) for f in fields + if ((not exclude) or (exclude and f not in exclude)) and (f not in ignored) and (f in field_dict)] + ) + return field_dict + + +# BEGIN HACK - This will not be required after http://code.djangoproject.com/ticket/14082 has been resolved + +class EntityFormBase(ModelForm): + pass + +_old_metaclass_new = ModelFormMetaclass.__new__ + +def _new_metaclass_new(cls, name, bases, attrs): + new_class = _old_metaclass_new(cls, name, bases, attrs) + if issubclass(new_class, EntityFormBase) and new_class._meta.model: + new_class.base_fields.update(proxy_fields_for_entity_model(new_class._meta.model, new_class._meta.fields, new_class._meta.exclude, new_class._meta.widgets)) # don't pass in formfield_callback + return new_class + +ModelFormMetaclass.__new__ = staticmethod(_new_metaclass_new) + +# END HACK + + +class EntityForm(EntityFormBase): # Would inherit from ModelForm directly if it weren't for the above HACK + def __init__(self, *args, **kwargs): + initial = kwargs.pop('initial', None) + instance = kwargs.get('instance', None) + if instance is not None: + new_initial = {} + for f in instance._entity_meta.proxy_fields: + if self._meta.fields and not f.name in self._meta.fields: + continue + if self._meta.exclude and f.name in self._meta.exclude: + continue + new_initial[f.name] = f.value_from_object(instance) + else: + new_initial = {} + if initial is not None: + new_initial.update(initial) + kwargs['initial'] = new_initial + super(EntityForm, self).__init__(*args, **kwargs) + + @fattr(alters_data=True) + def save(self, commit=True): + cleaned_data = self.cleaned_data + instance = super(EntityForm, self).save(commit=False) + + for f in instance._entity_meta.proxy_fields: + if not f.editable or not f.name in cleaned_data: + continue + if self._meta.fields and f.name not in self._meta.fields: + continue + if self._meta.exclude and f.name in self._meta.exclude: + continue + setattr(instance, f.attname, cleaned_data[f.name]) + + if commit: + instance.save() + self.save_m2m() + + return instance + + + def apply_data(self, cleaned_data): + self.value = cleaned_data.get('value', None) + + 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 + + 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.value = [] + +class AttributeForm(ModelForm): + """ + This class handles an attribute's fields as well as the fields for its value (if there is one.) + The fields defined will vary depending on the value type, but the fields for defining the value + (i.e. value_content_type and value_object_id) will always be defined. Except that value_object_id + will never be defined. BLARGH! + """ + 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. + value = self.instance.value + self._cached_value_ct = self.instance.value_content_type + self._cached_value = value + + # If there is a value, pull in its fields. + if value is not None: + self.value_fields = value.value_formfields() + self.fields.update(self.value_fields) + + 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: + # The value content type has changed. Clear the old value, if there was one. + if self._cached_value: + self._cached_value.delete() + + # Clear the submitted value, if any. + self.cleaned_data.pop('value', None) + + # Now create a new value instance so that on next instantiation, the form will + # know what fields to add. + if self.instance.value_content_type is not None: + self.instance.value = self.instance.value_content_type.model_class().objects.create() + elif self.instance.value is not None: + # The value content type is the same, but one of the value fields has changed. + + # Use construct_instance to apply the changes from the cleaned_data to the value instance. + fields = self.value_fields.keys() + if set(fields) & set(self.changed_data): + self.instance.value.construct_instance(**dict([(key, self.cleaned_data[key]) for key in fields])) + self.instance.value.save() + + return super(AttributeForm, self).save(*args, **kwargs) + + class Meta: + model = Attribute + + +class AttributeInlineFormSet(BaseGenericInlineFormSet): + "Necessary to force the GenericInlineFormset to use the form's save method for new objects." + def save_new(self, form, commit): + setattr(form.instance, self.ct_field.get_attname(), ContentType.objects.get_for_model(self.instance).pk) + setattr(form.instance, self.ct_fk_field.get_attname(), self.instance.pk) + return form.save() \ No newline at end of file diff --git a/forms/fields.py b/forms/fields.py new file mode 100644 index 0000000..9da6552 --- /dev/null +++ b/forms/fields.py @@ -0,0 +1,18 @@ +from django import forms +from django.utils import simplejson as json +from philo.validators import json_validator + + +__all__ = ('JSONFormField',) + + +class JSONFormField(forms.Field): + default_validators = [json_validator] + + def clean(self, value): + if value == '' and not self.required: + return None + try: + return json.loads(value) + except Exception, e: + raise ValidationError(u'JSON decode error: %s' % e) \ No newline at end of file diff --git a/models/base.py b/models/base.py index 8140520..20693b7 100644 --- a/models/base.py +++ b/models/base.py @@ -56,10 +56,15 @@ class AttributeValue(models.Model): def attribute(self): return self.attribute_set.all()[0] - def apply_data(self, data): + def set_value(self, value): + raise NotImplementedError + + def value_formfields(self, **kwargs): + """Define any formfields that would be used to construct an instance of this value.""" raise NotImplementedError - def value_formfield(self, **kwargs): + def construct_instance(self, **kwargs): + """Apply cleaned data from the formfields generated by valid_formfields to oneself.""" raise NotImplementedError def __unicode__(self): @@ -73,17 +78,22 @@ attribute_value_limiter = ContentTypeSubclassLimiter(AttributeValue) class JSONValue(AttributeValue): - value = JSONField() #verbose_name='Value (JSON)', help_text='This value must be valid JSON.') + value = JSONField(verbose_name='Value (JSON)', help_text='This value must be valid JSON.', default='null') def __unicode__(self): return smart_str(self.value) - def value_formfield(self, **kwargs): - kwargs['initial'] = self.value_json - return self._meta.get_field('value').formfield(**kwargs) + def value_formfields(self): + kwargs = {'initial': self.value_json} + field = self._meta.get_field('value') + return {field.name: field.formfield(**kwargs)} - def apply_data(self, cleaned_data): - self.value = cleaned_data.get('value', None) + def construct_instance(self, **kwargs): + field_name = self._meta.get_field('value').name + self.set_value(kwargs.pop(field_name, None)) + + def set_value(self, value): + self.value = value class Meta: app_label = 'philo' @@ -94,19 +104,33 @@ class ForeignKeyValue(AttributeValue): 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. + def value_formfields(self): + field = self._meta.get_field('content_type') + fields = {field.name: field.formfield(initial=getattr(self.content_type, 'pk', None))} + + if self.content_type: + kwargs = { + 'initial': self.object_id, + 'required': False, + 'queryset': self.content_type.model_class()._default_manager.all() + } + fields['value'] = forms.ModelChoiceField(**kwargs) + return fields + + def construct_instance(self, **kwargs): + field_name = self._meta.get_field('content_type').name + ct = kwargs.pop(field_name, None) + if ct is None or ct != self.content_type: self.object_id = None + self.content_type = ct + else: + value = kwargs.pop('value', None) + self.set_value(value) + if value is None: + self.content_type = ct + + def set_value(self, value): + self.value = value class Meta: app_label = 'philo' @@ -154,19 +178,29 @@ class ManyToManyValue(AttributeValue): 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): - if 'value' in cleaned_data and cleaned_data['value'] is not None: - self.value = cleaned_data['value'] + def value_formfields(self): + field = self._meta.get_field('content_type') + fields = {field.name: field.formfield(initial=getattr(self.content_type, 'pk', None))} + + if self.content_type: + kwargs = { + 'initial': self.get_object_id_list(), + 'required': False, + 'queryset': self.content_type.model_class()._default_manager.all() + } + fields['value'] = forms.ModelMultipleChoiceField(**kwargs) + return fields + + def construct_instance(self, **kwargs): + field_name = self._meta.get_field('content_type').name + ct = kwargs.pop(field_name, None) + if ct is None or ct != self.content_type: + self.values.clear() + self.content_type = ct 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.value = [] + value = kwargs.get('value', self.content_type.model_class()._default_manager.none()) + self.set_value(value) + construct_instance.alters_data = True class Meta: app_label = 'philo' @@ -245,37 +279,6 @@ class Entity(models.Model): def attributes(self): return QuerySetMapper(self.attribute_set.all()) - @property - def _added_attribute_registry(self): - if not hasattr(self, '_real_added_attribute_registry'): - self._real_added_attribute_registry = {} - return self._real_added_attribute_registry - - @property - def _removed_attribute_registry(self): - if not hasattr(self, '_real_removed_attribute_registry'): - self._real_removed_attribute_registry = [] - return self._real_removed_attribute_registry - - def save(self, *args, **kwargs): - super(Entity, self).save(*args, **kwargs) - - for key in self._removed_attribute_registry: - self.attribute_set.filter(key__exact=key).delete() - del self._removed_attribute_registry[:] - - for field, value in self._added_attribute_registry.items(): - try: - attribute = self.attribute_set.get(key__exact=field.key) - except Attribute.DoesNotExist: - attribute = Attribute() - attribute.entity = self - attribute.key = field.key - - field.set_attribute_value(attribute, value) - attribute.save() - self._added_attribute_registry.clear() - class Meta: abstract = True diff --git a/models/fields.py b/models/fields.py deleted file mode 100644 index 83798c4..0000000 --- a/models/fields.py +++ /dev/null @@ -1,263 +0,0 @@ -from django import forms -from django.core.exceptions import FieldError, ValidationError -from django.db import models -from django.db.models.fields import NOT_PROVIDED -from django.utils import simplejson as json -from django.utils.text import capfirst -from philo.signals import entity_class_prepared -from philo.validators import TemplateValidator, json_validator - - -__all__ = ('JSONAttribute', 'ForeignKeyAttribute', 'ManyToManyAttribute') - - -class EntityProxyField(object): - descriptor_class = None - - def __init__(self, verbose_name=None, help_text=None, default=NOT_PROVIDED, editable=True, *args, **kwargs): - if self.descriptor_class is None: - raise NotImplementedError('EntityProxyField subclasses must specify a descriptor_class.') - self.verbose_name = verbose_name - self.help_text = help_text - self.default = default - self.editable = editable - - def actually_contribute_to_class(self, sender, **kwargs): - sender._entity_meta.add_proxy_field(self) - setattr(sender, self.attname, self.descriptor_class(self)) - - def contribute_to_class(self, cls, name): - from philo.models.base import Entity - if issubclass(cls, Entity): - self.name = name - self.attname = name - if self.verbose_name is None and name: - self.verbose_name = name.replace('_', ' ') - entity_class_prepared.connect(self.actually_contribute_to_class, sender=cls) - else: - raise FieldError('%s instances can only be declared on Entity subclasses.' % self.__class__.__name__) - - def formfield(self, *args, **kwargs): - raise NotImplementedError('EntityProxyField subclasses must implement a formfield method.') - - def value_from_object(self, obj): - return getattr(obj, self.attname) - - def has_default(self): - return self.default is not NOT_PROVIDED - - -class AttributeFieldDescriptor(object): - def __init__(self, field): - self.field = field - - def __get__(self, instance, owner): - if instance: - if self.field in instance._added_attribute_registry: - return instance._added_attribute_registry[self.field] - if self.field in instance._removed_attribute_registry: - return None - try: - return instance.attributes[self.field.key] - except KeyError: - return None - else: - return None - - def __set__(self, instance, value): - raise NotImplementedError('AttributeFieldDescriptor subclasses must implement a __set__ method.') - - def __delete__(self, instance): - if self.field in instance._added_attribute_registry: - del instance._added_attribute_registry[self.field] - instance._removed_attribute_registry.append(self.field) - - -class JSONAttributeDescriptor(AttributeFieldDescriptor): - def __set__(self, instance, value): - if self.field in instance._removed_attribute_registry: - instance._removed_attribute_registry.remove(self.field) - instance._added_attribute_registry[self.field] = value - - -class ForeignKeyAttributeDescriptor(AttributeFieldDescriptor): - def __set__(self, instance, value): - if isinstance(value, (models.Model, type(None))): - if self.field in instance._removed_attribute_registry: - instance._removed_attribute_registry.remove(self.field) - instance._added_attribute_registry[self.field] = 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.query.QuerySet): - if self.field in instance._removed_attribute_registry: - instance._removed_attribute_registry.remove(self.field) - instance._added_attribute_registry[self.field] = value - else: - raise AttributeError('The \'%s\' attribute can only be set to a QuerySet.' % self.field.name) - - -class AttributeField(EntityProxyField): - def contribute_to_class(self, cls, name): - super(AttributeField, self).contribute_to_class(cls, name) - if self.key is None: - self.key = name - - def set_attribute_value(self, attribute, value, value_class): - if not isinstance(attribute.value, value_class): - if isinstance(attribute.value, models.Model): - attribute.value.delete() - new_value = value_class() - else: - new_value = attribute.value - new_value.value = value - new_value.save() - attribute.value = new_value - - -class JSONAttribute(AttributeField): - descriptor_class = JSONAttributeDescriptor - - def __init__(self, field_template=None, key=None, **kwargs): - super(AttributeField, self).__init__(**kwargs) - self.key = key - if field_template is None: - field_template = models.CharField(max_length=255) - self.field_template = field_template - - def formfield(self, **kwargs): - defaults = {'required': False, 'label': capfirst(self.verbose_name), 'help_text': self.help_text} - if self.has_default(): - defaults['initial'] = self.default - defaults.update(kwargs) - return self.field_template.formfield(**defaults) - - def value_from_object(self, obj): - try: - return getattr(obj, self.attname) - except AttributeError: - return None - - def set_attribute_value(self, attribute, value, value_class=None): - if value_class is None: - from philo.models.base import JSONValue - value_class = JSONValue - super(JSONAttribute, self).set_attribute_value(attribute, value, value_class) - - -class ForeignKeyAttribute(AttributeField): - descriptor_class = ForeignKeyAttributeDescriptor - - def __init__(self, model, limit_choices_to=None, key=None, **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 formfield(self, form_class=forms.ModelChoiceField, **kwargs): - defaults = {'required': False, 'label': capfirst(self.verbose_name), 'help_text': self.help_text} - if self.has_default(): - defaults['initial'] = self.default - defaults.update(kwargs) - return form_class(self.model._default_manager.complex_filter(self.limit_choices_to), **defaults) - - def value_from_object(self, obj): - try: - relobj = super(ForeignKeyAttribute, self).value_from_object(obj) - except AttributeError: - return None - return getattr(relobj, 'pk', None) - - def set_attribute_value(self, attribute, value, value_class=None): - if value_class is None: - from philo.models.base import ForeignKeyValue - value_class = ForeignKeyValue - super(ForeignKeyAttribute, self).set_attribute_value(attribute, value, value_class) - - -class ManyToManyAttribute(ForeignKeyAttribute): - descriptor_class = ManyToManyAttributeDescriptor - - def formfield(self, form_class=forms.ModelMultipleChoiceField, **kwargs): - return super(ManyToManyAttribute, self).formfield(form_class, **kwargs) - - def set_attribute_value(self, attribute, value, value_class=None): - if value_class is None: - from philo.models.base import ManyToManyValue - value_class = ManyToManyValue - super(ManyToManyAttribute, self).set_attribute_value(attribute, value, value_class) - - -class TemplateField(models.TextField): - def __init__(self, allow=None, disallow=None, secure=True, *args, **kwargs): - super(TemplateField, self).__init__(*args, **kwargs) - self.validators.append(TemplateValidator(allow, disallow, secure)) - - -class JSONFormField(forms.Field): - default_validators = [json_validator] - - def clean(self, value): - if value == '' and not self.required: - return None - try: - return json.loads(value) - except Exception, e: - raise ValidationError(u'JSON decode error: %s' % e) - - -class JSONDescriptor(object): - def __init__(self, field): - self.field = field - - def __get__(self, instance, owner): - if instance is None: - raise AttributeError # ? - - if self.field.name not in instance.__dict__: - json_string = getattr(instance, self.field.attname) - instance.__dict__[self.field.name] = json.loads(json_string) - - return instance.__dict__[self.field.name] - - def __set__(self, instance, value): - instance.__dict__[self.field.name] = value - setattr(instance, self.field.attname, json.dumps(value)) - - def __delete__(self, instance): - del(instance.__dict__[self.field.name]) - setattr(instance, self.field.attname, json.dumps(None)) - - -class JSONField(models.TextField): - default_validators = [json_validator] - - def get_attname(self): - return "%s_json" % self.name - - def contribute_to_class(self, cls, name): - super(JSONField, self).contribute_to_class(cls, name) - setattr(cls, name, JSONDescriptor(self)) - models.signals.pre_init.connect(self.fix_init_kwarg, sender=cls) - - def fix_init_kwarg(self, sender, args, kwargs, **signal_kwargs): - if self.name in kwargs: - kwargs[self.attname] = json.dumps(kwargs.pop(self.name)) - - def formfield(self, *args, **kwargs): - kwargs["form_class"] = JSONFormField - return super(JSONField, self).formfield(*args, **kwargs) - - -try: - from south.modelsinspector import add_introspection_rules -except ImportError: - pass -else: - add_introspection_rules([], ["^philo\.models\.fields\.TemplateField"]) - add_introspection_rules([], ["^philo\.models\.fields\.JSONField"]) \ No newline at end of file diff --git a/models/fields/__init__.py b/models/fields/__init__.py new file mode 100644 index 0000000..a22e161 --- /dev/null +++ b/models/fields/__init__.py @@ -0,0 +1,64 @@ +from django import forms +from django.db import models +from django.utils import simplejson as json +from philo.forms.fields import JSONFormField +from philo.validators import TemplateValidator, json_validator +from philo.models.fields.attributes import * + + +class TemplateField(models.TextField): + def __init__(self, allow=None, disallow=None, secure=True, *args, **kwargs): + super(TemplateField, self).__init__(*args, **kwargs) + self.validators.append(TemplateValidator(allow, disallow, secure)) + + +class JSONDescriptor(object): + def __init__(self, field): + self.field = field + + def __get__(self, instance, owner): + if instance is None: + raise AttributeError # ? + + if self.field.name not in instance.__dict__: + json_string = getattr(instance, self.field.attname) + instance.__dict__[self.field.name] = json.loads(json_string) + + return instance.__dict__[self.field.name] + + def __set__(self, instance, value): + instance.__dict__[self.field.name] = value + setattr(instance, self.field.attname, json.dumps(value)) + + def __delete__(self, instance): + del(instance.__dict__[self.field.name]) + setattr(instance, self.field.attname, json.dumps(None)) + + +class JSONField(models.TextField): + default_validators = [json_validator] + + def get_attname(self): + return "%s_json" % self.name + + def contribute_to_class(self, cls, name): + super(JSONField, self).contribute_to_class(cls, name) + setattr(cls, name, JSONDescriptor(self)) + models.signals.pre_init.connect(self.fix_init_kwarg, sender=cls) + + def fix_init_kwarg(self, sender, args, kwargs, **signal_kwargs): + if self.name in kwargs: + kwargs[self.attname] = json.dumps(kwargs.pop(self.name)) + + def formfield(self, *args, **kwargs): + kwargs["form_class"] = JSONFormField + return super(JSONField, self).formfield(*args, **kwargs) + + +try: + from south.modelsinspector import add_introspection_rules +except ImportError: + pass +else: + add_introspection_rules([], ["^philo\.models\.fields\.TemplateField"]) + add_introspection_rules([], ["^philo\.models\.fields\.JSONField"]) \ No newline at end of file diff --git a/models/fields/attributes.py b/models/fields/attributes.py new file mode 100644 index 0000000..f85fb32 --- /dev/null +++ b/models/fields/attributes.py @@ -0,0 +1,242 @@ +""" +The Attributes defined in this file can be assigned as fields on a proxy of +a subclass of philo.models.Entity. They act like any other model fields, +but instead of saving their data to the database, they save it to +attributes related to a model instance. Additionally, a new attribute will +be created for an instance if and only if the field's value has been set. +This is relevant i.e. for passthroughs, where the value of the field may +be defined by some other instance's attributes. + +Example:: + + class Thing(Entity): + numbers = models.PositiveIntegerField() + + class ThingProxy(Thing): + improvised = JSONAttribute(models.BooleanField) + + class Meta: + proxy = True +""" +from django import forms +from django.core.exceptions import FieldError +from django.db import models +from django.db.models.fields import NOT_PROVIDED +from django.utils.text import capfirst +from philo.signals import entity_class_prepared + + +__all__ = ('JSONAttribute', 'ForeignKeyAttribute', 'ManyToManyAttribute') + + +ATTRIBUTE_REGISTRY = '_attribute_registry' + + +class EntityProxyField(object): + def __init__(self, verbose_name=None, help_text=None, default=NOT_PROVIDED, editable=True, *args, **kwargs): + self.verbose_name = verbose_name + self.help_text = help_text + self.default = default + self.editable = editable + + def actually_contribute_to_class(self, sender, **kwargs): + sender._entity_meta.add_proxy_field(self) + + def contribute_to_class(self, cls, name): + from philo.models.base import Entity + if issubclass(cls, Entity): + self.name = name + self.model = cls + if self.verbose_name is None and name: + self.verbose_name = name.replace('_', ' ') + entity_class_prepared.connect(self.actually_contribute_to_class, sender=cls) + else: + raise FieldError('%s instances can only be declared on Entity subclasses.' % self.__class__.__name__) + + def formfield(self, form_class=forms.CharField, **kwargs): + defaults = { + 'required': False, + 'label': capfirst(self.verbose_name), + 'help_text': self.help_text + } + if self.has_default(): + defaults['initial'] = self.default + defaults.update(kwargs) + return form_class(**defaults) + + def value_from_object(self, obj): + return getattr(obj, self.name) + + def has_default(self): + return self.default is not NOT_PROVIDED + + +class AttributeFieldDescriptor(object): + def __init__(self, field): + self.field = field + + def get_registry(self, instance): + if ATTRIBUTE_REGISTRY not in instance.__dict__: + instance.__dict__[ATTRIBUTE_REGISTRY] = {'added': set(), 'removed': set()} + return instance.__dict__[ATTRIBUTE_REGISTRY] + + def __get__(self, instance, owner): + if instance is None: + return self + + if self.field.name not in instance.__dict__: + instance.__dict__[self.field.name] = instance.attributes[self.field.attribute_key] + + return instance.__dict__[self.field.name] + + def __set__(self, instance, value): + if instance is None: + raise AttributeError("%s must be accessed via instance" % self.field.name) + + self.field.validate_value(value) + instance.__dict__[self.field.name] = value + + registry = self.get_registry(instance) + registry['added'].add(self.field) + registry['removed'].remove(self.field) + + def __delete__(self, instance): + del instance.__dict__[self.field.name] + + registry = self.get_registry(instance) + registry['added'].remove(self.field) + registry['removed'].add(self.field) + + +def process_attribute_fields(sender, instance, created, **kwargs): + if ATTRIBUTE_REGISTRY in instance.__dict__: + registry = instance.__dict__[ATTRIBUTE_REGISTRY] + instance.attribute_set.filter(key__in=[field.attribute_key for field in registry['removed']]).delete() + + from philo.models import Attribute + for field in registry['added']: + try: + attribute = self.attribute_set.get(key=field.key) + except Attribute.DoesNotExist: + attribute = Attribute() + attribute.entity = instance + attribute.key = field.key + + value_class = field.get_value_class() + if isinstance(attribute.value, value_class): + value = attribute.value + else: + if isinstance(attribute.value, models.Model): + attribute.value.delete() + value = value_class() + + value.set_value(field.value_from_object(instance)) + value.save() + + attribute.value = value + attribute.save() + del instance.__dict__[ATTRIBUTE_REGISTRY] + + +class AttributeField(EntityProxyField): + def __init__(self, attribute_key=None, **kwargs): + self.attribute_key = attribute_key + super(AttributeField, self).__init__(**kwargs) + + def actually_contribute_to_class(self, sender, **kwargs): + super(AttributeField, self).actually_contribute_to_class(sender, **kwargs) + setattr(sender, self.name, AttributeFieldDescriptor(self)) + opts = sender._entity_meta + if not hasattr(opts, '_has_attribute_fields'): + opts._has_attribute_fields = True + models.signals.post_save.connect(process_attribute_fields, sender=sender) + + + def contribute_to_class(self, cls, name): + if self.attribute_key is None: + self.attribute_key = name + super(AttributeField, self).contribute_to_class(cls, name) + + def validate_value(self, value): + "Confirm that the value is valid or raise an appropriate error." + raise NotImplementedError("validate_value must be implemented by AttributeField subclasses.") + + def get_value_class(self): + raise NotImplementedError("get_value_class must be implemented by AttributeField subclasses.") + + +class JSONAttribute(AttributeField): + def __init__(self, field_template=None, **kwargs): + super(JSONAttribute, self).__init__(**kwargs) + if field_template is None: + field_template = models.CharField(max_length=255) + self.field_template = field_template + + def validate_value(self, value): + pass + + def formfield(self, **kwargs): + defaults = { + 'required': False, + 'label': capfirst(self.verbose_name), + 'help_text': self.help_text + } + if self.has_default(): + defaults['initial'] = self.default + defaults.update(kwargs) + return self.field_template.formfield(**defaults) + + def get_value_class(self): + from philo.models import JSONValue + return JSONValue + + # Not sure what this is doing - keep eyes open! + #def value_from_object(self, obj): + # try: + # return getattr(obj, self.name) + # except AttributeError: + # return None + + +class ForeignKeyAttribute(AttributeField): + def __init__(self, model, limit_choices_to=None, **kwargs): + super(ForeignKeyAttribute, self).__init__(**kwargs) + self.model = model + if limit_choices_to is None: + limit_choices_to = {} + self.limit_choices_to = limit_choices_to + + def validate_value(self, value): + if value is not None and not isinstance(value, self.model) : + raise TypeError("The '%s' attribute can only be set to an instance of %s or None." % (self.name, self.model.__name__)) + + def formfield(self, form_class=forms.ModelChoiceField, **kwargs): + defaults = { + 'queryset': self.model._default_manager.complex_filter(self.limit_choices_to) + } + defaults.update(kwargs) + return super(ForeignKeyAttribute, self).formfield(form_class=form_class, **defaults) + + def get_value_class(self): + from philo.models import ForeignKeyValue + return ForeignKeyValue + + #def value_from_object(self, obj): + # try: + # relobj = super(ForeignKeyAttribute, self).value_from_object(obj) + # except AttributeError: + # return None + # return getattr(relobj, 'pk', None) + + +class ManyToManyAttribute(ForeignKeyAttribute): + def validate_value(self, value): + if not isinstance(value, models.query.QuerySet) or value.model != self.model: + raise TypeError("The '%s' attribute can only be set to a %s QuerySet." % (self.name, self.model.__name__)) + + def formfield(self, form_class=forms.ModelMultipleChoiceField, **kwargs): + return super(ManyToManyAttribute, self).formfield(form_class=form_class, **kwargs) + + def get_value_class(self): + from philo.models import ManyToManyValue + return ManyToManyValue \ No newline at end of file -- 2.20.1