From 65350fb7f97eb0cbcbba837ee226563d6839fc91 Mon Sep 17 00:00:00 2001 From: Stephen Burrows Date: Mon, 24 Jan 2011 13:37:48 -0500 Subject: [PATCH] Initial raw_id_fields support for proxy fields. Involves hacks to bypass model validation for things like raw_id_fields, where validation depends on model._meta.get_field(). Renamed EntityForm to ProxyFieldForm to more accurately reflect its purpose. Removed extraneous code from ProxyFieldForm that didn't belong there. --- contrib/cowell/admin.py | 107 ++++++++++++++++++++++++++++++++++++++ contrib/cowell/fields.py | 28 +++++++--- contrib/cowell/forms.py | 42 +++++---------- contrib/cowell/widgets.py | 9 ++++ 4 files changed, 152 insertions(+), 34 deletions(-) create mode 100644 contrib/cowell/admin.py create mode 100644 contrib/cowell/widgets.py diff --git a/contrib/cowell/admin.py b/contrib/cowell/admin.py new file mode 100644 index 0000000..94df7cb --- /dev/null +++ b/contrib/cowell/admin.py @@ -0,0 +1,107 @@ +from django import forms +from django.contrib import admin +from philo.contrib.cowell.fields import ForeignKeyAttribute, ManyToManyAttribute +from philo.contrib.cowell.forms import ProxyFieldForm, proxy_fields_for_entity_model +from philo.contrib.cowell.widgets import ForeignKeyAttributeRawIdWidget, ManyToManyAttributeRawIdWidget +from philo.admin import EntityAdmin + + +def hide_proxy_fields(hidden, attrs, attname, attvalue, proxy_fields): + attvalue = set(attvalue) + proxy_fields = set(proxy_fields) + if proxy_fields & attvalue: + hidden[attname] = list(attvalue) + attrs[attname] = list(attvalue - proxy_fields) + + +class ProxyFieldAdminMetaclass(EntityAdmin.__metaclass__): + def __new__(cls, name, bases, attrs): + # HACK to bypass model validation for proxy fields by masking them as readonly fields + form = attrs.get('form') + if form: + opts = form._meta + if issubclass(form, ProxyFieldForm) and opts.model: + proxy_fields = proxy_fields_for_entity_model(opts.model).keys() + readonly_fields = attrs.pop('readonly_fields', ()) + cls._real_readonly_fields = readonly_fields + attrs['readonly_fields'] = list(readonly_fields) + proxy_fields + + # Additional HACKS to handle raw_id_fields and other attributes that the admin + # uses model._meta.get_field to validate. + hidden_attributes = {} + hide_proxy_fields(hidden_attributes, attrs, 'raw_id_fields', attrs.pop('raw_id_fields', ()), proxy_fields) + attrs['_hidden_attributes'] = hidden_attributes + #END HACK + return EntityAdmin.__metaclass__.__new__(cls, name, bases, attrs) + + +class ProxyFieldAdmin(EntityAdmin): + __metaclass__ = ProxyFieldAdminMetaclass + #form = ProxyFieldForm + + def __init__(self, *args, **kwargs): + # HACK PART 2 restores the actual readonly fields etc. on __init__. + self.readonly_fields = self.__class__._real_readonly_fields + if hasattr(self, '_hidden_attributes'): + for name, value in self._hidden_attributes.items(): + setattr(self, name, value) + # END HACK + super(ProxyFieldAdmin, self).__init__(*args, **kwargs) + + def formfield_for_dbfield(self, db_field, **kwargs): + """ + Override the default behavior to provide special formfields for EntityProxyFields. + Essentially clones the ForeignKey/ManyToManyField special behavior for the Attribute versions. + """ + if not db_field.choices and isinstance(db_field, (ForeignKeyAttribute, ManyToManyAttribute)): + request = kwargs.pop("request", None) + # Combine the field kwargs with any options for formfield_overrides. + # Make sure the passed in **kwargs override anything in + # formfield_overrides because **kwargs is more specific, and should + # always win. + if db_field.__class__ in self.formfield_overrides: + kwargs = dict(self.formfield_overrides[db_field.__class__], **kwargs) + + # Get the correct formfield. + if isinstance(db_field, ManyToManyAttribute): + formfield = self.formfield_for_manytomanyattribute(db_field, request, **kwargs) + elif isinstance(db_field, ForeignKeyAttribute): + formfield = self.formfield_for_foreignkeyattribute(db_field, request, **kwargs) + + # For non-raw_id fields, wrap the widget with a wrapper that adds + # extra HTML -- the "add other" interface -- to the end of the + # rendered output. formfield can be None if it came from a + # OneToOneField with parent_link=True or a M2M intermediary. + # TODO: Implement this. + #if formfield and db_field.name not in self.raw_id_fields: + # formfield.widget = admin.widgets.RelatedFieldWidgetWrapper(formfield.widget, db_field, self.admin_site) + + return formfield + return super(ProxyFieldAdmin, self).formfield_for_dbfield(db_field, **kwargs) + + def formfield_for_foreignkeyattribute(self, db_field, request=None, **kwargs): + """Get a form field for a ForeignKeyAttribute field.""" + db = kwargs.get('using') + if db_field.name in self.raw_id_fields: + kwargs['widget'] = ForeignKeyAttributeRawIdWidget(db_field, db) + #TODO: Add support for radio fields + #elif db_field.name in self.radio_fields: + # kwargs['widget'] = widgets.AdminRadioSelect(attrs={ + # 'class': get_ul_class(self.radio_fields[db_field.name]), + # }) + # kwargs['empty_label'] = db_field.blank and _('None') or None + + return db_field.formfield(**kwargs) + + def formfield_for_manytomanyattribute(self, db_field, request=None, **kwargs): + """Get a form field for a ManyToManyAttribute field.""" + db = kwargs.get('using') + + if db_field.name in self.raw_id_fields: + kwargs['widget'] = ManyToManyAttributeRawIdWidget(db_field, using=db) + kwargs['help_text'] = '' + #TODO: Add support for filtered fields. + #elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)): + # kwargs['widget'] = widgets.FilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical)) + + return db_field.formfield(**kwargs) \ No newline at end of file diff --git a/contrib/cowell/fields.py b/contrib/cowell/fields.py index d9f3c8a..c5db56d 100644 --- a/contrib/cowell/fields.py +++ b/contrib/cowell/fields.py @@ -1,6 +1,6 @@ """ -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, +The Attributes defined in this file can be assigned as fields on 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. @@ -14,10 +14,8 @@ Example:: class ThingProxy(Thing): improvised = JSONAttribute(models.BooleanField) - - class Meta: - proxy = True """ +from itertools import tee from django import forms from django.core.exceptions import FieldError from django.db import models @@ -34,11 +32,12 @@ ATTRIBUTE_REGISTRY = '_attribute_registry' class EntityProxyField(object): - def __init__(self, verbose_name=None, help_text=None, default=NOT_PROVIDED, editable=True, *args, **kwargs): + def __init__(self, verbose_name=None, help_text=None, default=NOT_PROVIDED, editable=True, choices=None, *args, **kwargs): self.verbose_name = verbose_name self.help_text = help_text self.default = default self.editable = editable + self._choices = choices or [] def actually_contribute_to_class(self, sender, **kwargs): sender._entity_meta.add_proxy_field(self) @@ -71,6 +70,14 @@ class EntityProxyField(object): def has_default(self): return self.default is not NOT_PROVIDED + + def _get_choices(self): + if hasattr(self._choices, 'next'): + choices, self._choices = tee(self._choices) + return choices + else: + return self._choices + choices = property(_get_choices) class AttributeFieldDescriptor(object): @@ -215,6 +222,15 @@ class ForeignKeyAttribute(AttributeField): def value_from_object(self, obj): relobj = super(ForeignKeyAttribute, self).value_from_object(obj) return getattr(relobj, 'pk', None) + + @property + def to(self): + """Spoof being a rel from a ForeignKey.""" + return self.model + + def get_related_field(self): + """Again, spoof being a rel from a ForeignKey.""" + return self.model._meta.pk class ManyToManyAttribute(ForeignKeyAttribute): diff --git a/contrib/cowell/forms.py b/contrib/cowell/forms.py index c4b573e..0b5a0c6 100644 --- a/contrib/cowell/forms.py +++ b/contrib/cowell/forms.py @@ -3,7 +3,7 @@ from django.utils.datastructures import SortedDict from philo.utils import fattr -__all__ = ('EntityForm',) +__all__ = ('ProxyFieldForm',) def proxy_fields_for_entity_model(entity_model, fields=None, exclude=None, widgets=None, formfield_callback=lambda f, **kwargs: f.formfield(**kwargs)): @@ -37,15 +37,21 @@ def proxy_fields_for_entity_model(entity_model, fields=None, exclude=None, widge # BEGIN HACK - This will not be required after http://code.djangoproject.com/ticket/14082 has been resolved -class EntityFormBase(ModelForm): +class ProxyFieldFormBase(ModelForm): pass _old_metaclass_new = ModelFormMetaclass.__new__ def _new_metaclass_new(cls, name, bases, attrs): + formfield_callback = attrs.get('formfield_callback', lambda f, **kwargs: f.formfield(**kwargs)) 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 + opts = new_class._meta + if issubclass(new_class, ProxyFieldFormBase) and opts.model: + # "override" proxy fields with declared fields by excluding them if there's a name conflict. + exclude = (list(opts.exclude or []) + new_class.declared_fields.keys()) or None + proxy_fields = proxy_fields_for_entity_model(opts.model, opts.fields, exclude, opts.widgets, formfield_callback) # don't pass in formfield_callback + new_class.proxy_fields = proxy_fields + new_class.base_fields.update(proxy_fields) return new_class ModelFormMetaclass.__new__ = staticmethod(_new_metaclass_new) @@ -53,7 +59,7 @@ ModelFormMetaclass.__new__ = staticmethod(_new_metaclass_new) # END HACK -class EntityForm(EntityFormBase): # Would inherit from ModelForm directly if it weren't for the above HACK +class ProxyFieldForm(ProxyFieldFormBase): # 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) @@ -70,12 +76,12 @@ class EntityForm(EntityFormBase): # Would inherit from ModelForm directly if it if initial is not None: new_initial.update(initial) kwargs['initial'] = new_initial - super(EntityForm, self).__init__(*args, **kwargs) + super(ProxyFieldForm, 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) + instance = super(ProxyFieldForm, self).save(commit=False) for f in instance._entity_meta.proxy_fields: if not f.editable or not f.name in cleaned_data: @@ -90,24 +96,4 @@ class EntityForm(EntityFormBase): # Would inherit from ModelForm directly if it 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 = [] \ No newline at end of file + return instance \ No newline at end of file diff --git a/contrib/cowell/widgets.py b/contrib/cowell/widgets.py new file mode 100644 index 0000000..cff09f4 --- /dev/null +++ b/contrib/cowell/widgets.py @@ -0,0 +1,9 @@ +from django.contrib.admin.widgets import ForeignKeyRawIdWidget, ManyToManyRawIdWidget + + +class ForeignKeyAttributeRawIdWidget(ForeignKeyRawIdWidget): + pass + + +class ManyToManyAttributeRawIdWidget(ManyToManyRawIdWidget): + pass \ No newline at end of file -- 2.20.1