43094b975d881304dcc10123ec15beb480c7daa5
[philo.git] / forms / entities.py
1 from django.forms.models import ModelFormMetaclass, ModelForm, ModelFormOptions
2 from django.utils.datastructures import SortedDict
3 from philo.utils import fattr
4
5
6 __all__ = ('EntityForm',)
7
8
9 def proxy_fields_for_entity_model(entity_model, fields=None, exclude=None, widgets=None, formfield_callback=None):
10         field_list = []
11         ignored = []
12         opts = entity_model._entity_meta
13         for f in opts.proxy_fields:
14                 if not f.editable:
15                         continue
16                 if fields and not f.name in fields:
17                         continue
18                 if exclude and f.name in exclude:
19                         continue
20                 if widgets and f.name in widgets:
21                         kwargs = {'widget': widgets[f.name]}
22                 else:
23                         kwargs = {}
24                 
25                 if formfield_callback is None:
26                         formfield = f.formfield(**kwargs)
27                 elif not callable(formfield_callback):
28                         raise TypeError('formfield_callback must be a function or callable')
29                 else:
30                         formfield = formfield_callback(f, **kwargs)
31                 
32                 if formfield:
33                         field_list.append((f.name, formfield))
34                 else:
35                         ignored.append(f.name)
36         field_dict = SortedDict(field_list)
37         if fields:
38                 field_dict = SortedDict(
39                         [(f, field_dict.get(f)) for f in fields
40                                 if ((not exclude) or (exclude and f not in exclude)) and (f not in ignored) and (f in field_dict)]
41                 )
42         return field_dict
43
44
45 class EntityFormMetaclass(ModelFormMetaclass):
46         def __new__(cls, name, bases, attrs):
47                 try:
48                         parents = [b for b in bases if issubclass(b, EntityForm)]
49                 except NameError:
50                         # We are defining EntityForm itself
51                         parents = None
52                 sup = super(EntityFormMetaclass, cls)
53                 
54                 if not parents:
55                         # Then there's no business trying to use proxy fields.
56                         return sup.__new__(cls, name, bases, attrs)
57                 
58                 # Preemptively make a meta so we can screen out any proxy fields.
59                 # After http://code.djangoproject.com/ticket/14082 has been resolved,
60                 # perhaps switch to setting proxy fields as "declared"?
61                 _opts = ModelFormOptions(attrs.get('Meta', None))
62                 
63                 # Make another copy of opts to spoof the proxy fields not being there.
64                 opts = ModelFormOptions(attrs.get('Meta', None))
65                 if opts.model:
66                         formfield_callback = attrs.get('formfield_callback', None)
67                         proxy_fields = proxy_fields_for_entity_model(opts.model, opts.fields, opts.exclude, opts.widgets, formfield_callback)
68                 else:
69                         proxy_fields = {}
70                 
71                 # Screen out all proxy fields from the meta
72                 opts.fields = list(opts.fields or [])
73                 opts.exclude = list(opts.exclude or [])
74                 
75                 for field in proxy_fields.keys():
76                         try:
77                                 opts.fields.remove(field)
78                         except ValueError:
79                                 pass
80                         try:
81                                 opts.exclude.remove(field)
82                         except ValueError:
83                                 pass
84                 opts.fields = opts.fields or None
85                 opts.exclude = opts.exclude or None
86                 
87                 # Put in the new Meta.
88                 attrs['Meta'] = opts
89                 
90                 new_class = sup.__new__(cls, name, bases, attrs)
91                 
92                 intersections = set(new_class.declared_fields.keys()) & set(proxy_fields.keys())
93                 for key in intersections:
94                         proxy_fields.pop(key)
95                 
96                 new_class.proxy_fields = proxy_fields
97                 new_class._meta = _opts
98                 new_class.base_fields.update(proxy_fields)
99                 return new_class
100
101
102 class EntityForm(ModelForm):
103         __metaclass__ = EntityFormMetaclass
104         
105         def __init__(self, *args, **kwargs):
106                 initial = kwargs.pop('initial', None)
107                 instance = kwargs.get('instance', None)
108                 if instance is not None:
109                         new_initial = {}
110                         for f in instance._entity_meta.proxy_fields:
111                                 if self._meta.fields and not f.name in self._meta.fields:
112                                         continue
113                                 if self._meta.exclude and f.name in self._meta.exclude:
114                                         continue
115                                 new_initial[f.name] = f.value_from_object(instance)
116                 else:
117                         new_initial = {}
118                 if initial is not None:
119                         new_initial.update(initial)
120                 kwargs['initial'] = new_initial
121                 super(EntityForm, self).__init__(*args, **kwargs)
122         
123         @fattr(alters_data=True)
124         def save(self, commit=True):
125                 cleaned_data = self.cleaned_data
126                 instance = super(EntityForm, self).save(commit=False)
127                 
128                 for f in instance._entity_meta.proxy_fields:
129                         if not f.editable or not f.name in cleaned_data:
130                                 continue
131                         if self._meta.fields and f.name not in self._meta.fields:
132                                 continue
133                         if self._meta.exclude and f.name in self._meta.exclude:
134                                 continue
135                         setattr(instance, f.attname, f.get_storage_value(cleaned_data[f.name]))
136                 
137                 if commit:
138                         instance.save()
139                         self.save_m2m()
140                 
141                 return instance