Skip to content

Commit

Permalink
Fixed #8648 -- Admin no longer ignores to_field. Thanks for the help …
Browse files Browse the repository at this point in the history
…Karen Tracey and SmileyChris.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@8823 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information
brosner committed Sep 1, 2008
1 parent dcb0e8f commit ce47d4a
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 27 deletions.
6 changes: 5 additions & 1 deletion django/contrib/admin/templatetags/admin_list.py
Expand Up @@ -222,7 +222,11 @@ def items_for_result(cl, result):
url = cl.url_for_result(result) url = cl.url_for_result(result)
# Convert the pk to something that can be used in Javascript. # Convert the pk to something that can be used in Javascript.
# Problem cases are long ints (23L) and non-ASCII strings. # 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><a href="%s"%s>%s</a></%s>' % \ yield mark_safe(u'<%s%s><a href="%s"%s>%s</a></%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)) (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: else:
Expand Down
4 changes: 4 additions & 0 deletions django/contrib/admin/views/main.py
Expand Up @@ -24,6 +24,7 @@
ORDER_TYPE_VAR = 'ot' ORDER_TYPE_VAR = 'ot'
PAGE_VAR = 'p' PAGE_VAR = 'p'
SEARCH_VAR = 'q' SEARCH_VAR = 'q'
TO_FIELD_VAR = 't'
IS_POPUP_VAR = 'pop' IS_POPUP_VAR = 'pop'
ERROR_FLAG = 'e' ERROR_FLAG = 'e'


Expand Down Expand Up @@ -52,9 +53,12 @@ def __init__(self, request, model, list_display, list_display_links, list_filter
self.page_num = 0 self.page_num = 0
self.show_all = ALL_VAR in request.GET self.show_all = ALL_VAR in request.GET
self.is_popup = IS_POPUP_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()) self.params = dict(request.GET.items())
if PAGE_VAR in self.params: if PAGE_VAR in self.params:
del self.params[PAGE_VAR] del self.params[PAGE_VAR]
if TO_FIELD_VAR in self.params:
del self.params[TO_FIELD_VAR]
if ERROR_FLAG in self.params: if ERROR_FLAG in self.params:
del self.params[ERROR_FLAG] del self.params[ERROR_FLAG]


Expand Down
33 changes: 18 additions & 15 deletions django/contrib/admin/widgets.py
Expand Up @@ -41,20 +41,20 @@ def render(self, name, value, attrs=None, choices=()):


class AdminDateWidget(forms.TextInput): class AdminDateWidget(forms.TextInput):
class Media: 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") settings.ADMIN_MEDIA_PREFIX + "js/admin/DateTimeShortcuts.js")

def __init__(self, attrs={}): def __init__(self, attrs={}):
super(AdminDateWidget, self).__init__(attrs={'class': 'vDateField', 'size': '10'}) super(AdminDateWidget, self).__init__(attrs={'class': 'vDateField', 'size': '10'})


class AdminTimeWidget(forms.TextInput): class AdminTimeWidget(forms.TextInput):
class Media: 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") settings.ADMIN_MEDIA_PREFIX + "js/admin/DateTimeShortcuts.js")


def __init__(self, attrs={}): def __init__(self, attrs={}):
super(AdminTimeWidget, self).__init__(attrs={'class': 'vTimeField', 'size': '8'}) super(AdminTimeWidget, self).__init__(attrs={'class': 'vTimeField', 'size': '8'})

class AdminSplitDateTime(forms.SplitDateTimeWidget): class AdminSplitDateTime(forms.SplitDateTimeWidget):
""" """
A SplitDateTime Widget that has some admin-specific styling. A SplitDateTime Widget that has some admin-specific styling.
Expand Down Expand Up @@ -86,7 +86,7 @@ class AdminFileWidget(forms.FileInput):
""" """
def __init__(self, attrs={}): def __init__(self, attrs={}):
super(AdminFileWidget, self).__init__(attrs) super(AdminFileWidget, self).__init__(attrs)

def render(self, name, value, attrs=None): def render(self, name, value, attrs=None):
output = [] output = []
if value and hasattr(value, "url"): if value and hasattr(value, "url"):
Expand All @@ -105,11 +105,13 @@ def __init__(self, rel, attrs=None):
super(ForeignKeyRawIdWidget, self).__init__(attrs) super(ForeignKeyRawIdWidget, self).__init__(attrs)


