Fixing setup.py to reference README instead of README.markdown as otherwise sdist...
[philo.git] / philo / forms / entities.py
1 from django.forms.models import ModelFormMetaclass, ModelForm, ModelFormOptions
2 from django.utils.datastructures import SortedDict
3
4 from philo.utils import fattr
5
6
7 __all__ = ('EntityForm',)
8
9
10 def proxy_fields_for_entity_model(entity_model, fields=None, exclude=None, widgets=None, formfield_callback=None):
11         field_list = []
12         ignored = []
13         opts = entity_model._entity_meta
14         for f in opts.proxy_fields:
15                 if not f.editable:
16                         continue
17                 if fields and not f.name in fields:
18                         continue
19                 if exclude and f.name in exclude:
20                         continue
21                 if widgets and f.name in widgets:
22                         kwargs = {'widget': widgets[f.name]}
23                 else:
24                         kwargs = {}
25                 
26                 if formfield_callback is None:
27                         formfield = f.formfield(**kwargs)
28                 elif not callable(formfield_callback):
29                         raise TypeError('formfield_callback must be a function or callable')
30                 else:
31                         formfield = formfield_callback(f, **kwargs)
32                 
33                 if formfield:
34                         field_list.append((f.name, formfield))
35                 else:
36                         ignored.append(f.name)
37         field_dict = SortedDict(field_list)
38         if fields:
39                 field_dict = SortedDict(
40                         [(f, field_dict.get(f)) for f in fields
41                                 if ((not exclude) or (exclude and f not in exclude)) and (f not in ignored) and (f in field_dict)]
42                 )
43         return field_dict
44
45
46 # HACK until http://code.djangoproject.com/ticket/14082 is resolved.
47 _old = ModelFormMetaclass.__new__
48 def _new(cls, name, bases, attrs):
49         if cls == ModelFormMetaclass:
50                 m = attrs.get('__metaclass__', None)
51                 if m is None:
52                         parents = [b for b in bases if issubclass(b, ModelForm)]
53                         for c in parents:
54                                 if c.__metaclass__ != ModelFormMetaclass:
55                                         m = c.__metaclass__
56                                         break
57         
58                 if m is not None:
59                         return m(name, bases, attrs)
60         
61         return _old(cls, name, bases, attrs)
62 ModelFormMetaclass.__new__ = staticmethod(_new)
63 # END HACK
64
65
66 class EntityFormMetaclass(ModelFormMetaclass):
67         def __new__(cls, name, bases, attrs):
68                 try:
69                         parents = [b for b in bases if issubclass(b, EntityForm)]
70                 except NameError:
71                         # We are defining EntityForm itself
72                         parents = None
73                 sup = super(EntityFormMetaclass, cls)
74                 
75                 if not parents:
76                         # Then there's no business trying to use proxy fields.
77                         return sup.__new__(cls, name, bases, attrs)
78                 
79                 # Fake a declaration of all proxy fields so they'll be handled correctly.
80                 opts = ModelFormOptions(attrs.get('Meta', None))
81                 
82                 if opts.model:
83                         formfield_callback = attrs.get('formfield_callback', None)
84                         proxy_fields = proxy_fields_for_entity_model(opts.model, opts.fields, opts.exclude, opts.widgets, formfield_callback)
85                 else:
86                         proxy_fields = {}
87                 
88                 new_attrs = proxy_fields.copy()
89                 new_attrs.update(attrs)
90                 
91                 new_class = sup.__new__(cls, name, bases, new_attrs)
92                 new_class.proxy_fields = proxy_fields
93                 return new_class
94
95
96 class EntityForm(ModelForm):
97         """
98         :class:`EntityForm` knows how to handle :class:`.Entity` instances - specifically, how to set initial values for :class:`.AttributeProxyField`\ s and save cleaned values to an instance on save.
99         
100         """
101         __metaclass__ = EntityFormMetaclass
102         
103         def __init__(self, *args, **kwargs):
104                 initial = kwargs.pop('initial', None)
105                 instance = kwargs.get('instance', None)
106                 if instance is not None:
107                         new_initial = {}
108                         for f in instance._entity_meta.proxy_fields:
109                                 if self._meta.fields and not f.name in self._meta.fields:
110                                         continue
111                                 if self._meta.exclude and f.name in self._meta.exclude:
112                                         continue
113                                 new_initial[f.name] = f.value_from_object(instance)
114                 else:
115                         new_initial = {}
116                 if initial is not None:
117                         new_initial.update(initial)
118                 kwargs['initial'] = new_initial
119                 super(EntityForm, self).__init__(*args, **kwargs)
120         
121         @fattr(alters_data=True)
122         def save(self, commit=True):
123                 cleaned_data = self.cleaned_data
124                 instance = super(EntityForm, self).save(commit=False)
125                 
126                 for f in instance._entity_meta.proxy_fields:
127                         if not f.editable or not f.name in cleaned_data:
128                                 continue
129                         if self._meta.fields and f.name not in self._meta.fields:
130                                 continue
131                         if self._meta.exclude and f.name in self._meta.exclude:
132                                 continue
133                         setattr(instance, f.attname, f.get_storage_value(cleaned_data[f.name]))
134                 
135                 if commit:
136                         instance.save()
137                         self.save_m2m()
138                 
139                 return instance