1 | from django.forms import forms
|
---|
2 | from django.forms import models as forms_models
|
---|
3 |
|
---|
4 | def intersect(a, b):
|
---|
5 | """
|
---|
6 | Finds the intersection of two dictionaries.
|
---|
7 |
|
---|
8 | A key and value pair is included in the result only if the key exists in both given dictionaries. Value is taken from
|
---|
9 | the second dictionary.
|
---|
10 | """
|
---|
11 |
|
---|
12 | return dict(filter(lambda (x, y): x in a, b.items()))
|
---|
13 |
|
---|
14 | class ParentsIncludedModelFormMetaclass(forms_models.ModelFormMetaclass):
|
---|
15 | """
|
---|
16 | `django.forms.models.ModelFormMetaclass` produces only all declared fields of the current and parent clasess combined with
|
---|
17 | fields from the model as defined in `Meta` subclass which is taken from the first class which defines it (as `getattr` finds it).
|
---|
18 | This metaclass adds also all other fields of parent classes to the current class.
|
---|
19 |
|
---|
20 | Workaround for http://code.djangoproject.com/ticket/7018.
|
---|
21 |
|
---|
22 | It works only on parent classes and not all ancestor classes.
|
---|
23 |
|
---|
24 | The order of fields could be probably improved. It would be much easier if `django.forms.models.ModelFormMetaclass` would
|
---|
25 | simply use `True` in `django.forms.forms.get_declared_fields`.
|
---|
26 |
|
---|
27 | `Meta` (`self._meta` attribute) is not merged but taken from the first class which defines it (as `getattr` finds it).
|
---|
28 |
|
---|
29 | Warning: This also means that all `django.forms.models.ModelForm` methods which operate on a model (like `save` and `clean`)
|
---|
30 | use only model from the first class which defines it. Use `ParentsIncludedModelFormMixin` for methods which operate also on
|
---|
31 | parent `django.forms.models.ModelForm` classes.
|
---|
32 |
|
---|
33 | It should be used as a `__metaclass__` class of the given multi-parent form class.
|
---|
34 | """
|
---|
35 |
|
---|
36 | def __new__(cls, name, bases, attrs):
|
---|
37 | # We store attrs as ModelFormMetaclass.__new__ clears all fields from it
|
---|
38 | attrs_copy = attrs.copy()
|
---|
39 | new_class = super(ParentsIncludedModelFormMetaclass, cls).__new__(cls, name, bases, attrs)
|
---|
40 | # All declared fields + model fields from parent classes
|
---|
41 | fields_without_current_model = forms.get_declared_fields(bases, attrs_copy, True)
|
---|
42 | new_class.base_fields.update(fields_without_current_model)
|
---|
43 | return new_class
|
---|
44 |
|
---|
45 | class ParentsIncludedModelFormMixin(object):
|
---|
46 | """
|
---|
47 | When combinining multiple forms based on `django.forms.models.ModelForm` into one form default methods operate only on the
|
---|
48 | first object instance (as `getattr` finds it) and also do not allow multiple initial instances to be passed to form
|
---|
49 | constructor. This mixin class redefines those methods to operate on multiple instances.
|
---|
50 |
|
---|
51 | It should be listed as the parent class before `django.forms.models.ModelForm` based classes so that methods here take
|
---|
52 | precedence.
|
---|
53 | """
|
---|
54 |
|
---|
55 | def __init__(self, *args, **kwargs):
|
---|
56 | """
|
---|
57 | Populates `self.instances` and `self.metas` with the given (or constructed empty) instances and `Meta` classes of the current
|
---|
58 | and parent (but not all ancestor) classes.
|
---|
59 |
|
---|
60 | Based on `django.forms.models.BaseModelForm.__init__` method and extended for multiple instances.
|
---|
61 |
|
---|
62 | Optional `instance` argument should be a list of all instances for the current and parent `django.forms.models.ModelForm`
|
---|
63 | classes with defined `Meta` class (with now required `model` attribute).
|
---|
64 | """
|
---|
65 |
|
---|
66 | self.metas = []
|
---|
67 | if 'Meta' in self.__class__.__dict__:
|
---|
68 | # We add meta of the current class
|
---|
69 | self.metas.append(forms_models.ModelFormOptions(self.Meta))
|
---|
70 | # We add metas from parent classes
|
---|
71 | self.metas += [forms_models.ModelFormOptions(getattr(cls, 'Meta', None)) for cls in self.__class__.__bases__ if issubclass(cls, forms_models.ModelForm)]
|
---|
72 |
|
---|
73 | instances = kwargs.pop('instance', None)
|
---|
74 | if instances is None:
|
---|
75 | for meta in self.metas:
|
---|
76 | if meta.model is None:
|
---|
77 | raise ValueError('ModelForm has no model class specified.')
|
---|
78 | self.instances = [meta.model() for meta in self.metas]
|
---|
79 | for instance in self.instances:
|
---|
80 | instance._adding = True
|
---|
81 | object_data = {}
|
---|
82 | else:
|
---|
83 | self.instances = instances
|
---|
84 | for instance in self.instances:
|
---|
85 | instance._adding = False
|
---|
86 | object_data = {}
|
---|
87 | if len(instances) != len(self.metas):
|
---|
88 | raise ValueError('Number of instances does not match number of metas.')
|
---|
89 | # We traverse in reverse order to keep in sync with get_declared_fields
|
---|
90 | for instance, meta in reversed(zip(self.instances, self.metas)):
|
---|
91 | object_data.update(forms_models.model_to_dict(instance, meta.fields, meta.exclude))
|
---|
92 |
|
---|
93 | initial = kwargs.pop('initial', None)
|
---|
94 | if initial is not None:
|
---|
95 | object_data.update(initial)
|
---|
96 |
|
---|
97 | super(ParentsIncludedModelFormMixin, self).__init__(initial=object_data, *args, **kwargs)
|
---|
98 |
|
---|
99 | def _iterate_over_instances(self, method_name, *args, **kwargs):
|
---|
100 | """
|
---|
101 | Somewhat hackish implementation of a wrapper for multiple instances.
|
---|
102 |
|
---|
103 | It temporary sets `self.instance` and `self._meta` and calls requested method. It collects possible results of this method calls
|
---|
104 | into the list and returns it. At the end it restores `self.instance` and `self._meta` to initial values.
|
---|
105 | """
|
---|
106 |
|
---|
107 | # Save original values
|
---|
108 | original_instance = self.instance
|
---|
109 | original_meta = self._meta
|
---|
110 |
|
---|
111 | results = []
|
---|
112 |
|
---|
113 | for instance, meta in zip(self.instances, self.metas):
|
---|
114 | # Temporary set values
|
---|
115 | self.instance = instance
|
---|
116 | self._meta = meta
|
---|
117 | results.append(getattr(super(ParentsIncludedModelFormMixin, self), method_name)(*args, **kwargs))
|
---|
118 |
|
---|
119 | # Restore original values
|
---|
120 | self.instance = original_instance
|
---|
121 | self._meta = original_meta
|
---|
122 |
|
---|
123 | return results
|
---|
124 |
|
---|
125 | def clean(self):
|
---|
126 | # We traverse in reverse order to keep in sync with get_declared_fields
|
---|
127 | return reduce(intersect, reversed(self._iterate_over_instances('clean')))
|
---|
128 |
|
---|
129 | def _post_clean(self):
|
---|
130 | self._iterate_over_instances('_post_clean')
|
---|
131 |
|
---|
132 | # We do not change validate_unique on purpose as it is called from _post_clean and we will probably do not use it otherwise
|
---|
133 |
|
---|
134 | def save(self, commit=True):
|
---|
135 | return self._iterate_over_instances('save', commit)
|
---|
136 |
|
---|
137 | save.alters_data = True
|
---|