Merge branch 'master' into navigation
[philo.git] / forms.py
1 from django import forms
2 from django.contrib.admin.widgets import AdminTextareaWidget
3 from django.contrib.contenttypes.generic import BaseGenericInlineFormSet
4 from django.contrib.contenttypes.models import ContentType
5 from django.core.exceptions import ValidationError, ObjectDoesNotExist
6 from django.db.models import Q
7 from django.forms.models import model_to_dict, fields_for_model, ModelFormMetaclass, ModelForm, BaseInlineFormSet
8 from django.forms.formsets import TOTAL_FORM_COUNT
9 from django.template import loader, loader_tags, TemplateDoesNotExist, Context, Template as DjangoTemplate
10 from django.utils.datastructures import SortedDict
11 from philo.admin.widgets import ModelLookupWidget
12 from philo.models import Entity, Template, Contentlet, ContentReference, Attribute, Node, NodeNavigationOverride
13 from philo.utils import fattr
14
15
16 __all__ = ('EntityForm', )
17
18
19 def proxy_fields_for_entity_model(entity_model, fields=None, exclude=None, widgets=None, formfield_callback=lambda f, **kwargs: f.formfield(**kwargs)):
20         field_list = []
21         ignored = []
22         opts = entity_model._entity_meta
23         for f in opts.proxy_fields:
24                 if not f.editable:
25                         continue
26                 if fields and not f.name in fields:
27                         continue
28                 if exclude and f.name in exclude:
29                         continue
30                 if widgets and f.name in widgets:
31                         kwargs = {'widget': widgets[f.name]}
32                 else:
33                         kwargs = {}
34                 formfield = formfield_callback(f, **kwargs)
35                 if formfield:
36                         field_list.append((f.name, formfield))
37                 else:
38                         ignored.append(f.name)
39         field_dict = SortedDict(field_list)
40         if fields:
41                 field_dict = SortedDict(
42                         [(f, field_dict.get(f)) for f in fields
43                                 if ((not exclude) or (exclude and f not in exclude)) and (f not in ignored) and (f in field_dict)]
44                 )
45         return field_dict
46
47
48 # BEGIN HACK - This will not be required after http://code.djangoproject.com/ticket/14082 has been resolved
49
50 class EntityFormBase(ModelForm):
51         pass
52
53 _old_metaclass_new = ModelFormMetaclass.__new__
54
55 def _new_metaclass_new(cls, name, bases, attrs):
56         new_class = _old_metaclass_new(cls, name, bases, attrs)
57         if issubclass(new_class, EntityFormBase) and new_class._meta.model:
58                 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
59         return new_class
60
61 ModelFormMetaclass.__new__ = staticmethod(_new_metaclass_new)
62
63 # END HACK
64
65
66 class EntityForm(EntityFormBase): # Would inherit from ModelForm directly if it weren't for the above HACK
67         def __init__(self, *args, **kwargs):
68                 initial = kwargs.pop('initial', None)
69                 instance = kwargs.get('instance', None)
70                 if instance is not None:
71                         new_initial = {}
72                         for f in instance._entity_meta.proxy_fields:
73                                 if self._meta.fields and not f.name in self._meta.fields:
74                                         continue
75                                 if self._meta.exclude and f.name in self._meta.exclude:
76                                         continue
77                                 new_initial[f.name] = f.value_from_object(instance)
78                 else:
79                         new_initial = {}
80                 if initial is not None:
81                         new_initial.update(initial)
82                 kwargs['initial'] = new_initial
83                 super(EntityForm, self).__init__(*args, **kwargs)
84         
85         @fattr(alters_data=True)
86         def save(self, commit=True):
87                 cleaned_data = self.cleaned_data
88                 instance = super(EntityForm, self).save(commit=False)
89                 
90                 for f in instance._entity_meta.proxy_fields:
91                         if not f.editable or not f.name in cleaned_data:
92                                 continue
93                         if self._meta.fields and f.name not in self._meta.fields:
94                                 continue
95                         if self._meta.exclude and f.name in self._meta.exclude:
96                                 continue
97                         setattr(instance, f.attname, cleaned_data[f.name])
98                 
99                 if commit:
100                         instance.save()
101                         self.save_m2m()
102                 
103                 return instance
104
105
106 class AttributeForm(ModelForm):
107         def __init__(self, *args, **kwargs):
108                 super(AttributeForm, self).__init__(*args, **kwargs)
109                 
110                 # This is necessary because model forms store changes to self.instance in their clean method.
111                 # Mutter mutter.
112                 self._cached_value_ct = self.instance.value_content_type
113                 self._cached_value = self.instance.value
114                 
115                 if self.instance.value is not None:
116                         value_field = self.instance.value.value_formfield()
117                         if value_field:
118                                 self.fields['value'] = value_field
119                         if hasattr(self.instance.value, 'content_type'):
120                                 self.fields['content_type'] = self.instance.value._meta.get_field('content_type').formfield(initial=getattr(self.instance.value.content_type, 'pk', None))
121         
122         def save(self, *args, **kwargs):
123                 # At this point, the cleaned_data has already been stored on self.instance.
124                 if self.instance.value_content_type != self._cached_value_ct:
125                         if self.instance.value is not None:
126                                 self._cached_value.delete()
127                                 if 'value' in self.cleaned_data:
128                                         del(self.cleaned_data['value'])
129                         
130                         if self.instance.value_content_type is not None:
131                                 # Make a blank value of the new type! Run special code for content_type attributes.
132                                 if hasattr(self.instance.value_content_type.model_class(), 'content_type'):
133                                         if self._cached_value and hasattr(self._cached_value, 'content_type'):
134                                                 new_ct = self._cached_value.content_type
135                                         else:
136                                                 new_ct = None
137                                         new_value = self.instance.value_content_type.model_class().objects.create(content_type=new_ct)
138                                 else:
139                                         new_value = self.instance.value_content_type.model_class().objects.create()
140                                 
141                                 new_value.apply_data(self.cleaned_data)
142                                 new_value.save()
143                                 self.instance.value = new_value
144                 else:
145                         # The value type is the same, but one of the fields has changed.
146                         # Check to see if the changed value was the content type. We have to check the
147                         # cleaned_data because self.instance.value.content_type was overridden.
148                         if hasattr(self.instance.value, 'content_type') and 'content_type' in self.cleaned_data and 'value' in self.cleaned_data and (not hasattr(self._cached_value, 'content_type') or self._cached_value.content_type != self.cleaned_data['content_type']):
149                                 self.cleaned_data['value'] = None
150                         
151                         self.instance.value.apply_data(self.cleaned_data)
152                         self.instance.value.save()
153                 
154                 super(AttributeForm, self).save(*args, **kwargs)
155                 return self.instance
156         
157         class Meta:
158                 model = Attribute
159
160
161 class AttributeInlineFormSet(BaseGenericInlineFormSet):
162         "Necessary to force the GenericInlineFormset to use the form's save method for new objects."
163         def save_new(self, form, commit):
164                 setattr(form.instance, self.ct_field.get_attname(), ContentType.objects.get_for_model(self.instance).pk)
165                 setattr(form.instance, self.ct_fk_field.get_attname(), self.instance.pk)
166                 return form.save()
167
168
169 class ContainerForm(ModelForm):
170         def __init__(self, *args, **kwargs):
171                 super(ContainerForm, self).__init__(*args, **kwargs)
172                 self.verbose_name = self.instance.name.replace('_', ' ')
173
174
175 class ContentletForm(ContainerForm):
176         content = forms.CharField(required=False, widget=AdminTextareaWidget, label='Content')
177         
178         def should_delete(self):
179                 return not bool(self.cleaned_data['content'])
180         
181         class Meta:
182                 model = Contentlet
183                 fields = ['name', 'content']
184
185
186 class ContentReferenceForm(ContainerForm):
187         def __init__(self, *args, **kwargs):
188                 super(ContentReferenceForm, self).__init__(*args, **kwargs)
189                 try:
190                         self.fields['content_id'].widget = ModelLookupWidget(self.instance.content_type)
191                 except ObjectDoesNotExist:
192                         # This will happen when an empty form (which we will never use) gets instantiated.
193                         pass
194         
195         def should_delete(self):
196                 return (self.cleaned_data['content_id'] is None)
197         
198         class Meta:
199                 model = ContentReference
200                 fields = ['name', 'content_id']
201
202
203 class ContainerInlineFormSet(BaseInlineFormSet):
204         def __init__(self, containers, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=None):
205                 # Unfortunately, I need to add some things to BaseInline between its __init__ and its
206                 # super call, so a lot of this is repetition.
207                 
208                 # Start cribbed from BaseInline
209                 from django.db.models.fields.related import RelatedObject
210                 self.save_as_new = save_as_new
211                 # is there a better way to get the object descriptor?
212                 self.rel_name = RelatedObject(self.fk.rel.to, self.model, self.fk).get_accessor_name()
213                 if self.fk.rel.field_name == self.fk.rel.to._meta.pk.name:
214                         backlink_value = self.instance
215                 else:
216                         backlink_value = getattr(self.instance, self.fk.rel.field_name)
217                 if queryset is None:
218                         queryset = self.model._default_manager
219                 qs = queryset.filter(**{self.fk.name: backlink_value})
220                 # End cribbed from BaseInline
221                 
222                 self.container_instances, qs = self.get_container_instances(containers, qs)
223                 self.extra_containers = containers
224                 self.extra = len(self.extra_containers)
225                 super(BaseInlineFormSet, self).__init__(data, files, prefix=prefix, queryset=qs)
226         
227         def get_container_instances(self, containers, qs):
228                 raise NotImplementedError
229         
230         def total_form_count(self):
231                 if self.data or self.files:
232                         return self.management_form.cleaned_data[TOTAL_FORM_COUNT]
233                 else:
234                         return self.initial_form_count() + self.extra
235         
236         def save_existing_objects(self, commit=True):
237                 self.changed_objects = []
238                 self.deleted_objects = []
239                 if not self.get_queryset():
240                         return []
241
242                 saved_instances = []
243                 for form in self.initial_forms:
244                         pk_name = self._pk_field.name
245                         raw_pk_value = form._raw_value(pk_name)
246
247                         # clean() for different types of PK fields can sometimes return
248                         # the model instance, and sometimes the PK. Handle either.
249                         pk_value = form.fields[pk_name].clean(raw_pk_value)
250                         pk_value = getattr(pk_value, 'pk', pk_value)
251
252                         obj = self._existing_object(pk_value)
253                         if form.should_delete():
254                                 self.deleted_objects.append(obj)
255                                 obj.delete()
256                                 continue
257                         if form.has_changed():
258                                 self.changed_objects.append((obj, form.changed_data))
259                                 saved_instances.append(self.save_existing(form, obj, commit=commit))
260                                 if not commit:
261                                         self.saved_forms.append(form)
262                 return saved_instances
263
264         def save_new_objects(self, commit=True):
265                 self.new_objects = []
266                 for form in self.extra_forms:
267                         if not form.has_changed():
268                                 continue
269                         # If someone has marked an add form for deletion, don't save the
270                         # object.
271                         if form.should_delete():
272                                 continue
273                         self.new_objects.append(self.save_new(form, commit=commit))
274                         if not commit:
275                                 self.saved_forms.append(form)
276                 return self.new_objects
277
278
279 class ContentletInlineFormSet(ContainerInlineFormSet):
280         def __init__(self, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=None):
281                 if instance is None:
282                         self.instance = self.fk.rel.to()
283                 else:
284                         self.instance = instance
285                 
286                 try:
287                         containers = list(self.instance.containers[0])
288                 except ObjectDoesNotExist:
289                         containers = []
290         
291                 super(ContentletInlineFormSet, self).__init__(containers, data, files, instance, save_as_new, prefix, queryset)
292         
293         def get_container_instances(self, containers, qs):
294                 qs = qs.filter(name__in=containers)
295                 container_instances = []
296                 for container in qs:
297                         container_instances.append(container)
298                         containers.remove(container.name)
299                 return container_instances, qs
300         
301         def _construct_form(self, i, **kwargs):
302                 if i >= self.initial_form_count(): # and not kwargs.get('instance'):
303                         kwargs['instance'] = self.model(name=self.extra_containers[i - self.initial_form_count() - 1])
304                 
305                 return super(ContentletInlineFormSet, self)._construct_form(i, **kwargs)
306
307
308 class ContentReferenceInlineFormSet(ContainerInlineFormSet):
309         def __init__(self, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=None):
310                 if instance is None:
311                         self.instance = self.fk.rel.to()
312                 else:
313                         self.instance = instance
314                 
315                 try:
316                         containers = list(self.instance.containers[1])
317                 except ObjectDoesNotExist:
318                         containers = []
319         
320                 super(ContentReferenceInlineFormSet, self).__init__(containers, data, files, instance, save_as_new, prefix, queryset)
321         
322         def get_container_instances(self, containers, qs):
323                 filter = Q()
324                 
325                 for name, ct in containers:
326                         filter |= Q(name=name, content_type=ct)
327                 
328                 qs = qs.filter(filter)
329                 container_instances = []
330                 for container in qs:
331                         container_instances.append(container)
332                         containers.remove((container.name, container.content_type))
333                 return container_instances, qs
334
335         def _construct_form(self, i, **kwargs):
336                 if i >= self.initial_form_count(): # and not kwargs.get('instance'):
337                         name, content_type = self.extra_containers[i - self.initial_form_count() - 1]
338                         kwargs['instance'] = self.model(name=name, content_type=content_type)
339
340                 return super(ContentReferenceInlineFormSet, self)._construct_form(i, **kwargs)
341
342
343 class NodeWithOverrideForm(forms.ModelForm):
344         title = NodeNavigationOverride._meta.get_field('title').formfield()
345         url = NodeNavigationOverride._meta.get_field('url').formfield()
346         child_navigation = NodeNavigationOverride._meta.get_field('child_navigation').formfield(required=False)
347         
348         def __init__(self, *args, **kwargs):
349                 super(NodeWithOverrideForm, self).__init__(*args, **kwargs)
350                 if self.instance.pk:
351                         self._override = override = self.get_override(self.instance)
352                         self.initial.update({
353                                 'title': override.title,
354                                 'url': override.url,
355                                 'child_navigation': override.child_navigation_json
356                         })
357         
358         def get_override(self, instance):
359                 try:
360                         return NodeNavigationOverride.objects.get(parent=self.instance.parent, child=self.instance)
361                 except NodeNavigationOverride.DoesNotExist:
362                         override = NodeNavigationOverride(parent=self.instance.parent, child=self.instance)
363                         override.child_navigation = None
364                         return override
365         
366         def save(self, commit=True):
367                 obj = super(NodeWithOverrideForm, self).save(commit)
368                 cleaned_data = self.cleaned_data
369                 override = self.get_override(obj)
370                         
371                 # Override information should only be set if there was no previous override or if the
372                 # information was just manually set - i.e. was not equal to the data on the cached override.
373                 if not override.pk or cleaned_data['title'] != self._override.title or cleaned_data['url'] != self._override.url or cleaned_data['child_navigation'] != self._override.child_navigation:
374                         override.title = self.cleaned_data['title']
375                         override.url = self.cleaned_data['url']
376                         override.child_navigation = self.cleaned_data['child_navigation']
377                         override.save()
378                 return obj
379         
380         class Meta:
381                 model = Node
382
383
384 class NodeOverrideInlineFormSet(BaseInlineFormSet):
385         def __init__(self, data=None, files=None, instance=None, save_as_new=False, prefix=None, queryset=None):
386                 if queryset is None:
387                         queryset = self.model._default_manager
388                 queryset = queryset.filter(parent=instance, child__parent=instance)
389                 super(NodeOverrideInlineFormSet, self).__init__(data, files, instance, save_as_new, prefix, queryset)
390         
391         def add_fields(self, form, index):
392                 super(NodeOverrideInlineFormSet, self).add_fields(form, index)
393                 form.fields['child'].queryset = self.instance.children.all()