Code

Ticket #8648: 8648.3.diff

File 8648.3.diff, 10.2 KB (added by SmileyChris, 6 years ago)
Line 
1Index: django/db/models/fields/related.py
2===================================================================
3--- django/db/models/fields/related.py  (revision 8779)
4+++ django/db/models/fields/related.py  (working copy)
5@@ -686,7 +686,12 @@
6         setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related))
7         
8     def formfield(self, **kwargs):
9-        defaults = {'form_class': forms.ModelChoiceField, 'queryset': self.rel.to._default_manager.complex_filter(self.rel.limit_choices_to)}
10+        defaults = {
11+            'form_class': forms.ModelChoiceField,
12+            'queryset': self.rel.to._default_manager.complex_filter(
13+                                                    self.rel.limit_choices_to),
14+            'to_field_name': self.rel.field_name,
15+        }
16         defaults.update(kwargs)
17         return super(ForeignKey, self).formfield(**defaults)
18 
19Index: django/forms/models.py
20===================================================================
21--- django/forms/models.py      (revision 8779)
22+++ django/forms/models.py      (working copy)
23@@ -460,15 +460,22 @@
24         if self.field.cache_choices:
25             if self.field.choice_cache is None:
26                 self.field.choice_cache = [
27-                    (obj.pk, self.field.label_from_instance(obj))
28-                    for obj in self.queryset.all()
29+                    self.choice(obj) for obj in self.queryset.all()
30                 ]
31             for choice in self.field.choice_cache:
32                 yield choice
33         else:
34             for obj in self.queryset.all():
35-                yield (obj.pk, self.field.label_from_instance(obj))
36+                yield self.choice(obj)
37 
38+    def choice(self, obj):
39+        if self.field.to_field_name:
40+            key = getattr(obj, self.field.to_field_name)
41+        else:
42+            key = obj.pk
43+        return (key, self.field.label_from_instance(obj))
44+
45+
46 class ModelChoiceField(ChoiceField):
47     """A ChoiceField whose choices are a model QuerySet."""
48     # This class is a subclass of ChoiceField for purity, but it doesn't
49@@ -480,7 +487,7 @@
50 
51     def __init__(self, queryset, empty_label=u"---------", cache_choices=False,
52                  required=True, widget=None, label=None, initial=None,
53-                 help_text=None, *args, **kwargs):
54+                 help_text=None, to_field_name=None, *args, **kwargs):
55         self.empty_label = empty_label
56         self.cache_choices = cache_choices
57 
58@@ -490,6 +497,7 @@
59                        *args, **kwargs)
60         self.queryset = queryset
61         self.choice_cache = None
62+        self.to_field_name = to_field_name
63 
64     def _get_queryset(self):
65         return self._queryset
66@@ -532,7 +540,8 @@
67         if value in EMPTY_VALUES:
68             return None
69         try:
70-            value = self.queryset.get(pk=value)
71+            key = self.to_field_name or 'pk'
72+            value = self.queryset.get(**{key: value})
73         except self.queryset.model.DoesNotExist:
74             raise ValidationError(self.error_messages['invalid_choice'])
75         return value
76Index: tests/modeltests/model_forms/models.py
77===================================================================
78--- tests/modeltests/model_forms/models.py      (revision 8779)
79+++ tests/modeltests/model_forms/models.py      (working copy)
80@@ -78,7 +78,7 @@
81 class WriterProfile(models.Model):
82     writer = models.OneToOneField(Writer, primary_key=True)
83     age = models.PositiveIntegerField()
84-   
85+
86     def __unicode__(self):
87         return "%s is %s" % (self.writer, self.age)
88 
89@@ -120,6 +120,14 @@
90 class ArticleStatus(models.Model):
91     status = models.CharField(max_length=2, choices=ARTICLE_STATUS_CHAR, blank=True, null=True)
92 
93+class Inventory(models.Model):
94+   barcode = models.PositiveIntegerField(unique=True)
95+   parent = models.ForeignKey('self', to_field='barcode', blank=True, null=True)
96+   name = models.CharField(blank=False, max_length=20)
97+
98+   def __unicode__(self):
99+      return self.name
100+
101 __test__ = {'API_TESTS': """
102 >>> from django import forms
103 >>> from django.forms.models import ModelForm, model_to_dict
104@@ -1117,7 +1125,7 @@
105 Traceback (most recent call last):
106 ...
107 ValidationError: [u'Enter only digits separated by commas.']
108->>> f.clean(',,,,')
109+>>> f.clean(',,,,')
110 u',,,,'
111 >>> f.clean('1.2')
112 Traceback (most recent call last):
113@@ -1152,4 +1160,36 @@
114 ...
115 ValidationError: [u'Select a valid choice. z is not one of the available choices.']
116 
117+# Foreign keys which use to_field #############################################
118+
119+>>> apple = Inventory.objects.create(barcode=86, name='Apple')
120+>>> pear = Inventory.objects.create(barcode=22, name='Pear')
121+>>> core = Inventory.objects.create(barcode=87, name='Core', parent=apple)
122+
123+>>> field = ModelChoiceField(Inventory.objects.all(), to_field_name='barcode')
124+>>> for choice in field.choices:
125+...     print choice
126+(u'', u'---------')
127+(86, u'Apple')
128+(22, u'Pear')
129+(87, u'Core')
130+
131+>>> class InventoryForm(ModelForm):
132+...     class Meta:
133+...         model = Inventory
134+>>> form = InventoryForm(instance=core)
135+>>> print form['parent']
136+<select name="parent" id="id_parent">
137+<option value="">---------</option>
138+<option value="86" selected="selected">Apple</option>
139+<option value="22">Pear</option>
140+<option value="87">Core</option>
141+</select>
142+
143+>>> data = model_to_dict(core)
144+>>> data['parent'] = '22'
145+>>> form = InventoryForm(data=data, instance=core)
146+>>> core = form.save()
147+>>> core.parent
148+<Inventory: Pear>
149 """}
150Index: tests/regressiontests/admin_widgets/models.py
151===================================================================
152--- tests/regressiontests/admin_widgets/models.py       (revision 8779)
153+++ tests/regressiontests/admin_widgets/models.py       (working copy)
154@@ -5,14 +5,14 @@
155 
156 class Member(models.Model):
157     name = models.CharField(max_length=100)
158-   
159+
160     def __unicode__(self):
161         return self.name
162 
163 class Band(models.Model):
164     name = models.CharField(max_length=100)
165     members = models.ManyToManyField(Member)
166-   
167+
168     def __unicode__(self):
169         return self.name
170 
171@@ -20,10 +20,18 @@
172     band = models.ForeignKey(Band)
173     name = models.CharField(max_length=100)
174     cover_art = models.FileField(upload_to='albums')
175-   
176+
177     def __unicode__(self):
178         return self.name
179 
180+class Inventory(models.Model):
181+   barcode = models.PositiveIntegerField(unique=True)
182+   parent = models.ForeignKey('self', to_field='barcode', blank=True, null=True)
183+   name = models.CharField(blank=False, max_length=20)
184+
185+   def __unicode__(self):
186+      return self.name
187+
188 __test__ = {'WIDGETS_TESTS': """
189 >>> from datetime import datetime
190 >>> from django.utils.html import escape, conditional_escape
191@@ -84,6 +92,15 @@
192 >>> w._has_changed([1, 2], [u'1', u'3'])
193 True
194 
195+# Check that ForeignKeyRawIdWidget works with fields which aren't related to
196+# the model's primary key.
197+>>> apple = Inventory.objects.create(barcode=86, name='Apple')
198+>>> pear = Inventory.objects.create(barcode=22, name='Pear')
199+>>> core = Inventory.objects.create(barcode=87, name='Core', parent=apple)
200+>>> rel = Inventory._meta.get_field('parent').rel
201+>>> w = ForeignKeyRawIdWidget(rel)
202+>>> print w.render('test', core.parent_id, attrs={})
203+<input type="text" name="test" value="86" class="vForeignKeyRawIdAdminField" /><a href="../../../admin_widgets/inventory/" class="related-lookup" id="lookup_id_test" onclick="return showRelatedObjectLookupPopup(this);"> <img src="/admin_media/img/admin/selector-search.gif" width="16" height="16" alt="Lookup" /></a>&nbsp;<strong>Apple</strong>
204 """ % {
205     'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX,
206     'STORAGE_URL': default_storage.url(''),
207Index: django/contrib/admin/widgets.py
208===================================================================
209--- django/contrib/admin/widgets.py     (revision 8779)
210+++ django/contrib/admin/widgets.py     (working copy)
211@@ -41,20 +41,20 @@
212 
213 class AdminDateWidget(forms.TextInput):
214     class Media:
215-        js = (settings.ADMIN_MEDIA_PREFIX + "js/calendar.js",
216+        js = (settings.ADMIN_MEDIA_PREFIX + "js/calendar.js",
217               settings.ADMIN_MEDIA_PREFIX + "js/admin/DateTimeShortcuts.js")
218-       
219+
220     def __init__(self, attrs={}):
221         super(AdminDateWidget, self).__init__(attrs={'class': 'vDateField', 'size': '10'})
222 
223 class AdminTimeWidget(forms.TextInput):
224     class Media:
225-        js = (settings.ADMIN_MEDIA_PREFIX + "js/calendar.js",
226+        js = (settings.ADMIN_MEDIA_PREFIX + "js/calendar.js",
227               settings.ADMIN_MEDIA_PREFIX + "js/admin/DateTimeShortcuts.js")
228 
229     def __init__(self, attrs={}):
230         super(AdminTimeWidget, self).__init__(attrs={'class': 'vTimeField', 'size': '8'})
231-   
232+
233 class AdminSplitDateTime(forms.SplitDateTimeWidget):
234     """
235     A SplitDateTime Widget that has some admin-specific styling.
236@@ -86,7 +86,7 @@
237     """
238     def __init__(self, attrs={}):
239         super(AdminFileWidget, self).__init__(attrs)
240-       
241+
242     def render(self, name, value, attrs=None):
243         output = []
244         if value and hasattr(value, "url"):
245@@ -121,11 +121,12 @@
246         if value:
247             output.append(self.label_for_value(value))
248         return mark_safe(u''.join(output))
249-   
250+
251     def label_for_value(self, value):
252-        return '&nbsp;<strong>%s</strong>' % \
253-            truncate_words(self.rel.to.objects.get(pk=value), 14)
254-           
255+        key = self.rel.get_related_field().name
256+        obj = self.rel.to.objects.get(**{key: value})
257+        return '&nbsp;<strong>%s</strong>' % truncate_words(obj, 14)
258+
259 class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):
260     """
261     A Widget for displaying ManyToMany ids in the "raw_id" interface rather than
262@@ -133,7 +134,7 @@
263     """
264     def __init__(self, rel, attrs=None):
265         super(ManyToManyRawIdWidget, self).__init__(rel, attrs)
266-   
267+
268     def render(self, name, value, attrs=None):
269         attrs['class'] = 'vManyToManyRawIdAdminField'
270         if value:
271@@ -141,7 +142,7 @@
272         else:
273             value = ''
274         return super(ManyToManyRawIdWidget, self).render(name, value, attrs)
275-   
276+
277     def label_for_value(self, value):
278         return ''
279 
280@@ -152,7 +153,7 @@
281         if value:
282             return [value]
283         return None
284-   
285+
286     def _has_changed(self, initial, data):
287         if initial is None:
288             initial = []