diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index acc7d3962a..27f8676a64 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -198,7 +198,7 @@ class BaseModelAdmin(object):
formfield = db_field.formfield(**kwargs)
# Don't wrap raw_id fields. Their add function is in the popup window.
if not db_field.name in self.raw_id_fields:
- formfield.widget.render = widgets.RelatedFieldWidgetWrapper(formfield.widget.render, db_field.rel, self.admin_site)
+ formfield.widget = widgets.RelatedFieldWidgetWrapper(formfield.widget, db_field.rel, self.admin_site)
return formfield
if db_field.choices and db_field.name in self.radio_fields:
diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
index e7ea4aa129..4ae8889ac4 100644
--- a/django/contrib/admin/widgets.py
+++ b/django/contrib/admin/widgets.py
@@ -2,6 +2,8 @@
Form Widget classes specific to the Django admin site.
"""
+import copy
+
from django import newforms as forms
from django.newforms.widgets import RadioFieldRenderer
from django.newforms.util import flatatt
@@ -162,21 +164,34 @@ class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):
return True
return False
-class RelatedFieldWidgetWrapper(object):
+class RelatedFieldWidgetWrapper(forms.Widget):
"""
- This class is a wrapper whose __call__() method mimics the interface of a
- Widget's render() method.
+ This class is a wrapper to a given widget to add the add icon for the
+ admin interface.
"""
- def __init__(self, render_func, rel, admin_site):
- self.render_func, self.rel = render_func, rel
+ def __init__(self, widget, rel, admin_site):
+ self.is_hidden = widget.is_hidden
+ self.needs_multipart_form = widget.needs_multipart_form
+ self.attrs = widget.attrs
+ self.choices = widget.choices
+ self.widget = widget
+ self.rel = rel
# so we can check if the related object is registered with this AdminSite
self.admin_site = admin_site
- def __call__(self, name, value, *args, **kwargs):
+ def __deepcopy__(self, memo):
+ obj = copy.copy(self)
+ obj.widget = copy.deepcopy(self.widget, memo)
+ obj.attrs = self.widget.attrs
+ memo[id(self)] = obj
+ return obj
+
+ def render(self, name, value, *args, **kwargs):
from django.conf import settings
rel_to = self.rel.to
related_url = '../../../%s/%s/' % (rel_to._meta.app_label, rel_to._meta.object_name.lower())
- output = [self.render_func(name, value, *args, **kwargs)]
+ self.widget.choices = self.choices
+ output = [self.widget.render(name, value, *args, **kwargs)]
if rel_to in self.admin_site._registry: # If the related object has an admin interface:
# TODO: "id_" is hard-coded here. This should instead use the correct
# API to determine the ID dynamically.
@@ -185,7 +200,16 @@ class RelatedFieldWidgetWrapper(object):
output.append(u'' % settings.ADMIN_MEDIA_PREFIX)
return mark_safe(u''.join(output))
- def __deepcopy__(self, memo):
- # There's no reason to deepcopy admin_site, etc, so just return self.
- memo[id(self)] = self
- return self
+ def build_attrs(self, extra_attrs=None, **kwargs):
+ "Helper function for building an attribute dictionary."
+ self.attrs = self.widget.build_attrs(extra_attrs=None, **kwargs)
+ return self.attrs
+
+ def value_from_datadict(self, data, files, name):
+ return self.widget.value_from_datadict(data, files, name)
+
+ def _has_changed(self, initial, data):
+ return self.widget._has_changed(initial, data)
+
+ def id_for_label(self, id_):
+ return self.widget.id_for_label(id_)
diff --git a/tests/regressiontests/modeladmin/models.py b/tests/regressiontests/modeladmin/models.py
index e1690c143d..8fdcbe1bde 100644
--- a/tests/regressiontests/modeladmin/models.py
+++ b/tests/regressiontests/modeladmin/models.py
@@ -122,21 +122,50 @@ properly. This won't, however, break any of the admin widgets or media.
>>> type(ma.get_form(request).base_fields['sign_date'].widget)
+If we need to override the queryset of a ModelChoiceField in our custom form
+make sure that RelatedFieldWidgetWrapper doesn't mess that up.
+
+>>> band2 = Band(name='The Beetles', bio='', sign_date=date(1962, 1, 1))
+>>> band2.save()
+
+>>> class AdminConcertForm(forms.ModelForm):
+... class Meta:
+... model = Concert
+...
+... def __init__(self, *args, **kwargs):
+... super(AdminConcertForm, self).__init__(*args, **kwargs)
+... self.fields["main_band"].queryset = Band.objects.filter(name='The Doors')
+
+>>> class ConcertAdmin(ModelAdmin):
+... form = AdminConcertForm
+
+>>> ma = ConcertAdmin(Concert, site)
+>>> form = ma.get_form(request)()
+>>> print form["main_band"]
+
+
+>>> band2.delete()
+
# radio_fields behavior ################################################
First, without any radio_fields specified, the widgets for ForeignKey
and fields with choices specified ought to be a basic Select widget.
-For Select fields, all of the choices lists have a first entry of dashes.
+ForeignKey widgets in the admin are wrapped with RelatedFieldWidgetWrapper so
+they need to be handled properly when type checking. For Select fields, all of
+the choices lists have a first entry of dashes.
>>> cma = ModelAdmin(Concert, site)
>>> cmafa = cma.get_form(request)
->>> type(cmafa.base_fields['main_band'].widget)
+>>> type(cmafa.base_fields['main_band'].widget.widget)
>>> list(cmafa.base_fields['main_band'].widget.choices)
[(u'', u'---------'), (1, u'The Doors')]
->>> type(cmafa.base_fields['opening_band'].widget)
+>>> type(cmafa.base_fields['opening_band'].widget.widget)
>>> list(cmafa.base_fields['opening_band'].widget.choices)
[(u'', u'---------'), (1, u'The Doors')]
@@ -152,7 +181,7 @@ For Select fields, all of the choices lists have a first entry of dashes.
[('', '---------'), (1, 'Plane'), (2, 'Train'), (3, 'Bus')]
Now specify all the fields as radio_fields. Widgets should now be
-RadioSelect, and the choices list should have a first entry of 'None' iff
+RadioSelect, and the choices list should have a first entry of 'None' if
blank=True for the model field. Finally, the widget should have the
'radiolist' attr, and 'inline' as well if the field is specified HORIZONTAL.
@@ -167,14 +196,14 @@ blank=True for the model field. Finally, the widget should have the
>>> cma = ConcertAdmin(Concert, site)
>>> cmafa = cma.get_form(request)
->>> type(cmafa.base_fields['main_band'].widget)
+>>> type(cmafa.base_fields['main_band'].widget.widget)
>>> cmafa.base_fields['main_band'].widget.attrs
{'class': 'radiolist inline'}
>>> list(cmafa.base_fields['main_band'].widget.choices)
[(1, u'The Doors')]
->>> type(cmafa.base_fields['opening_band'].widget)
+>>> type(cmafa.base_fields['opening_band'].widget.widget)
>>> cmafa.base_fields['opening_band'].widget.attrs
{'class': 'radiolist'}