Code

Ticket #8719: primary_key_formset_fixes.diff

File primary_key_formset_fixes.diff, 10.5 KB (added by brosner, 6 years ago)

rough patch.

Line 
1diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py
2index 7a60943..56b4f56 100644
3--- a/django/contrib/admin/helpers.py
4+++ b/django/contrib/admin/helpers.py
5@@ -126,7 +126,7 @@ class InlineAdminForm(AdminForm):
6         super(InlineAdminForm, self).__init__(form, fieldsets, prepopulated_fields)
7 
8     def pk_field(self):
9-        return AdminField(self.form, self.formset._pk_field_name, False)
10+        return AdminField(self.form, self.formset._pk_field.name, False)
11 
12     def deletion_field(self):
13         from django.forms.formsets import DELETION_FIELD_NAME
14diff --git a/django/forms/models.py b/django/forms/models.py
15index bd9597d..3e3d56c 100644
16--- a/django/forms/models.py
17+++ b/django/forms/models.py
18@@ -21,7 +21,7 @@ __all__ = (
19 )
20 
21 def save_instance(form, instance, fields=None, fail_message='saved',
22-                  commit=True):
23+                  commit=True, exclude=None):
24     """
25     Saves bound Form ``form``'s cleaned_data into model instance ``instance``.
26 
27@@ -36,10 +36,13 @@ def save_instance(form, instance, fields=None, fail_message='saved',
28     cleaned_data = form.cleaned_data
29     for f in opts.fields:
30         if not f.editable or isinstance(f, models.AutoField) \
31+                or (isinstance(f, models.OneToOneField) and f.rel.parent_link) \
32                 or not f.name in cleaned_data:
33             continue
34         if fields and f.name not in fields:
35             continue
36+        if exclude and f.name in exclude:
37+            continue
38         f.save_form_data(instance, cleaned_data[f.name])
39     # Wrap up the saving of m2m data as a function.
40     def save_m2m():
41@@ -115,8 +118,6 @@ def model_to_dict(instance, fields=None, exclude=None):
42             else:
43                 # MultipleChoiceWidget needs a list of pks, not object instances.
44                 data[f.name] = [obj.pk for obj in f.value_from_object(instance)]
45-        elif isinstance(f, OneToOneField):
46-            data[f.attname] = f.value_from_object(instance)
47         else:
48             data[f.name] = f.value_from_object(instance)
49     return data
50@@ -291,7 +292,11 @@ class BaseModelFormSet(BaseFormSet):
51             existing_objects[obj.pk] = obj
52         saved_instances = []
53         for form in self.initial_forms:
54-            obj = existing_objects[form.cleaned_data[self.model._meta.pk.attname]]
55+            cleaned_pk = form.cleaned_data[self._pk_field.name]
56+            if hasattr(cleaned_pk, "pk"):
57+                obj = existing_objects[cleaned_pk.pk]
58+            else:
59+                obj = existing_objects[cleaned_pk]
60             if self.can_delete and form.cleaned_data[DELETION_FIELD_NAME]:
61                 self.deleted_objects.append(obj)
62                 obj.delete()
63@@ -319,9 +324,13 @@ class BaseModelFormSet(BaseFormSet):
64 
65     def add_fields(self, form, index):
66         """Add a hidden field for the object's primary key."""
67-        if self.model._meta.pk.auto_created:
68-            self._pk_field_name = self.model._meta.pk.attname
69-            form.fields[self._pk_field_name] = IntegerField(required=False, widget=HiddenInput)
70+        from django.db.models import OneToOneField
71+        pk = self.model._meta.pk
72+        while isinstance(pk, OneToOneField):
73+            pk = pk.rel.to._meta.pk # drill down until we find a "real" pk
74+        if pk.auto_created:
75+            self._pk_field = self.model._meta.pk
76+            form.fields[self._pk_field.name] = IntegerField(required=False, widget=HiddenInput)
77         super(BaseModelFormSet, self).add_fields(form, index)
78 
79 def modelformset_factory(model, form=ModelForm, formfield_callback=lambda f: f.formfield(),
80@@ -369,7 +378,18 @@ class BaseInlineFormSet(BaseModelFormSet):
81     def save_new(self, form, commit=True):
82         kwargs = {self.fk.get_attname(): self.instance.pk}
83         new_obj = self.model(**kwargs)
84-        return save_instance(form, new_obj, commit=commit)
85+        exclude = []
86+        if self.fk == self._pk_field:
87+            exclude.append(self.fk.name)
88+        return save_instance(form, new_obj, exclude=exclude, commit=commit)
89+   
90+    def add_fields(self, form, index):
91+        from django.db.models import OneToOneField
92+        super(BaseInlineFormSet, self).add_fields(form, index)
93+        if isinstance(self._pk_field, OneToOneField):
94+            queryset = self.instance.__class__._default_manager.all()
95+            form.fields[self._pk_field.name] = ModelChoiceField(queryset,
96+                required=False, widget=HiddenInput)
97 
98 def _get_foreign_key(parent_model, model, fk_name=None):
99     """
100@@ -419,13 +439,21 @@ def inlineformset_factory(parent_model, model, form=ModelForm,
101     """
102     fk = _get_foreign_key(parent_model, model, fk_name=fk_name)
103     # let the formset handle object deletion by default
104-
105-    if exclude is not None:
106-        exclude.append(fk.name)
107-    else:
108-        exclude = [fk.name]
109+   
110+    def _formfield_callback(f, **kwargs):
111+        if fk == model._meta.pk:
112+            if f.primary_key:
113+                return None
114+        return formfield_callback(f, **kwargs)
115+   
116+    if fk != model._meta.pk:
117+        if exclude is not None:
118+            exclude.append(fk.name)
119+        else:
120+            exclude = [fk.name]
121+   
122     FormSet = modelformset_factory(model, form=form,
123-                                    formfield_callback=formfield_callback,
124+                                    formfield_callback=_formfield_callback,
125                                     formset=formset,
126                                     extra=extra, can_delete=can_delete, can_order=can_order,
127                                     fields=fields, exclude=exclude, max_num=max_num)
128diff --git a/tests/modeltests/model_forms/models.py b/tests/modeltests/model_forms/models.py
129index b8d3f93..bff869f 100644
130--- a/tests/modeltests/model_forms/models.py
131+++ b/tests/modeltests/model_forms/models.py
132@@ -66,6 +66,12 @@ class ImprovedArticle(models.Model):
133 class ImprovedArticleWithParentLink(models.Model):
134     article = models.OneToOneField(Article, parent_link=True)
135 
136+class ImprovedArticleWithOneToOnePK(models.Model):
137+    article = models.OneToOneField(Article, primary_key=True)
138+   
139+    def __unicode__(self):
140+        return self.article.headline
141+
142 class BetterWriter(Writer):
143     pass
144 
145@@ -811,7 +817,35 @@ ValidationError: [u'Select a valid choice. 4 is not one of the available choices
146 >>> bw = BetterWriter(name=u'Joe Better')
147 >>> bw.save()
148 >>> sorted(model_to_dict(bw).keys())
149-['id', 'name', 'writer_ptr_id']
150+['id', 'name', 'writer_ptr']
151+
152+>>> class ImprovedArticleWithOneToOnePKForm(ModelForm):
153+...     class Meta:
154+...         model = ImprovedArticleWithOneToOnePK
155+
156+>>> print ImprovedArticleWithOneToOnePKForm().as_p()
157+<p><label for="id_article">Article:</label> <select name="article" id="id_article">
158+<option value="" selected="selected">---------</option>
159+<option value="1">New headline</option>
160+<option value="2">The walrus was Paul</option>
161+<option value="3">The walrus was Paul</option>
162+<option value="4">The walrus was Paul</option>
163+</select></p>
164+
165+>>> form = ImprovedArticleWithOneToOnePKForm(data={'article': u'1'})
166+>>> instance = form.save()
167+>>> instance
168+<ImprovedArticleWithOneToOnePK: New headline>
169+
170+>>> form = ImprovedArticleWithOneToOnePKForm(instance=instance)
171+>>> print form.as_p()
172+<p><label for="id_article">Article:</label> <select name="article" id="id_article">
173+<option value="">---------</option>
174+<option value="1" selected="selected">New headline</option>
175+<option value="2">The walrus was Paul</option>
176+<option value="3">The walrus was Paul</option>
177+<option value="4">The walrus was Paul</option>
178+</select></p>
179 
180 # PhoneNumberField ############################################################
181 
182diff --git a/tests/modeltests/model_formsets/models.py b/tests/modeltests/model_formsets/models.py
183index 65987c1..746cfbb 100644
184--- a/tests/modeltests/model_formsets/models.py
185+++ b/tests/modeltests/model_formsets/models.py
186@@ -262,12 +262,12 @@ used.
187 >>> for form in formset.forms:
188 ...     print form.as_p()
189 <p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" maxlength="100" /></p>
190-<p><label for="id_form-0-write_speed">Write speed:</label> <input type="text" name="form-0-write_speed" id="id_form-0-write_speed" /><input type="hidden" name="form-0-author_ptr_id" id="id_form-0-author_ptr_id" /></p>
191+<p><label for="id_form-0-write_speed">Write speed:</label> <input type="text" name="form-0-write_speed" id="id_form-0-write_speed" /><input type="hidden" name="form-0-author_ptr" id="id_form-0-author_ptr" /></p>
192 
193 >>> data = {
194 ...     'form-TOTAL_FORMS': '1', # the number of forms rendered
195 ...     'form-INITIAL_FORMS': '0', # the number of forms with initial data
196-...     'form-0-author_ptr_id': '',
197+...     'form-0-author_ptr': '',
198 ...     'form-0-name': 'Ernest Hemingway',
199 ...     'form-0-write_speed': '10',
200 ... }
201@@ -283,17 +283,17 @@ True
202 >>> for form in formset.forms:
203 ...     print form.as_p()
204 <p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" value="Ernest Hemingway" maxlength="100" /></p>
205-<p><label for="id_form-0-write_speed">Write speed:</label> <input type="text" name="form-0-write_speed" value="10" id="id_form-0-write_speed" /><input type="hidden" name="form-0-author_ptr_id" value="..." id="id_form-0-author_ptr_id" /></p>
206+<p><label for="id_form-0-write_speed">Write speed:</label> <input type="text" name="form-0-write_speed" value="10" id="id_form-0-write_speed" /><input type="hidden" name="form-0-author_ptr" value="..." id="id_form-0-author_ptr" /></p>
207 <p><label for="id_form-1-name">Name:</label> <input id="id_form-1-name" type="text" name="form-1-name" maxlength="100" /></p>
208-<p><label for="id_form-1-write_speed">Write speed:</label> <input type="text" name="form-1-write_speed" id="id_form-1-write_speed" /><input type="hidden" name="form-1-author_ptr_id" id="id_form-1-author_ptr_id" /></p>
209+<p><label for="id_form-1-write_speed">Write speed:</label> <input type="text" name="form-1-write_speed" id="id_form-1-write_speed" /><input type="hidden" name="form-1-author_ptr" id="id_form-1-author_ptr" /></p>
210 
211 >>> data = {
212 ...     'form-TOTAL_FORMS': '2', # the number of forms rendered
213 ...     'form-INITIAL_FORMS': '1', # the number of forms with initial data
214-...     'form-0-author_ptr_id': hemingway_id,
215+...     'form-0-author_ptr': hemingway_id,
216 ...     'form-0-name': 'Ernest Hemingway',
217 ...     'form-0-write_speed': '10',
218-...     'form-1-author_ptr_id': '',
219+...     'form-1-author_ptr': '',
220 ...     'form-1-name': '',
221 ...     'form-1-write_speed': '',
222 ... }