From bddd7e55dc480658fef0304611207a256550b914 Mon Sep 17 00:00:00 2001 From: Stephen Burrows Date: Thu, 27 Jan 2011 16:27:48 -0500 Subject: [PATCH 1/1] Merged cowell back into core. --- admin/base.py | 103 ++++++++++++++++- contrib/cowell/__init__.py | 5 - contrib/cowell/admin.py | 107 ------------------ contrib/cowell/widgets.py | 9 -- forms/__init__.py | 3 +- contrib/cowell/forms.py => forms/entities.py | 12 +- models/__init__.py | 1 - models/{fields.py => fields/__init__.py} | 1 + .../fields.py => models/fields/entities.py | 14 +-- 9 files changed, 118 insertions(+), 137 deletions(-) delete mode 100644 contrib/cowell/__init__.py delete mode 100644 contrib/cowell/admin.py delete mode 100644 contrib/cowell/widgets.py rename contrib/cowell/forms.py => forms/entities.py (88%) rename models/{fields.py => fields/__init__.py} (97%) rename contrib/cowell/fields.py => models/fields/entities.py (94%) diff --git a/admin/base.py b/admin/base.py index acba9c3..81ff8f8 100644 --- a/admin/base.py +++ b/admin/base.py @@ -5,8 +5,10 @@ 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.models.fields.entities import ForeignKeyAttribute, ManyToManyAttribute from philo.admin.forms.attributes import AttributeForm, AttributeInlineFormSet from philo.admin.widgets import TagFilteredSelectMultiple +from philo.forms.entities import EntityForm, proxy_fields_for_entity_model from mptt.admin import MPTTModelAdmin @@ -29,16 +31,115 @@ class AttributeInline(generic.GenericTabularInline): template = 'admin/philo/edit_inline/tabular_attribute.html' +def hide_proxy_fields(cls, attname, proxy_field_set): + val_set = set(getattr(cls, attname)) + if proxy_field_set & val_set: + cls._hidden_attributes[attname] = list(val_set) + setattr(cls, attname, list(val_set - proxy_field_set)) + + +class EntityAdminMetaclass(admin.ModelAdmin.__metaclass__): + def __new__(cls, name, bases, attrs): + # HACK to bypass model validation for proxy fields by masking them as readonly fields + new_class = super(EntityAdminMetaclass, cls).__new__(cls, name, bases, attrs) + form = getattr(new_class, 'form', None) + if form: + opts = form._meta + if issubclass(form, EntityForm) and opts.model: + proxy_fields = proxy_fields_for_entity_model(opts.model).keys() + readonly_fields = new_class.readonly_fields + new_class._real_readonly_fields = readonly_fields + new_class.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. + new_class._hidden_attributes = {} + proxy_fields = set(proxy_fields) + hide_proxy_fields(new_class, 'raw_id_fields', proxy_fields) + #END HACK + return new_class + + class EntityAdmin(admin.ModelAdmin): + __metaclass__ = EntityAdminMetaclass + form = EntityForm inlines = [AttributeInline] save_on_top = True + + def __init__(self, *args, **kwargs): + # HACK PART 2 restores the actual readonly fields etc. on __init__. + if hasattr(self, '_real_readonly_fields'): + 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(EntityAdmin, self).__init__(*args, **kwargs) + + def formfield_for_dbfield(self, db_field, **kwargs): + """ + Override the default behavior to provide special formfields for EntityEntitys. + 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(EntityAdmin, 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'] = admin.widgets.ForeignKeyRawIdWidget(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'] = admin.widgets.ManyToManyRawIdWidget(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) class TreeAdmin(MPTTModelAdmin): pass -class TreeEntityAdmin(TreeAdmin, EntityAdmin): +class TreeEntityAdmin(EntityAdmin, TreeAdmin): pass diff --git a/contrib/cowell/__init__.py b/contrib/cowell/__init__.py deleted file mode 100644 index 710d164..0000000 --- a/contrib/cowell/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -""" -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/contrib/cowell/admin.py b/contrib/cowell/admin.py deleted file mode 100644 index 94df7cb..0000000 --- a/contrib/cowell/admin.py +++ /dev/null @@ -1,107 +0,0 @@ -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/widgets.py b/contrib/cowell/widgets.py deleted file mode 100644 index cff09f4..0000000 --- a/contrib/cowell/widgets.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.contrib.admin.widgets import ForeignKeyRawIdWidget, ManyToManyRawIdWidget - - -class ForeignKeyAttributeRawIdWidget(ForeignKeyRawIdWidget): - pass - - -class ManyToManyAttributeRawIdWidget(ManyToManyRawIdWidget): - pass \ No newline at end of file diff --git a/forms/__init__.py b/forms/__init__.py index 9e60966..7e0b0d9 100644 --- a/forms/__init__.py +++ b/forms/__init__.py @@ -1 +1,2 @@ -from philo.forms.fields import * \ No newline at end of file +from philo.forms.fields import * +from philo.forms.entities import * \ No newline at end of file diff --git a/contrib/cowell/forms.py b/forms/entities.py similarity index 88% rename from contrib/cowell/forms.py rename to forms/entities.py index aa8b6ca..b6259a3 100644 --- a/contrib/cowell/forms.py +++ b/forms/entities.py @@ -3,7 +3,7 @@ from django.utils.datastructures import SortedDict from philo.utils import fattr -__all__ = ('ProxyFieldForm',) +__all__ = ('EntityForm',) def proxy_fields_for_entity_model(entity_model, fields=None, exclude=None, widgets=None, formfield_callback=lambda f, **kwargs: f.formfield(**kwargs)): @@ -37,7 +37,7 @@ 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 ProxyFieldFormBase(ModelForm): +class EntityFormBase(ModelForm): pass _old_metaclass_new = ModelFormMetaclass.__new__ @@ -46,7 +46,7 @@ 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) opts = new_class._meta - if issubclass(new_class, ProxyFieldFormBase) and opts.model: + if issubclass(new_class, EntityFormBase) 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 @@ -59,7 +59,7 @@ ModelFormMetaclass.__new__ = staticmethod(_new_metaclass_new) # END HACK -class ProxyFieldForm(ProxyFieldFormBase): # Would inherit from ModelForm directly if it weren't for the above 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) @@ -76,12 +76,12 @@ class ProxyFieldForm(ProxyFieldFormBase): # Would inherit from ModelForm directl if initial is not None: new_initial.update(initial) kwargs['initial'] = new_initial - super(ProxyFieldForm, self).__init__(*args, **kwargs) + super(EntityForm, self).__init__(*args, **kwargs) @fattr(alters_data=True) def save(self, commit=True): cleaned_data = self.cleaned_data - instance = super(ProxyFieldForm, self).save(commit=False) + 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: diff --git a/models/__init__.py b/models/__init__.py index 76d7812..523f789 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -2,7 +2,6 @@ from philo.models.base import * from philo.models.collections import * from philo.models.nodes import * from philo.models.pages import * -from philo.models.fields import * from django.contrib.auth.models import User, Group from django.contrib.sites.models import Site diff --git a/models/fields.py b/models/fields/__init__.py similarity index 97% rename from models/fields.py rename to models/fields/__init__.py index 0289e57..d8ed839 100644 --- a/models/fields.py +++ b/models/fields/__init__.py @@ -3,6 +3,7 @@ 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.entities import * class TemplateField(models.TextField): diff --git a/contrib/cowell/fields.py b/models/fields/entities.py similarity index 94% rename from contrib/cowell/fields.py rename to models/fields/entities.py index 176554e..6c407d0 100644 --- a/contrib/cowell/fields.py +++ b/models/fields/entities.py @@ -1,11 +1,11 @@ """ -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. -This is relevant i.e. for passthroughs, where the value of the field may -be defined by some other instance's attributes. +The EntityProxyFields 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. This is relevant i.e. for passthroughs, where the +value of the field may be defined by some other instance's attributes. Example:: -- 2.20.1