def render(self, name, value, attrs=None): 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()) related_url = '../../../%s/%s/' % (self.rel.to._meta.app_label, self.rel.to._meta.object_name.lower())
params = {}
if self.rel.limit_choices_to: if self.rel.limit_choices_to:
url = '?' + '&amp;'.join(['%s=%s' % (k, ','.join(v)) for k, v in self.rel.limit_choices_to.items()]) params.update(dict([(k, ','.join(v)) for k, v in self.rel.limit_choices_to.items()]))
else: params.update({TO_FIELD_VAR: self.rel.get_related_field().name})
url = '' url = '?' + '&amp;'.join(['%s=%s' % (k, v) for k, v in params.items()])
if not attrs.has_key('class'): if not attrs.has_key('class'):
attrs['class'] = 'vForeignKeyRawIdAdminField' # The JavaScript looks for this hook. attrs['class'] = 'vForeignKeyRawIdAdminField' # The JavaScript looks for this hook.
output = [super(ForeignKeyRawIdWidget, self).render(name, value, attrs)] output = [super(ForeignKeyRawIdWidget, self).render(name, value, attrs)]
Expand All @@ -121,27 +123,28 @@ def render(self, name, value, attrs=None):
if value: if value:
output.append(self.label_for_value(value)) output.append(self.label_for_value(value))
return mark_safe(u''.join(output)) return mark_safe(u''.join(output))

def label_for_value(self, value): def label_for_value(self, value):
return '&nbsp;<strong>%s</strong>' % \ key = self.rel.get_related_field().name
truncate_words(self.rel.to.objects.get(pk=value), 14) obj = self.rel.to.objects.get(**{key: value})

return '&nbsp;<strong>%s</strong>' % truncate_words(obj, 14)

class ManyToManyRawIdWidget(ForeignKeyRawIdWidget): class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):
""" """
A Widget for displaying ManyToMany ids in the "raw_id" interface rather than A Widget for displaying ManyToMany ids in the "raw_id" interface rather than
in a <select multiple> box. in a <select multiple> box.
""" """
def __init__(self, rel, attrs=None): def __init__(self, rel, attrs=None):
super(ManyToManyRawIdWidget, self).__init__(rel, attrs) super(ManyToManyRawIdWidget, self).__init__(rel, attrs)

def render(self, name, value, attrs=None): def render(self, name, value, attrs=None):
attrs['class'] = 'vManyToManyRawIdAdminField' attrs['class'] = 'vManyToManyRawIdAdminField'
if value: if value:
value = ','.join([str(v) for v in value]) value = ','.join([str(v) for v in value])
else: else:
value = '' value = ''
return super(ManyToManyRawIdWidget, self).render(name, value, attrs) return super(ManyToManyRawIdWidget, self).render(name, value, attrs)

def label_for_value(self, value): def label_for_value(self, value):
return '' return ''


Expand All @@ -152,7 +155,7 @@ def value_from_datadict(self, data, files, name):
if value: if value:
return [value] return [value]
return None return None

def _has_changed(self, initial, data): def _has_changed(self, initial, data):
if initial is None: if initial is None:
initial = [] initial = []
Expand Down
7 changes: 6 additions & 1 deletion django/db/models/fields/related.py
Expand Up @@ -691,7 +691,12 @@ def contribute_to_related_class(self, cls, related):
setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related)) setattr(cls, related.get_accessor_name(), ForeignRelatedObjectsDescriptor(related))


def formfield(self, **kwargs): 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) defaults.update(kwargs)
return super(ForeignKey, self).formfield(**defaults) return super(ForeignKey, self).formfield(**defaults)


Expand Down
19 changes: 14 additions & 5 deletions django/forms/models.py
Expand Up @@ -550,14 +550,21 @@ def __iter__(self):
if self.field.cache_choices: if self.field.cache_choices:
if self.field.choice_cache is None: if self.field.choice_cache is None:
self.field.choice_cache = [ self.field.choice_cache = [
(obj.pk, self.field.label_from_instance(obj)) self.choice(obj) for obj in self.queryset.all()
for obj in self.queryset.all()
] ]
for choice in self.field.choice_cache: for choice in self.field.choice_cache:
yield choice yield choice
else: else:
for obj in self.queryset.all(): 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): class ModelChoiceField(ChoiceField):
"""A ChoiceField whose choices are a model QuerySet.""" """A ChoiceField whose choices are a model QuerySet."""
Expand All @@ -570,7 +577,7 @@ class ModelChoiceField(ChoiceField):


