diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index cae33cc20e..21e87f572b 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -222,7 +222,11 @@ def items_for_result(cl, result): url = cl.url_for_result(result) # Convert the pk to something that can be used in Javascript. # Problem cases are long ints (23L) and non-ASCII strings. - result_id = repr(force_unicode(getattr(result, pk)))[1:] + if cl.to_field: + attr = str(cl.to_field) + else: + attr = pk + result_id = repr(force_unicode(getattr(result, attr)))[1:] yield mark_safe(u'<%s%s>%s' % \ (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)) else: diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index 0a5ab372ec..a6a206d1dc 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -24,6 +24,7 @@ ORDER_VAR = 'o' ORDER_TYPE_VAR = 'ot' PAGE_VAR = 'p' SEARCH_VAR = 'q' +TO_FIELD_VAR = 't' IS_POPUP_VAR = 'pop' ERROR_FLAG = 'e' @@ -52,9 +53,12 @@ class ChangeList(object): self.page_num = 0 self.show_all = ALL_VAR in request.GET self.is_popup = IS_POPUP_VAR in request.GET + self.to_field = request.GET.get(TO_FIELD_VAR) self.params = dict(request.GET.items()) if PAGE_VAR in self.params: del self.params[PAGE_VAR] + if TO_FIELD_VAR in self.params: + del self.params[TO_FIELD_VAR] if ERROR_FLAG in self.params: del self.params[ERROR_FLAG] diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py index 15720a792b..50e55dc4d5 100644 --- a/django/contrib/admin/widgets.py +++ b/django/contrib/admin/widgets.py @@ -41,20 +41,20 @@ class FilteredSelectMultiple(forms.SelectMultiple): class AdminDateWidget(forms.TextInput): class Media: - js = (settings.ADMIN_MEDIA_PREFIX + "js/calendar.js", + js = (settings.ADMIN_MEDIA_PREFIX + "js/calendar.js", settings.ADMIN_MEDIA_PREFIX + "js/admin/DateTimeShortcuts.js") - + def __init__(self, attrs={}): super(AdminDateWidget, self).__init__(attrs={'class': 'vDateField', 'size': '10'}) class AdminTimeWidget(forms.TextInput): class Media: - js = (settings.ADMIN_MEDIA_PREFIX + "js/calendar.js", + js = (settings.ADMIN_MEDIA_PREFIX + "js/calendar.js", settings.ADMIN_MEDIA_PREFIX + "js/admin/DateTimeShortcuts.js") def __init__(self, attrs={}): super(AdminTimeWidget, self).__init__(attrs={'class': 'vTimeField', 'size': '8'}) - + class AdminSplitDateTime(forms.SplitDateTimeWidget): """ A SplitDateTime Widget that has some admin-specific styling. @@ -86,7 +86,7 @@ class AdminFileWidget(forms.FileInput): """ def __init__(self, attrs={}): super(AdminFileWidget, self).__init__(attrs) - + def render(self, name, value, attrs=None): output = [] if value and hasattr(value, "url"): @@ -105,11 +105,13 @@ class ForeignKeyRawIdWidget(forms.TextInput): super(ForeignKeyRawIdWidget, self).__init__(attrs) def render(self, name, value, attrs=None): + from django.contrib.admin.views.main import TO_FIELD_VAR related_url = '../../../%s/%s/' % (self.rel.to._meta.app_label, self.rel.to._meta.object_name.lower()) + params = {} if self.rel.limit_choices_to: - url = '?' + '&'.join(['%s=%s' % (k, ','.join(v)) for k, v in self.rel.limit_choices_to.items()]) - else: - url = '' + params.update(dict([(k, ','.join(v)) for k, v in self.rel.limit_choices_to.items()])) + params.update({TO_FIELD_VAR: self.rel.get_related_field().name}) + url = '?' + '&'.join(['%s=%s' % (k, v) for k, v in params.items()]) if not attrs.has_key('class'): attrs['class'] = 'vForeignKeyRawIdAdminField' # The JavaScript looks for this hook. output = [super(ForeignKeyRawIdWidget, self).render(name, value, attrs)] @@ -121,11 +123,12 @@ class ForeignKeyRawIdWidget(forms.TextInput): if value: output.append(self.label_for_value(value)) return mark_safe(u''.join(output)) - + def label_for_value(self, value): - return ' %s' % \ - truncate_words(self.rel.to.objects.get(pk=value), 14) - + key = self.rel.get_related_field().name + obj = self.rel.to.objects.get(**{key: value}) + return ' %s' % truncate_words(obj, 14) + class ManyToManyRawIdWidget(ForeignKeyRawIdWidget): """ A Widget for displaying ManyToMany ids in the "raw_id" interface rather than @@ -133,7 +136,7 @@ class ManyToManyRawIdWidget(ForeignKeyRawIdWidget): """ def __init__(self, rel, attrs=None): super(ManyToManyRawIdWidget, self).__init__(rel, attrs) - + def render(self, name, value, attrs=None): attrs['class'] = 'vManyToManyRawIdAdminField' if value: @@ -141,7 +144,7 @@ class ManyToManyRawIdWidget(ForeignKeyRawIdWidget): else: value = '' return super(ManyToManyRawIdWidget, self).render(name, value, attrs) - + def label_for_value(self, value): return '' @@ -152,7 +155,7 @@ class ManyToManyRawIdWidget(ForeignKeyRawIdWidget): if value: return [value] return None - + def _has_changed(self, initial, data): if initial is None: initial = [] diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index ef19477c30..679f63c995 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -691,7 +691,12 @@ class ForeignKey(RelatedField, Field): setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related)) def formfield(self, **kwargs): - defaults = {'form_class': forms.ModelChoiceField, 'queryset': self.rel.to._default_manager.complex_filter(self.rel.limit_choices_to)} + defaults = { + 'form_class': forms.ModelChoiceField, + 'queryset': self.rel.to._default_manager.complex_filter( + self.rel.limit_choices_to), + 'to_field_name': self.rel.field_name, + } defaults.update(kwargs) return super(ForeignKey, self).formfield(**defaults) diff --git a/django/forms/models.py b/django/forms/models.py index 8e59ebecda..56e7f2aabd 100644 --- a/django/forms/models.py +++ b/django/forms/models.py @@ -550,14 +550,21 @@ class ModelChoiceIterator(object): if self.field.cache_choices: if self.field.choice_cache is None: self.field.choice_cache = [ - (obj.pk, self.field.label_from_instance(obj)) - for obj in self.queryset.all() + self.choice(obj) for obj in self.queryset.all() ] for choice in self.field.choice_cache: yield choice else: for obj in self.queryset.all(): - yield (obj.pk, self.field.label_from_instance(obj)) + yield self.choice(obj) + + def choice(self, obj): + if self.field.to_field_name: + key = getattr(obj, self.field.to_field_name) + else: + key = obj.pk + return (key, self.field.label_from_instance(obj)) + class ModelChoiceField(ChoiceField): """A ChoiceField whose choices are a model QuerySet.""" @@ -570,7 +577,7 @@ class ModelChoiceField(ChoiceField): def __init__(self, queryset, empty_label=u"---------", cache_choices=False, required=True, widget=None, label=None, initial=None, - help_text=None, *args, **kwargs): + help_text=None, to_field_name=None, *args, **kwargs): self.empty_label = empty_label self.cache_choices = cache_choices @@ -580,6 +587,7 @@ class ModelChoiceField(ChoiceField): *args, **kwargs) self.queryset = queryset self.choice_cache = None + self.to_field_name = to_field_name def _get_queryset(self): return self._queryset @@ -622,7 +630,8 @@ class ModelChoiceField(ChoiceField): if value in EMPTY_VALUES: return None try: - value = self.queryset.get(pk=value) + key = self.to_field_name or 'pk' + value = self.queryset.get(**{key: value}) except self.queryset.model.DoesNotExist: raise ValidationError(self.error_messages['invalid_choice']) return value diff --git a/tests/modeltests/model_forms/models.py b/tests/modeltests/model_forms/models.py index a11972d431..b6c666307a 100644 --- a/tests/modeltests/model_forms/models.py +++ b/tests/modeltests/model_forms/models.py @@ -78,7 +78,7 @@ class BetterWriter(Writer): class WriterProfile(models.Model): writer = models.OneToOneField(Writer, primary_key=True) age = models.PositiveIntegerField() - + def __unicode__(self): return "%s is %s" % (self.writer, self.age) @@ -137,7 +137,14 @@ class Price(models.Model): class ArticleStatus(models.Model): status = models.CharField(max_length=2, choices=ARTICLE_STATUS_CHAR, blank=True, null=True) +class Inventory(models.Model): + barcode = models.PositiveIntegerField(unique=True) + parent = models.ForeignKey('self', to_field='barcode', blank=True, null=True) + name = models.CharField(blank=False, max_length=20) + def __unicode__(self): + return self.name + __test__ = {'API_TESTS': """ >>> from django import forms >>> from django.forms.models import ModelForm, model_to_dict @@ -1135,7 +1142,7 @@ u'1,2,3' Traceback (most recent call last): ... ValidationError: [u'Enter only digits separated by commas.'] ->>> f.clean(',,,,') +>>> f.clean(',,,,') u',,,,' >>> f.clean('1.2') Traceback (most recent call last): @@ -1204,4 +1211,36 @@ Traceback (most recent call last): ... ValidationError: [u'Select a valid choice. z is not one of the available choices.'] +# Foreign keys which use to_field ############################################# + +>>> apple = Inventory.objects.create(barcode=86, name='Apple') +>>> pear = Inventory.objects.create(barcode=22, name='Pear') +>>> core = Inventory.objects.create(barcode=87, name='Core', parent=apple) + +>>> field = ModelChoiceField(Inventory.objects.all(), to_field_name='barcode') +>>> for choice in field.choices: +... print choice +(u'', u'---------') +(86, u'Apple') +(22, u'Pear') +(87, u'Core') + +>>> class InventoryForm(ModelForm): +... class Meta: +... model = Inventory +>>> form = InventoryForm(instance=core) +>>> print form['parent'] + + +>>> data = model_to_dict(core) +>>> data['parent'] = '22' +>>> form = InventoryForm(data=data, instance=core) +>>> core = form.save() +>>> core.parent + """} diff --git a/tests/regressiontests/admin_widgets/models.py b/tests/regressiontests/admin_widgets/models.py index e178a750e3..2bc907557a 100644 --- a/tests/regressiontests/admin_widgets/models.py +++ b/tests/regressiontests/admin_widgets/models.py @@ -5,14 +5,14 @@ from django.core.files.storage import default_storage class Member(models.Model): name = models.CharField(max_length=100) - + def __unicode__(self): return self.name class Band(models.Model): name = models.CharField(max_length=100) members = models.ManyToManyField(Member) - + def __unicode__(self): return self.name @@ -20,10 +20,18 @@ class Album(models.Model): band = models.ForeignKey(Band) name = models.CharField(max_length=100) cover_art = models.FileField(upload_to='albums') - + def __unicode__(self): return self.name +class Inventory(models.Model): + barcode = models.PositiveIntegerField(unique=True) + parent = models.ForeignKey('self', to_field='barcode', blank=True, null=True) + name = models.CharField(blank=False, max_length=20) + + def __unicode__(self): + return self.name + __test__ = {'WIDGETS_TESTS': """ >>> from datetime import datetime >>> from django.utils.html import escape, conditional_escape @@ -84,6 +92,15 @@ True >>> w._has_changed([1, 2], [u'1', u'3']) True +# Check that ForeignKeyRawIdWidget works with fields which aren't related to +# the model's primary key. +>>> apple = Inventory.objects.create(barcode=86, name='Apple') +>>> pear = Inventory.objects.create(barcode=22, name='Pear') +>>> core = Inventory.objects.create(barcode=87, name='Core', parent=apple) +>>> rel = Inventory._meta.get_field('parent').rel +>>> w = ForeignKeyRawIdWidget(rel) +>>> print w.render('test', core.parent_id, attrs={}) + Lookup Apple """ % { 'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX, 'STORAGE_URL': default_storage.url(''),