a392e45f0d98802f194f78988e50a12cd2bf4dcd
[philo.git] / forms / entities.py
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
8
9
10 __all__ = ('EntityForm', 'AttributeForm', 'AttributeInlineFormSet')
11
12
13 def proxy_fields_for_entity_model(entity_model, fields=None, exclude=None, widgets=None, formfield_callback=lambda f, **kwargs: f.formfield(**kwargs)):
14         field_list = []
15         ignored = []
16         opts = entity_model._entity_meta
17         for f in opts.proxy_fields:
18                 if not f.editable:
19                         continue
20                 if fields and not f.name in fields:
21                         continue
22                 if exclude and f.name in exclude:
23                         continue
24                 if widgets and f.name in widgets:
25                         kwargs = {'widget': widgets[f.name]}
26                 else:
27                         kwargs = {}
28                 formfield = formfield_callback(f, **kwargs)
29                 if formfield:
30                         field_list.append((f.name, formfield))
31                 else:
32                         ignored.append(f.name)
33         field_dict = SortedDict(field_list)
34         if fields:
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)]
38                 )
39         return field_dict
40
41
42 # BEGIN HACK - This will not be required after http://code.djangoproject.com/ticket/14082 has been resolved
43
44 class EntityFormBase(ModelForm):
45         pass
46
47 _old_metaclass_new = ModelFormMetaclass.__new__
48
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
53         return new_class
54
55 ModelFormMetaclass.__new__ = staticmethod(_new_metaclass_new)
56
57 # END HACK
58
59
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:
65                         new_initial = {}
66                         for f in instance._entity_meta.proxy_fields:
67                                 if self._meta.fields and not f.name in self._meta.fields:
68                                         continue
69                                 if self._meta.exclude and f.name in self._meta.exclude:
70                                         continue
71                                 new_initial[f.name] = f.value_from_object(instance)
72                 else:
73                         new_initial = {}
74                 if initial is not None:
75                         new_initial.update(initial)
76                 kwargs['initial'] = new_initial
77                 super(EntityForm, self).__init__(*args, **kwargs)
78         
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)
83                 
84                 for f in instance._entity_meta.proxy_fields:
85                         if not f.editable or not f.name in cleaned_data:
86                                 continue
87                         if self._meta.fields and f.name not in self._meta.fields:
88                                 continue
89                         if self._meta.exclude and f.name in self._meta.exclude:
90                                 continue
91                         setattr(instance, f.attname, cleaned_data[f.name])
92                 
93                 if commit:
94                         instance.save()
95                         self.save_m2m()
96                 
97                 return instance
98
99         
100         def apply_data(self, cleaned_data):
101                 self.value = cleaned_data.get('value', None)
102         
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']
106                 else:
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
110         
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']
114                 else:
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.
117                         self.value = []
118
119 class AttributeForm(ModelForm):
120         """
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!
125         """
126         def __init__(self, *args, **kwargs):
127                 super(AttributeForm, self).__init__(*args, **kwargs)
128                 
129                 # This is necessary because model forms store changes to self.instance in their clean method.
130                 # Mutter mutter.
131                 value = self.instance.value
132                 self._cached_value_ct = self.instance.value_content_type
133                 self._cached_value = value
134                 
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)
139         
140         def save(self, *args, **kwargs):
141                 # At this point, the cleaned_data has already been stored on self.instance.
142                 
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()
147                         
148                         # Clear the submitted value, if any.
149                         self.cleaned_data.pop('value', None)
150                         
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.
157                         
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()
163                 
164                 return super(AttributeForm, self).save(*args, **kwargs)
165         
166         class Meta:
167                 model = Attribute
168
169
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)
175                 return form.save()