def __init__(self, queryset, empty_label=u"---------", cache_choices=False, def __init__(self, queryset, empty_label=u"---------", cache_choices=False,
required=True, widget=None, label=None, initial=None, 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.empty_label = empty_label
self.cache_choices = cache_choices self.cache_choices = cache_choices


Expand All @@ -580,6 +587,7 @@ def __init__(self, queryset, empty_label=u"---------", cache_choices=False,
*args, **kwargs) *args, **kwargs)
self.queryset = queryset self.queryset = queryset
self.choice_cache = None self.choice_cache = None
self.to_field_name = to_field_name


def _get_queryset(self): def _get_queryset(self):
return self._queryset return self._queryset
Expand Down Expand Up @@ -622,7 +630,8 @@ def clean(self, value):
if value in EMPTY_VALUES: if value in EMPTY_VALUES:
return None return None
try: 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: except self.queryset.model.DoesNotExist:
raise ValidationError(self.error_messages['invalid_choice']) raise ValidationError(self.error_messages['invalid_choice'])
return value return value
Expand Down
43 changes: 41 additions & 2 deletions tests/modeltests/model_forms/models.py
Expand Up @@ -78,7 +78,7 @@ class BetterWriter(Writer):
class WriterProfile(models.Model): class WriterProfile(models.Model):
writer = models.OneToOneField(Writer, primary_key=True) writer = models.OneToOneField(Writer, primary_key=True)
age = models.PositiveIntegerField() age = models.PositiveIntegerField()

def __unicode__(self): def __unicode__(self):
return "%s is %s" % (self.writer, self.age) return "%s is %s" % (self.writer, self.age)


Expand Down Expand Up @@ -137,7 +137,14 @@ class Meta:
class ArticleStatus(models.Model): class ArticleStatus(models.Model):
status = models.CharField(max_length=2, choices=ARTICLE_STATUS_CHAR, blank=True, null=True) 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': """ __test__ = {'API_TESTS': """
>>> from django import forms >>> from django import forms
>>> from django.forms.models import ModelForm, model_to_dict >>> from django.forms.models import ModelForm, model_to_dict
Expand Down Expand Up @@ -1135,7 +1142,7 @@ class ArticleStatus(models.Model):
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter only digits separated by commas.'] ValidationError: [u'Enter only digits separated by commas.']
>>> f.clean(',,,,') >>> f.clean(',,,,')
u',,,,' u',,,,'
>>> f.clean('1.2') >>> f.clean('1.2')
Traceback (most recent call last): Traceback (most recent call last):
Expand Down Expand Up @@ -1204,4 +1211,36 @@ class ArticleStatus(models.Model):
... ...
ValidationError: [u'Select a valid choice. z is not one of the available choices.'] 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']
<select name="parent" id="id_parent">
<option value="">---------</option>
<option value="86" selected="selected">Apple</option>
<option value="22">Pear</option>
<option value="87">Core</option>
</select>
>>> data = model_to_dict(core)
>>> data['parent'] = '22'
>>> form = InventoryForm(data=data, instance=core)
>>> core = form.save()
>>> core.parent
<Inventory: Pear>
"""} """}
23 changes: 20 additions & 3 deletions tests/regressiontests/admin_widgets/models.py
Expand Up @@ -5,25 +5,33 @@


class Member(models.Model): class Member(models.Model):
name = models.CharField(max_length=100) name = models.CharField(max_length=100)

def __unicode__(self): def __unicode__(self):
return self.name return self.name


class Band(models.Model): class Band(models.Model):
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
members = models.ManyToManyField(Member) members = models.ManyToManyField(Member)

def __unicode__(self): def __unicode__(self):
return self.name return self.name


class Album(models.Model): class Album(models.Model):
band = models.ForeignKey(Band) band = models.ForeignKey(Band)
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
cover_art = models.FileField(upload_to='albums') cover_art = models.FileField(upload_to='albums')

def __unicode__(self): def __unicode__(self):
return self.name 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': """ __test__ = {'WIDGETS_TESTS': """
>>> from datetime import datetime >>> from datetime import datetime
>>> from django.utils.html import escape, conditional_escape >>> from django.utils.html import escape, conditional_escape
Expand Down Expand Up @@ -84,6 +92,15 @@ def __unicode__(self):
>>> w._has_changed([1, 2], [u'1', u'3']) >>> w._has_changed([1, 2], [u'1', u'3'])
True 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={})
<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>
""" % { """ % {
'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX, 'ADMIN_MEDIA_PREFIX': settings.ADMIN_MEDIA_PREFIX,
'STORAGE_URL': default_storage.url(''), 'STORAGE_URL': default_storage.url(''),
Expand Down

0 comments on commit ce47d4a

Please sign in to comment.