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'Add Another' % 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'}