From: Stephen Burrows Date: Fri, 21 Jan 2011 22:09:26 +0000 (-0500) Subject: Moved container forms and attribute forms into admin. Moved AttributeFields and Entit... X-Git-Tag: philo-0.9~22^2~5^2 X-Git-Url: http://git.ithinksw.org/philo.git/commitdiff_plain/dff8591e4ad14b7f1026adde11b4a43a36b364a2?hp=1422f0d2c8325b2c4d6781bd3c7b21a3d5873b90 Moved container forms and attribute forms into admin. Moved AttributeFields and EntityForm into contrib/cowell/; this more accurately reflects their status as convenient in certain circumstances. Simplified imports according to these changes. Made further tweaks to ManyToManyValue's methods. --- diff --git a/admin/base.py b/admin/base.py index 100eb31..acba9c3 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.entities import AttributeForm, AttributeInlineFormSet +from philo.admin.forms.attributes import AttributeForm, AttributeInlineFormSet from philo.admin.widgets import TagFilteredSelectMultiple from mptt.admin import MPTTModelAdmin diff --git a/admin/forms/__init__.py b/admin/forms/__init__.py new file mode 100644 index 0000000..1906380 --- /dev/null +++ b/admin/forms/__init__.py @@ -0,0 +1,2 @@ +from philo.admin.forms.attributes import * +from philo.admin.forms.containers import * \ No newline at end of file diff --git a/admin/forms/attributes.py b/admin/forms/attributes.py new file mode 100644 index 0000000..fc77d0f --- /dev/null +++ b/admin/forms/attributes.py @@ -0,0 +1,66 @@ +from django.contrib.contenttypes.generic import BaseGenericInlineFormSet +from django.contrib.contenttypes.models import ContentType +from django.forms.models import ModelForm +from philo.models import Attribute + + +__all__ = ('AttributeForm', 'AttributeInlineFormSet') + + +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/containers.py b/admin/forms/containers.py similarity index 100% rename from forms/containers.py rename to admin/forms/containers.py diff --git a/admin/pages.py b/admin/pages.py index 0a09c03..13d4098 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.containers import * +from philo.admin.forms.containers import * class ContentletInline(admin.StackedInline): diff --git a/contrib/cowell/__init__.py b/contrib/cowell/__init__.py new file mode 100644 index 0000000..710d164 --- /dev/null +++ b/contrib/cowell/__init__.py @@ -0,0 +1,5 @@ +""" +Cowell handles the code necessary for creating AttributeFields on Entities. This can be used +to give the appearance of fields added to models without the needing to change the schema or +to define multiple models due to minor differences in requirements. +""" \ No newline at end of file diff --git a/models/fields/attributes.py b/contrib/cowell/fields.py similarity index 84% rename from models/fields/attributes.py rename to contrib/cowell/fields.py index f85fb32..d9f3c8a 100644 --- a/models/fields/attributes.py +++ b/contrib/cowell/fields.py @@ -24,6 +24,7 @@ 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 +from philo.models import ManyToManyValue, JSONValue, ForeignKeyValue, Attribute, Entity __all__ = ('JSONAttribute', 'ForeignKeyAttribute', 'ManyToManyAttribute') @@ -43,9 +44,8 @@ class EntityProxyField(object): 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.name = self.attname = name self.model = cls if self.verbose_name is None and name: self.verbose_name = name.replace('_', ' ') @@ -65,6 +65,8 @@ class EntityProxyField(object): return form_class(**defaults) def value_from_object(self, obj): + """The return value of this method will be used by the EntityForm as + this field's initial value.""" return getattr(obj, self.name) def has_default(self): @@ -85,7 +87,7 @@ class AttributeFieldDescriptor(object): return self if self.field.name not in instance.__dict__: - instance.__dict__[self.field.name] = instance.attributes[self.field.attribute_key] + instance.__dict__[self.field.name] = instance.attributes.get(self.field.attribute_key, None) return instance.__dict__[self.field.name] @@ -98,13 +100,13 @@ class AttributeFieldDescriptor(object): registry = self.get_registry(instance) registry['added'].add(self.field) - registry['removed'].remove(self.field) + registry['removed'].discard(self.field) def __delete__(self, instance): del instance.__dict__[self.field.name] registry = self.get_registry(instance) - registry['added'].remove(self.field) + registry['added'].discard(self.field) registry['removed'].add(self.field) @@ -113,16 +115,15 @@ def process_attribute_fields(sender, instance, created, **kwargs): 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) + attribute = instance.attribute_set.get(key=field.attribute_key) except Attribute.DoesNotExist: attribute = Attribute() attribute.entity = instance - attribute.key = field.key + attribute.key = field.attribute_key - value_class = field.get_value_class() + value_class = field.value_class if isinstance(attribute.value, value_class): value = attribute.value else: @@ -130,7 +131,7 @@ def process_attribute_fields(sender, instance, created, **kwargs): attribute.value.delete() value = value_class() - value.set_value(field.value_from_object(instance)) + value.set_value(getattr(instance, field.name, None)) value.save() attribute.value = value @@ -161,11 +162,14 @@ class AttributeField(EntityProxyField): "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.") + @property + def value_class(self): + raise AttributeError("value_class must be defined on AttributeField subclasses.") class JSONAttribute(AttributeField): + value_class = JSONValue + def __init__(self, field_template=None, **kwargs): super(JSONAttribute, self).__init__(**kwargs) if field_template is None: @@ -185,20 +189,11 @@ class JSONAttribute(AttributeField): 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): + value_class = ForeignKeyValue + def __init__(self, model, limit_choices_to=None, **kwargs): super(ForeignKeyAttribute, self).__init__(**kwargs) self.model = model @@ -217,19 +212,14 @@ class ForeignKeyAttribute(AttributeField): 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) + def value_from_object(self, obj): + relobj = super(ForeignKeyAttribute, self).value_from_object(obj) + return getattr(relobj, 'pk', None) class ManyToManyAttribute(ForeignKeyAttribute): + value_class = ManyToManyValue + 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__)) @@ -237,6 +227,9 @@ class ManyToManyAttribute(ForeignKeyAttribute): 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 + def value_from_object(self, obj): + qs = super(ForeignKeyAttribute, self).value_from_object(obj) + try: + return qs.values_list('pk', flat=True) + except: + return [] \ No newline at end of file diff --git a/forms/entities.py b/contrib/cowell/forms.py similarity index 56% rename from forms/entities.py rename to contrib/cowell/forms.py index a392e45..c4b573e 100644 --- a/forms/entities.py +++ b/contrib/cowell/forms.py @@ -1,13 +1,9 @@ -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') +__all__ = ('EntityForm',) def proxy_fields_for_entity_model(entity_model, fields=None, exclude=None, widgets=None, formfield_callback=lambda f, **kwargs: f.formfield(**kwargs)): @@ -114,62 +110,4 @@ class EntityForm(EntityFormBase): # Would inherit from ModelForm directly if it 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 + self.value = [] \ No newline at end of file diff --git a/forms/__init__.py b/forms/__init__.py index e69de29..9e60966 100644 --- a/forms/__init__.py +++ b/forms/__init__.py @@ -0,0 +1 @@ +from philo.forms.fields import * \ No newline at end of file diff --git a/models/base.py b/models/base.py index 20693b7..3bcf394 100644 --- a/models/base.py +++ b/models/base.py @@ -140,41 +140,48 @@ class ManyToManyValue(AttributeValue): content_type = models.ForeignKey(ContentType, limit_choices_to=value_content_type_limiter, verbose_name='Value type', null=True, blank=True) values = models.ManyToManyField(ForeignKeyValue, blank=True, null=True) - def get_object_id_list(self): - if not self.values.count(): - return [] - else: - return self.values.values_list('object_id', flat=True) - - 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 get_object_ids(self): + return self.values.values_list('object_id', flat=True) + object_ids = property(get_object_ids) def set_value(self, value): - # Value is probably a queryset - but allow any iterable. + # Value must be a queryset. Watch out for ModelMultipleChoiceField; + # it returns its value as a list if empty. - # These lines shouldn't be necessary; however, if value is an EmptyQuerySet, - # the code (specifically the object_id__in query) won't work without them. Unclear why... - if not value: - value = [] + self.content_type = ContentType.objects.get_for_model(value.model) # Before we can fiddle with the many-to-many to foreignkeyvalues, we need # a pk. if self.pk is None: self.save() - if isinstance(value, models.query.QuerySet): - value = value.values_list('id', flat=True) + object_ids = value.values_list('id', flat=True) - self.values.filter(~models.Q(object_id__in=value)).delete() - current = self.get_object_id_list() + # These lines shouldn't be necessary; however, if object_ids is an EmptyQuerySet, + # the code (specifically the object_id__in query) won't work without them. Unclear why... + # TODO: is this still the case? + if not object_ids: + self.values.all().delete() + else: + self.values.exclude(object_id__in=object_ids, content_type=self.content_type).delete() + + current_ids = self.object_ids + + for object_id in object_ids: + if object_id in current_ids: + continue + self.values.create(content_type=self.content_type, object_id=object_id) + + def get_value(self): + if self.content_type is None: + return None - for v in value: - if v in current: - continue - self.values.create(content_type=self.content_type, object_id=v) + # HACK to be safely explicit until http://code.djangoproject.com/ticket/15145 is resolved + object_ids = self.object_ids + manager = self.content_type.model_class()._default_manager + if not object_ids: + return manager.none() + return manager.filter(id__in=self.object_ids) value = property(get_value, set_value) @@ -184,7 +191,7 @@ class ManyToManyValue(AttributeValue): if self.content_type: kwargs = { - 'initial': self.get_object_id_list(), + 'initial': self.object_ids, 'required': False, 'queryset': self.content_type.model_class()._default_manager.all() } @@ -198,7 +205,9 @@ class ManyToManyValue(AttributeValue): self.values.clear() self.content_type = ct else: - value = kwargs.get('value', self.content_type.model_class()._default_manager.none()) + value = kwargs.get('value', None) + if not value: + value = self.content_type.model_class()._default_manager.none() self.set_value(value) construct_instance.alters_data = True diff --git a/models/fields/__init__.py b/models/fields.py similarity index 97% rename from models/fields/__init__.py rename to models/fields.py index a22e161..0289e57 100644 --- a/models/fields/__init__.py +++ b/models/fields.py @@ -3,7 +3,6 @@ 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):