Created
January 26, 2011 22:09
-
-
Save melinath/797588 to your computer and use it in GitHub Desktop.
Diff of present entity_raw_id_fields branch from current master
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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..176554e 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 | |
| @@ -25,6 +23,7 @@ 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 | |
| +import datetime | |
| __all__ = ('JSONAttribute', 'ForeignKeyAttribute', 'ManyToManyAttribute') | |
| @@ -34,11 +33,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) | |
| @@ -69,8 +69,21 @@ class EntityProxyField(object): | |
| this field's initial value.""" | |
| return getattr(obj, self.name) | |
| + def get_storage_value(self, value): | |
| + """Final conversion of `value` before it gets stored on an Entity instance. | |
| + This step is performed by the ProxyFieldForm.""" | |
| + return value | |
| + | |
| 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): | |
| @@ -151,7 +164,6 @@ class AttributeField(EntityProxyField): | |
| 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: | |
| @@ -160,7 +172,7 @@ class AttributeField(EntityProxyField): | |
| 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.") | |
| + pass | |
| @property | |
| def value_class(self): | |
| @@ -176,9 +188,6 @@ class JSONAttribute(AttributeField): | |
| 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, | |
| @@ -189,6 +198,19 @@ class JSONAttribute(AttributeField): | |
| defaults['initial'] = self.default | |
| defaults.update(kwargs) | |
| return self.field_template.formfield(**defaults) | |
| + | |
| + def value_from_object(self, obj): | |
| + value = super(JSONAttribute, self).value_from_object(obj) | |
| + if isinstance(self.field_template, (models.DateField, models.DateTimeField)): | |
| + value = self.field_template.to_python(value) | |
| + return value | |
| + | |
| + def get_storage_value(self, value): | |
| + if isinstance(value, datetime.datetime): | |
| + return value.strftime("%Y-%m-%d %H:%M:%S") | |
| + if isinstance(value, datetime.date): | |
| + return value.strftime("%Y-%m-%d") | |
| + return value | |
| class ForeignKeyAttribute(AttributeField): | |
| @@ -196,18 +218,18 @@ class ForeignKeyAttribute(AttributeField): | |
| def __init__(self, model, limit_choices_to=None, **kwargs): | |
| super(ForeignKeyAttribute, self).__init__(**kwargs) | |
| - self.model = model | |
| + self.to = 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__)) | |
| + if value is not None and not isinstance(value, self.to) : | |
| + raise TypeError("The '%s' attribute can only be set to an instance of %s or None." % (self.name, self.to.__name__)) | |
| def formfield(self, form_class=forms.ModelChoiceField, **kwargs): | |
| defaults = { | |
| - 'queryset': self.model._default_manager.complex_filter(self.limit_choices_to) | |
| + 'queryset': self.to._default_manager.complex_filter(self.limit_choices_to) | |
| } | |
| defaults.update(kwargs) | |
| return super(ForeignKeyAttribute, self).formfield(form_class=form_class, **defaults) | |
| @@ -215,14 +237,18 @@ class ForeignKeyAttribute(AttributeField): | |
| def value_from_object(self, obj): | |
| relobj = super(ForeignKeyAttribute, self).value_from_object(obj) | |
| return getattr(relobj, 'pk', None) | |
| + | |
| + def get_related_field(self): | |
| + """Spoof being a rel from a ForeignKey.""" | |
| + return self.to._meta.pk | |
| 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__)) | |
| + if not isinstance(value, models.query.QuerySet) or value.model != self.to: | |
| + raise TypeError("The '%s' attribute can only be set to a %s QuerySet." % (self.name, self.to.__name__)) | |
| def formfield(self, form_class=forms.ModelMultipleChoiceField, **kwargs): | |
| return super(ManyToManyAttribute, self).formfield(form_class=form_class, **kwargs) | |
| diff --git a/contrib/cowell/forms.py b/contrib/cowell/forms.py | |
| index c4b573e..aa8b6ca 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: | |
| @@ -84,30 +90,10 @@ class EntityForm(EntityFormBase): # Would inherit from ModelForm directly if it | |
| continue | |
| if self._meta.exclude and f.name in self._meta.exclude: | |
| continue | |
| - setattr(instance, f.attname, cleaned_data[f.name]) | |
| + setattr(instance, f.attname, f.get_storage_value(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 = [] | |
| \ 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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment