Code

Ticket #8648: 8648.4.diff

File 8648.4.diff, 13.4 KB (added by brosner, 6 years ago)
Line 
1diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py
2index cae33cc..99df955 100644
3--- a/django/contrib/admin/templatetags/admin_list.py
4+++ b/django/contrib/admin/templatetags/admin_list.py
5@@ -222,7 +222,7 @@ def items_for_result(cl, result):
6             url = cl.url_for_result(result)
7             # Convert the pk to something that can be used in Javascript.
8             # Problem cases are long ints (23L) and non-ASCII strings.
9-            result_id = repr(force_unicode(getattr(result, pk)))[1:]
10+            result_id = repr(force_unicode(getattr(result, cl.to_field)))[1:]
11             yield mark_safe(u'<%s%s><a href="%s"%s>%s</a></%s>' % \
12                 (table_tag, row_class, url, (cl.is_popup and ' onclick="opener.dismissRelatedLookupPopup(window, %s); return false;"' % result_id or ''), conditional_escape(result_repr), table_tag))
13         else:
14diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py
15index 0a5ab37..a6a206d 100644
16--- a/django/contrib/admin/views/main.py
17+++ b/django/contrib/admin/views/main.py
18@@ -24,6 +24,7 @@ ORDER_VAR = 'o'
19 ORDER_TYPE_VAR = 'ot'
20 PAGE_VAR = 'p'
21 SEARCH_VAR = 'q'
22+TO_FIELD_VAR = 't'
23 IS_POPUP_VAR = 'pop'
24 ERROR_FLAG = 'e'
25 
26@@ -52,9 +53,12 @@ class ChangeList(object):
27             self.page_num = 0
28         self.show_all = ALL_VAR in request.GET
29         self.is_popup = IS_POPUP_VAR in request.GET
30+        self.to_field = request.GET.get(TO_FIELD_VAR)
31         self.params = dict(request.GET.items())
32         if PAGE_VAR in self.params:
33             del self.params[PAGE_VAR]
34+        if TO_FIELD_VAR in self.params:
35+            del self.params[TO_FIELD_VAR]
36         if ERROR_FLAG in self.params:
37             del self.params[ERROR_FLAG]
38 
39diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
40index 15720a7..50e55dc 100644
41--- a/django/contrib/admin/widgets.py
42+++ b/django/contrib/admin/widgets.py
43@@ -41,20 +41,20 @@ class FilteredSelectMultiple(forms.SelectMultiple):
44 
45 class AdminDateWidget(forms.TextInput):
46     class Media:
47-        js = (settings.ADMIN_MEDIA_PREFIX + "js/calendar.js",
48+        js = (settings.ADMIN_MEDIA_PREFIX + "js/calendar.js",
49               settings.ADMIN_MEDIA_PREFIX + "js/admin/DateTimeShortcuts.js")
50-       
51+
52     def __init__(self, attrs={}):
53         super(AdminDateWidget, self).__init__(attrs={'class': 'vDateField', 'size': '10'})
54 
55 class AdminTimeWidget(forms.TextInput):
56     class Media:
57-        js = (settings.ADMIN_MEDIA_PREFIX + "js/calendar.js",
58+        js = (settings.ADMIN_MEDIA_PREFIX + "js/calendar.js",
59               settings.ADMIN_MEDIA_PREFIX + "js/admin/DateTimeShortcuts.js")
60 
61     def __init__(self, attrs={}):
62         super(AdminTimeWidget, self).__init__(attrs={'class': 'vTimeField', 'size': '8'})
63-   
64+
65 class AdminSplitDateTime(forms.SplitDateTimeWidget):
66     """
67     A SplitDateTime Widget that has some admin-specific styling.
68@@ -86,7 +86,7 @@ class AdminFileWidget(forms.FileInput):
69     """
70     def __init__(self, attrs={}):
71         super(AdminFileWidget, self).__init__(attrs)
72-       
73+
74     def render(self, name, value, attrs=None):
75         output = []
76         if value and hasattr(value, "url"):
77@@ -105,11 +105,13 @@ class ForeignKeyRawIdWidget(forms.TextInput):
78         super(ForeignKeyRawIdWidget, self).__init__(attrs)
79 
80     def render(self, name, value, attrs=None):
81+        from django.contrib.admin.views.main import TO_FIELD_VAR
82         related_url = '../../../%s/%s/' % (self.rel.to._meta.app_label, self.rel.to._meta.object_name.lower())
83+        params = {}
84         if self.rel.limit_choices_to:
85-            url = '?' + '&amp;'.join(['%s=%s' % (k, ','.join(v)) for k, v in self.rel.limit_choices_to.items()])
86-        else:
87-            url = ''
88+            params.update(dict([(k, ','.join(v)) for k, v in self.rel.limit_choices_to.items()]))
89+        params.update({TO_FIELD_VAR: self.rel.get_related_field().name})
90+        url = '?' + '&amp;'.join(['%s=%s' % (k, v) for k, v in params.items()])
91         if not attrs.has_key('class'):
92           attrs['class'] = 'vForeignKeyRawIdAdminField' # The JavaScript looks for this hook.
93         output = [super(ForeignKeyRawIdWidget, self).render(name, value, attrs)]
94@@ -121,11 +123,12 @@ class ForeignKeyRawIdWidget(forms.TextInput):
95         if value:
96             output.append(self.label_for_value(value))
97         return mark_safe(u''.join(output))
98-   
99+
100     def label_for_value(self, value):
101-        return '&nbsp;<strong>%s</strong>' % \
102-            truncate_words(self.rel.to.objects.get(pk=value), 14)
103-           
104+        key = self.rel.get_related_field().name
105+        obj = self.rel.to.objects.get(**{key: value})
106+        return '&nbsp;<strong>%s</strong>' % truncate_words(obj, 14)
107+
108 class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):
109     """
110     A Widget for displaying ManyToMany ids in the "raw_id" interface rather than
111@@ -133,7 +136,7 @@ class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):
112     """
113     def __init__(self, rel, attrs=None):
114         super(ManyToManyRawIdWidget, self).__init__(rel, attrs)
115-   
116+
117     def render(self, name, value, attrs=None):
118         attrs['class'] = 'vManyToManyRawIdAdminField'
119         if value:
120@@ -141,7 +144,7 @@ class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):
121         else:
122             value = ''
123         return super(ManyToManyRawIdWidget, self).render(name, value, attrs)
124-   
125+
126     def label_for_value(self, value):
127         return ''
128 
129@@ -152,7 +155,7 @@ class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):
130         if value:
131             return [value]
132         return None
133-   
134+
135     def _has_changed(self, initial, data):
136         if initial is None:
137             initial = []
138diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
139index ef19477..679f63c 100644
140--- a/django/db/models/fields/related.py
141+++ b/django/db/models/fields/related.py
142@@ -691,7 +691,12 @@ class ForeignKey(RelatedField, Field):
143         setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related))
144 
145     def formfield(self, **kwargs):
146-        defaults = {'form_class': forms.ModelChoiceField, 'queryset': self.rel.to._default_manager.complex_filter(self.rel.limit_choices_to)}
147+        defaults = {
148+            'form_class': forms.ModelChoiceField,
149+            'queryset': self.rel.to._default_manager.complex_filter(
150+                                                    self.rel.limit_choices_to),
151+            'to_field_name': self.rel.field_name,
152+        }
153         defaults.update(kwargs)
154         return super(ForeignKey, self).formfield(**defaults)
155 
156diff --git a/django/forms/models.py b/django/forms/models.py
157index c153512..3e5d9f8 100644
158--- a/django/forms/models.py
159+++ b/django/forms/models.py
160@@ -548,14 +548,21 @@ class ModelChoiceIterator(object):
161         if self.field.cache_choices:
162             if self.field.choice_cache is None:
163                 self.field.choice_cache = [
164-                    (obj.pk, self.field.label_from_instance(obj))
165-                    for obj in self.queryset.all()
166+                    self.choice(obj) for obj in self.queryset.all()
167                 ]
168             for choice in self.field.choice_cache:
169                 yield choice
170         else:
171             for obj in self.queryset.all():
172-                yield (obj.pk, self.field.label_from_instance(obj))
173+                yield self.choice(obj)
174+
175+    def choice(self, obj):
176+        if self.field.to_field_name:
177+            key = getattr(obj, self.field.to_field_name)
178+        else:
179+            key = obj.pk
180+        return (key, self.field.label_from_instance(obj))
181+
182 
183 class ModelChoiceField(ChoiceField):
184     """A ChoiceField whose choices are a model QuerySet."""
185@@ -568,7 +575,7 @@ class ModelChoiceField(ChoiceField):
186 
187     def __init__(self, queryset, empty_label=u"---------", cache_choices=False,
188                  required=True, widget=None, label=None, initial=None,
189-                 help_text=None, *args, **kwargs):
190+                 help_text=None, to_field_name=None, *args, **kwargs):
191         self.empty_label = empty_label
192         self.cache_choices = cache_choices
193 
194@@ -578,6 +585,7 @@ class ModelChoiceField(ChoiceField):
195                        *args, **kwargs)
196         self.queryset = queryset
197         self.choice_cache = None
198+        self.to_field_name = to_field_name
199 
200     def _get_queryset(self):
201         return self._queryset
202@@ -620,7 +628,8 @@ class ModelChoiceField(ChoiceField):
203         if value in EMPTY_VALUES:
204             return None
205         try:
206-            value = self.queryset.get(pk=value)
207+            key = self.to_field_name or 'pk'
208+            value = self.queryset.get(**{key: value})
209         except self.queryset.model.DoesNotExist:
210             raise ValidationError(self.error_messages['invalid_choice'])
211         return value
212diff --git a/tests/modeltests/model_forms/models.py b/tests/modeltests/model_forms/models.py
213index 252d46d..9c9ac82 100644
214--- a/tests/modeltests/model_forms/models.py
215+++ b/tests/modeltests/model_forms/models.py
216@@ -78,7 +78,7 @@ class BetterWriter(Writer):
217 class WriterProfile(models.Model):
218     writer = models.OneToOneField(Writer, primary_key=True)
219     age = models.PositiveIntegerField()
220-   
221+
222     def __unicode__(self):
223         return "%s is %s" % (self.writer, self.age)
224 
225@@ -136,7 +136,14 @@ class Price(models.Model):
226 class ArticleStatus(models.Model):
227     status = models.CharField(max_length=2, choices=ARTICLE_STATUS_CHAR, blank=True, null=True)
228 
229+class Inventory(models.Model):
230+   barcode = models.PositiveIntegerField(unique=True)
231+   parent = models.ForeignKey('self', to_field='barcode', blank=True, null=True)
232+   name = models.CharField(blank=False, max_length=20)
233 
234+   def __unicode__(self):
235+      return self.name
236+     
237 __test__ = {'API_TESTS': """
238 >>> from django import forms
239 >>> from django.forms.models import ModelForm, model_to_dict
240@@ -1134,7 +1141,7 @@ u'1,2,3'
241 Traceback (most recent call last):
242 ...
243 ValidationError: [u'Enter only digits separated by commas.']
244->>> f.clean(',,,,')
245+>>> f.clean(',,,,')
246 u',,,,'
247 >>> f.clean('1.2')
248 Traceback (most recent call last):
249@@ -1203,4 +1210,36 @@ Traceback (most recent call last):
250 ...
251 ValidationError: [u'Select a valid choice. z is not one of the available choices.']
252 
253+# Foreign keys which use to_field #############################################
254+
255+>>> apple = Inventory.objects.create(barcode=86, name='Apple')
256+>>> pear = Inventory.objects.create(barcode=22, name='Pear')
257+>>> core = Inventory.objects.create(barcode=87, name='Core', parent=apple)
258+
259+>>> field = ModelChoiceField(Inventory.objects.all(), to_field_name='barcode')
260+>>> for choice in field.choices:
261+...     print choice
262+(u'', u'---------')
263+(86, u'Apple')
264+(22, u'Pear')
265+(87, u'Core')
266+
267+>>> class InventoryForm(ModelForm):
268+...     class Meta:
269+...         model = Inventory
270+>>> form = InventoryForm(instance=core)
271+>>> print form['parent']
272+<select name="parent" id="id_parent">
273+<option value="">---------</option>
274+<option value="86" selected="selected">Apple</option>
275+<option value="22">Pear</option>
276+<option value="87">Core</option>
277+</select>
278+
279+>>> data = model_to_dict(core)
280+>>> data['parent'] = '22'
281+>>> form = InventoryForm(data=data, instance=core)
282+>>> core = form.save()
283+>>> core.parent
284+<Inventory: Pear>
285 """}
286diff --git a/tests/regressiontests/admin_widgets/models.py b/tests/regressiontests/admin_widgets/models.py
287index e178a75..2bc9075 100644
288--- a/tests/regressiontests/admin_widgets/models.py
289+++ b/tests/regressiontests/admin_widgets/models.py
290@@ -5,14 +5,14 @@ from django.core.files.storage import default_storage
291 
292 class Member(models.Model):
293     name = models.CharField(max_length=100)
294-   
295+
296     def __unicode__(self):
297         return self.name
298 
299 class Band(models.Model):
300     name = models.CharField(max_length=100)
301     members = models.ManyToManyField(Member)
302-   
303+
304     def __unicode__(self):
305         return self.name
306 
307@@ -20,10 +20,18 @@ class Album(models.Model):
308     band = models.ForeignKey(Band)
309     name = models.CharField(max_length=100)
310     cover_art = models.FileField(upload_to='albums')
311-   
312+
313     def __unicode__(self):
314         return self.name
315 
316+class Inventory(models.Model):
317+   barcode = models.PositiveIntegerField(unique=True)
318+   parent = models.ForeignKey('self', to_field='barcode', blank=True, null=True)
319+   name = models.CharField(blank=False, max_length=20)
320+
321+   def __unicode__(self):
322+      return self.name
323+
324 __test__ = {'WIDGETS_TESTS': """
325 >>> from datetime import datetime
326 >>> from django.utils.html import escape, conditional_escape
327@@ -84,6 +92,15 @@ True
328 >>> w._has_changed([1, 2], [u'1', u'3'])
329 True
330 
331+# Check that ForeignKeyRawIdWidget works with fields which aren't related to
332+# the model's primary key.
333+>>> apple = Inventory.objects.create(barcode=86, name='Apple')
334+>>> pear = Inventory.objects.create(barcode=22, name='Pear')
335+>>> core = Inventory.objects.create(barcode=87, name='Core', parent=apple)
336+>>> rel = Inventory._meta.get_field('parent').rel
337+>>> w = ForeignKeyRawIdWidget(rel)
338+>>> print w.render('test', core.parent_id, attrs={})
339+<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>
340 """ % {
341     'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX,
342     'STORAGE_URL': default_storage.url(''),