1 from django import forms
2 from django.contrib.contenttypes.generic import BaseGenericInlineFormSet
3 from django.contrib.contenttypes.models import ContentType
4 from django.forms.models import ModelFormMetaclass, ModelForm
5 from django.utils.datastructures import SortedDict
6 from philo.models import Attribute
7 from philo.utils import fattr
10 __all__ = ('EntityForm', 'AttributeForm', 'AttributeInlineFormSet')
13 def proxy_fields_for_entity_model(entity_model, fields=None, exclude=None, widgets=None, formfield_callback=lambda f, **kwargs: f.formfield(**kwargs)):
16 opts = entity_model._entity_meta
17 for f in opts.proxy_fields:
20 if fields and not f.name in fields:
22 if exclude and f.name in exclude:
24 if widgets and f.name in widgets:
25 kwargs = {'widget': widgets[f.name]}
28 formfield = formfield_callback(f, **kwargs)
30 field_list.append((f.name, formfield))
32 ignored.append(f.name)
33 field_dict = SortedDict(field_list)
35 field_dict = SortedDict(
36 [(f, field_dict.get(f)) for f in fields
37 if ((not exclude) or (exclude and f not in exclude)) and (f not in ignored) and (f in field_dict)]
42 # BEGIN HACK - This will not be required after http://code.djangoproject.com/ticket/14082 has been resolved
44 class EntityFormBase(ModelForm):
47 _old_metaclass_new = ModelFormMetaclass.__new__
49 def _new_metaclass_new(cls, name, bases, attrs):
50 new_class = _old_metaclass_new(cls, name, bases, attrs)
51 if issubclass(new_class, EntityFormBase) and new_class._meta.model:
52 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
55 ModelFormMetaclass.__new__ = staticmethod(_new_metaclass_new)
60 class EntityForm(EntityFormBase): # Would inherit from ModelForm directly if it weren't for the above HACK
61 def __init__(self, *args, **kwargs):
62 initial = kwargs.pop('initial', None)
63 instance = kwargs.get('instance', None)
64 if instance is not None:
66 for f in instance._entity_meta.proxy_fields:
67 if self._meta.fields and not f.name in self._meta.fields:
69 if self._meta.exclude and f.name in self._meta.exclude:
71 new_initial[f.name] = f.value_from_object(instance)
74 if initial is not None:
75 new_initial.update(initial)
76 kwargs['initial'] = new_initial
77 super(EntityForm, self).__init__(*args, **kwargs)
79 @fattr(alters_data=True)
80 def save(self, commit=True):
81 cleaned_data = self.cleaned_data
82 instance = super(EntityForm, self).save(commit=False)
84 for f in instance._entity_meta.proxy_fields:
85 if not f.editable or not f.name in cleaned_data:
87 if self._meta.fields and f.name not in self._meta.fields:
89 if self._meta.exclude and f.name in self._meta.exclude:
91 setattr(instance, f.attname, cleaned_data[f.name])
100 def apply_data(self, cleaned_data):
101 self.value = cleaned_data.get('value', None)
103 def apply_data(self, cleaned_data):
104 if 'value' in cleaned_data and cleaned_data['value'] is not None:
105 self.value = cleaned_data['value']
107 self.content_type = cleaned_data.get('content_type', None)
108 # If there is no value set in the cleaned data, clear the stored value.
109 self.object_id = None
111 def apply_data(self, cleaned_data):
112 if 'value' in cleaned_data and cleaned_data['value'] is not None:
113 self.value = cleaned_data['value']
115 self.content_type = cleaned_data.get('content_type', None)
116 # If there is no value set in the cleaned data, clear the stored value.
119 class AttributeForm(ModelForm):
121 This class handles an attribute's fields as well as the fields for its value (if there is one.)
122 The fields defined will vary depending on the value type, but the fields for defining the value
123 (i.e. value_content_type and value_object_id) will always be defined. Except that value_object_id
124 will never be defined. BLARGH!
126 def __init__(self, *args, **kwargs):
127 super(AttributeForm, self).__init__(*args, **kwargs)
129 # This is necessary because model forms store changes to self.instance in their clean method.
131 value = self.instance.value
132 self._cached_value_ct = self.instance.value_content_type
133 self._cached_value = value
135 # If there is a value, pull in its fields.
136 if value is not None:
137 self.value_fields = value.value_formfields()
138 self.fields.update(self.value_fields)
140 def save(self, *args, **kwargs):
141 # At this point, the cleaned_data has already been stored on self.instance.
143 if self.instance.value_content_type != self._cached_value_ct:
144 # The value content type has changed. Clear the old value, if there was one.
145 if self._cached_value:
146 self._cached_value.delete()
148 # Clear the submitted value, if any.
149 self.cleaned_data.pop('value', None)
151 # Now create a new value instance so that on next instantiation, the form will
152 # know what fields to add.
153 if self.instance.value_content_type is not None:
154 self.instance.value = self.instance.value_content_type.model_class().objects.create()
155 elif self.instance.value is not None:
156 # The value content type is the same, but one of the value fields has changed.
158 # Use construct_instance to apply the changes from the cleaned_data to the value instance.
159 fields = self.value_fields.keys()
160 if set(fields) & set(self.changed_data):
161 self.instance.value.construct_instance(**dict([(key, self.cleaned_data[key]) for key in fields]))
162 self.instance.value.save()
164 return super(AttributeForm, self).save(*args, **kwargs)
170 class AttributeInlineFormSet(BaseGenericInlineFormSet):
171 "Necessary to force the GenericInlineFormset to use the form's save method for new objects."
172 def save_new(self, form, commit):
173 setattr(form.instance, self.ct_field.get_attname(), ContentType.objects.get_for_model(self.instance).pk)
174 setattr(form.instance, self.ct_fk_field.get_attname(), self.instance.pk)