Kaydet (Commit) f212b24b authored tarafından Jacob Kaplan-Moss's avatar Jacob Kaplan-Moss

Cleaned up and refactored `ModelAdmin.formfield_for_dbfield`:

  * The new method uses an admin configuration option (`formfield_overrides`); this makes custom admin widgets especially easy.
  * Refactored what was left of `formfield_for_dbfield` into a handful of smaller methods so that it's easier to hook in and return custom fields where needed.
  * These `formfield_for_*` methods now pass around `request` so that you can easily modify fields based on request (as in #3987).

Fixes #8306, #3987, #9148.

Thanks to James Bennet for the original patch; Alex Gaynor and Brian Rosner also contributed.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@9760 bcc190cf-cafb-0310-a4f2-bffc1f526a37
üst d579e716
......@@ -13,6 +13,7 @@ from django.shortcuts import get_object_or_404, render_to_response
from django.utils.functional import update_wrapper
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.functional import curry
from django.utils.text import capfirst, get_text_list
from django.utils.translation import ugettext as _
from django.utils.encoding import force_unicode
......@@ -28,8 +29,28 @@ get_ul_class = lambda x: 'radiolist%s' % ((x == HORIZONTAL) and ' inline' or '')
class IncorrectLookupParameters(Exception):
pass
# Defaults for formfield_overrides. ModelAdmin subclasses can change this
# by adding to ModelAdmin.formfield_overrides.
FORMFIELD_FOR_DBFIELD_DEFAULTS = {
models.DateTimeField: {
'form_class': forms.SplitDateTimeField,
'widget': widgets.AdminSplitDateTime
},
models.DateField: {'widget': widgets.AdminDateWidget},
models.TimeField: {'widget': widgets.AdminTimeWidget},
models.TextField: {'widget': widgets.AdminTextareaWidget},
models.URLField: {'widget': widgets.AdminURLFieldWidget},
models.IntegerField: {'widget': widgets.AdminIntegerFieldWidget},
models.CharField: {'widget': widgets.AdminTextInputWidget},
models.ImageField: {'widget': widgets.AdminFileWidget},
models.FileField: {'widget': widgets.AdminFileWidget},
}
class BaseModelAdmin(object):
"""Functionality common to both ModelAdmin and InlineAdmin."""
raw_id_fields = ()
fields = None
exclude = None
......@@ -39,6 +60,10 @@ class BaseModelAdmin(object):
filter_horizontal = ()
radio_fields = {}
prepopulated_fields = {}
formfield_overrides = {}
def __init__(self):
self.formfield_overrides = dict(FORMFIELD_FOR_DBFIELD_DEFAULTS, **self.formfield_overrides)
def formfield_for_dbfield(self, db_field, **kwargs):
"""
......@@ -47,101 +72,92 @@ class BaseModelAdmin(object):
If kwargs are given, they're passed to the form Field's constructor.
"""
request = kwargs.pop("request", None)
# If the field specifies choices, we don't need to look for special
# admin widgets - we just need to use a select widget of some kind.
if db_field.choices:
if db_field.name in self.radio_fields:
# If the field is named as a radio_field, use a RadioSelect
return self.formfield_for_choice_field(db_field, request, **kwargs)
# ForeignKey or ManyToManyFields
if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)):
# Combine the field kwargs with any options for formfield_overrides.
# Make sure the passed in **kwargs override anything in
# formfield_overrides because **kwargs is more specific, and should
# always win.
if db_field.__class__ in self.formfield_overrides:
kwargs = dict(self.formfield_overrides[db_field.__class__], **kwargs)
# Get the correct formfield.
if isinstance(db_field, models.ForeignKey):
formfield = self.formfield_for_foreignkey(db_field, request, **kwargs)
elif isinstance(db_field, models.ManyToManyField):
formfield = self.formfield_for_manytomany(db_field, request, **kwargs)
# For non-raw_id fields, wrap the widget with a wrapper that adds
# extra HTML -- the "add other" interface -- to the end of the
# rendered output. formfield can be None if it came from a
# OneToOneField with parent_link=True or a M2M intermediary.
if formfield and db_field.name not in self.raw_id_fields:
formfield.widget = widgets.RelatedFieldWidgetWrapper(formfield.widget, db_field.rel, self.admin_site)
return formfield
# If we've got overrides for the formfield defined, use 'em. **kwargs
# passed to formfield_for_dbfield override the defaults.
if db_field.__class__ in self.formfield_overrides:
kwargs = dict(self.formfield_overrides[db_field.__class__], **kwargs)
return db_field.formfield(**kwargs)
# For any other type of field, just call its formfield() method.
return db_field.formfield(**kwargs)
def formfield_for_choice_field(self, db_field, request=None, **kwargs):
"""
Get a form Field for a database Field that has declared choices.
"""
# If the field is named as a radio_field, use a RadioSelect
if db_field.name in self.radio_fields:
# Avoid stomping on custom widget/choices arguments.
if 'widget' not in kwargs:
kwargs['widget'] = widgets.AdminRadioSelect(attrs={
'class': get_ul_class(self.radio_fields[db_field.name]),
})
if 'choices' not in kwargs:
kwargs['choices'] = db_field.get_choices(
include_blank = db_field.blank,
blank_choice=[('', _('None'))]
)
return db_field.formfield(**kwargs)
else:
# Otherwise, use the default select widget.
return db_field.formfield(**kwargs)
# For DateTimeFields, use a special field and widget.
if isinstance(db_field, models.DateTimeField):
kwargs['form_class'] = forms.SplitDateTimeField
kwargs['widget'] = widgets.AdminSplitDateTime()
return db_field.formfield(**kwargs)
# For DateFields, add a custom CSS class.
if isinstance(db_field, models.DateField):
kwargs['widget'] = widgets.AdminDateWidget
return db_field.formfield(**kwargs)
# For TimeFields, add a custom CSS class.
if isinstance(db_field, models.TimeField):
kwargs['widget'] = widgets.AdminTimeWidget
return db_field.formfield(**kwargs)
# For TextFields, add a custom CSS class.
if isinstance(db_field, models.TextField):
kwargs['widget'] = widgets.AdminTextareaWidget
return db_field.formfield(**kwargs)
# For URLFields, add a custom CSS class.
if isinstance(db_field, models.URLField):
kwargs['widget'] = widgets.AdminURLFieldWidget
return db_field.formfield(**kwargs)
# For IntegerFields, add a custom CSS class.
if isinstance(db_field, models.IntegerField):
kwargs['widget'] = widgets.AdminIntegerFieldWidget
return db_field.formfield(**kwargs)
# For CommaSeparatedIntegerFields, add a custom CSS class.
if isinstance(db_field, models.CommaSeparatedIntegerField):
kwargs['widget'] = widgets.AdminCommaSeparatedIntegerFieldWidget
return db_field.formfield(**kwargs)
# For TextInputs, add a custom CSS class.
if isinstance(db_field, models.CharField):
kwargs['widget'] = widgets.AdminTextInputWidget
return db_field.formfield(**kwargs)
return db_field.formfield(**kwargs)
def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
"""
Get a form Field for a ForeignKey.
"""
if db_field.name in self.raw_id_fields:
kwargs['widget'] = widgets.ForeignKeyRawIdWidget(db_field.rel)
elif db_field.name in self.radio_fields:
kwargs['widget'] = widgets.AdminRadioSelect(attrs={
'class': get_ul_class(self.radio_fields[db_field.name]),
})
kwargs['empty_label'] = db_field.blank and _('None') or None
# For FileFields and ImageFields add a link to the current file.
if isinstance(db_field, models.ImageField) or isinstance(db_field, models.FileField):
kwargs['widget'] = widgets.AdminFileWidget
return db_field.formfield(**kwargs)
return db_field.formfield(**kwargs)
def formfield_for_manytomany(self, db_field, request=None, **kwargs):
"""
Get a form Field for a ManyToManyField.
"""
# If it uses an intermediary model, don't show field in admin.
if db_field.rel.through is not None:
return None
# For ForeignKey or ManyToManyFields, use a special widget.
if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)):
if isinstance(db_field, models.ForeignKey) and db_field.name in self.raw_id_fields:
kwargs['widget'] = widgets.ForeignKeyRawIdWidget(db_field.rel)
elif isinstance(db_field, models.ForeignKey) and db_field.name in self.radio_fields:
kwargs['widget'] = widgets.AdminRadioSelect(attrs={
'class': get_ul_class(self.radio_fields[db_field.name]),
})
kwargs['empty_label'] = db_field.blank and _('None') or None
else:
if isinstance(db_field, models.ManyToManyField):
# If it uses an intermediary model, don't show field in admin.
if db_field.rel.through is not None:
return None
elif db_field.name in self.raw_id_fields:
kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel)
kwargs['help_text'] = ''
elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)):
kwargs['widget'] = widgets.FilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical))
# Wrap the widget's render() method with a method that adds
# extra HTML to the end of the rendered output.
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 can be None if it came from a OneToOneField with
# parent_link=True
if formfield is not None:
formfield.widget = widgets.RelatedFieldWidgetWrapper(formfield.widget, db_field.rel, self.admin_site)
return formfield
if db_field.name in self.raw_id_fields:
kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel)
kwargs['help_text'] = ''
elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)):
kwargs['widget'] = widgets.FilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical))
# For any other type of field, just call its formfield() method.
return db_field.formfield(**kwargs)
def _declared_fieldsets(self):
......@@ -292,7 +308,7 @@ class ModelAdmin(BaseModelAdmin):
"form": self.form,
"fields": fields,
"exclude": exclude + kwargs.get("exclude", []),
"formfield_callback": self.formfield_for_dbfield,
"formfield_callback": curry(self.formfield_for_dbfield, request=request),
}
defaults.update(kwargs)
return modelform_factory(self.model, **defaults)
......@@ -837,7 +853,7 @@ class InlineModelAdmin(BaseModelAdmin):
"fk_name": self.fk_name,
"fields": fields,
"exclude": exclude + kwargs.get("exclude", []),
"formfield_callback": self.formfield_for_dbfield,
"formfield_callback": curry(self.formfield_for_dbfield, request=request),
"extra": self.extra,
"max_num": self.max_num,
}
......
......@@ -597,6 +597,47 @@ with an operator:
Performs a full-text match. This is like the default search method but uses
an index. Currently this is only available for MySQL.
``formfield_overrides``
~~~~~~~~~~~~~~~~~~~~~~~
This provides a quick-and-dirty way to override some of the
:class:`~django.forms.Field` options for use in the admin.
``formfield_overrides`` is a dictionary mapping a field class to a dict of
arguments to pass to the field at construction time.
Since that's a bit abstract, let's look at a concrete example. The most common
use of ``formfield_overrides`` is to add a custom widget for a certain type of
field. So, imagine we've written a ``RichTextEditorWidget`` that we'd like to
use for large text fields instead of the default ``<textarea>``. Here's how we'd
do that::
from django.db import models
from django.contrib import admin
# Import our custom widget and our model from where they're defined
from myapp.widgets import RichTextEditorWidget
from myapp.models import MyModel
class MyModelAdmin(admin.ModelAdmin):
formfield_overrides = {
models.TextField: {'widget': RichTextEditorWidget},
}
Note that the key in the dictionary is the actual field class, *not* a string.
The value is another dictionary; these arguments will be passed to
:meth:`~django.forms.Field.__init__`. See :ref:`ref-forms-api` for details.
.. warning::
If you want to use a custom widget with a relation field (i.e.
:class:`~django.db.models.ForeignKey` or
:class:`~django.db.models.ManyToManyField`), make sure you haven't included
that field's name in ``raw_id_fields`` or ``radio_fields``.
``formfield_overrides`` won't let you change the widget on relation fields
that have ``raw_id_fields`` or ``radio_fields`` set. That's because
``raw_id_fields`` and ``radio_fields`` imply custom widgets of their own.
``ModelAdmin`` methods
----------------------
......@@ -675,6 +716,23 @@ Notice the wrapped view in the fifth line above::
This wrapping will protect ``self.my_view`` from unauthorized access.
``formfield_for_foreignkey(self, db_field, request, **kwargs)``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``formfield_for_foreignkey`` method on a ``ModelAdmin`` allows you to
override the default formfield for a foreign key field. For example, to
return a subset of objects for this foreign key field based on the user::
class MyModelAdmin(admin.ModelAdmin):
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "car":
kwargs["queryset"] = Car.object.filter(owner=request.user)
return db_field.formfield(**kwargs)
return super(MyModelAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
This uses the ``HttpRequest`` instance to filter the ``Car`` foreign key field
to only the cars owned by the ``User`` instance.
``ModelAdmin`` media definitions
--------------------------------
......
<?xml version="1.0" encoding="utf-8"?>
<django-objects version="1.0">
<object pk="100" model="auth.user">
<field type="CharField" name="username">super</field>
<field type="CharField" name="first_name">Super</field>
<field type="CharField" name="last_name">User</field>
<field type="CharField" name="email">super@example.com</field>
<field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field>
<field type="BooleanField" name="is_staff">True</field>
<field type="BooleanField" name="is_active">True</field>
<field type="BooleanField" name="is_superuser">True</field>
<field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field>
<field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field>
<field to="auth.group" name="groups" rel="ManyToManyRel"></field>
<field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field>
</object>
<object pk="101" model="auth.user">
<field type="CharField" name="username">testser</field>
<field type="CharField" name="first_name">Add</field>
<field type="CharField" name="last_name">User</field>
<field type="CharField" name="email">auser@example.com</field>
<field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field>
<field type="BooleanField" name="is_staff">True</field>
<field type="BooleanField" name="is_active">True</field>
<field type="BooleanField" name="is_superuser">False</field>
<field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field>
<field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field>
<field to="auth.group" name="groups" rel="ManyToManyRel"></field>
<field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field>
</object>
<object pk="1" model="admin_widgets.car">
<field to="auth.user" name="owner" rel="ManyToOneRel">100</field>
<field type="CharField" name="make">Volkswagon</field>
<field type="CharField" name="model">Passat</field>
</object>
<object pk="2" model="admin_widgets.car">
<field to="auth.user" name="owner" rel="ManyToOneRel">101</field>
<field type="CharField" name="make">BMW</field>
<field type="CharField" name="model">M3</field>
</object>
</django-objects>
\ No newline at end of file
......@@ -2,9 +2,12 @@
from django.conf import settings
from django.db import models
from django.core.files.storage import default_storage
from django.contrib.auth.models import User
class Member(models.Model):
name = models.CharField(max_length=100)
birthdate = models.DateTimeField(blank=True, null=True)
gender = models.CharField(max_length=1, blank=True, choices=[('M','Male'), ('F', 'Female')])
def __unicode__(self):
return self.name
......@@ -40,6 +43,28 @@ class Inventory(models.Model):
def __unicode__(self):
return self.name
class Event(models.Model):
band = models.ForeignKey(Band)
date = models.DateField(blank=True, null=True)
start_time = models.TimeField(blank=True, null=True)
description = models.TextField(blank=True)
link = models.URLField(blank=True)
min_age = models.IntegerField(blank=True, null=True)
class Car(models.Model):
owner = models.ForeignKey(User)
make = models.CharField(max_length=30)
model = models.CharField(max_length=30)
def __unicode__(self):
return u"%s %s" % (self.make, self.model)
class CarTire(models.Model):
"""
A single car tire. This to test that a user can only select their own cars.
"""
car = models.ForeignKey(Car)
__test__ = {'WIDGETS_TESTS': """
>>> from datetime import datetime
......
from django import forms
from django.contrib import admin
from django.contrib.admin import widgets
from unittest import TestCase
from django.test import TestCase as DjangoTestCase
import models
class AdminFormfieldForDBFieldTests(TestCase):
"""
Tests for correct behavior of ModelAdmin.formfield_for_dbfield
"""
def assertFormfield(self, model, fieldname, widgetclass, **admin_overrides):
"""
Helper to call formfield_for_dbfield for a given model and field name
and verify that the returned formfield is appropriate.
"""
# Override any settings on the model admin
class MyModelAdmin(admin.ModelAdmin): pass
for k in admin_overrides:
setattr(MyModelAdmin, k, admin_overrides[k])
# Construct the admin, and ask it for a formfield
ma = MyModelAdmin(model, admin.site)
ff = ma.formfield_for_dbfield(model._meta.get_field(fieldname), request=None)
# "unwrap" the widget wrapper, if needed
if isinstance(ff.widget, widgets.RelatedFieldWidgetWrapper):
widget = ff.widget.widget
else:
widget = ff.widget
# Check that we got a field of the right type
self.assert_(
isinstance(widget, widgetclass),
"Wrong widget for %s.%s: expected %s, got %s" % \
(model.__class__.__name__, fieldname, widgetclass, type(widget))
)
# Return the formfield so that other tests can continue
return ff
def testDateField(self):
self.assertFormfield(models.Event, 'date', widgets.AdminDateWidget)
def testDateTimeField(self):
self.assertFormfield(models.Member, 'birthdate', widgets.AdminSplitDateTime)
def testTimeField(self):
self.assertFormfield(models.Event, 'start_time', widgets.AdminTimeWidget)
def testTextField(self):
self.assertFormfield(models.Event, 'description', widgets.AdminTextareaWidget)
def testURLField(self):
self.assertFormfield(models.Event, 'link', widgets.AdminURLFieldWidget)
def testIntegerField(self):
self.assertFormfield(models.Event, 'min_age', widgets.AdminIntegerFieldWidget)
def testCharField(self):
self.assertFormfield(models.Member, 'name', widgets.AdminTextInputWidget)
def testFileField(self):
self.assertFormfield(models.Album, 'cover_art', widgets.AdminFileWidget)
def testForeignKey(self):
self.assertFormfield(models.Event, 'band', forms.Select)
def testRawIDForeignKey(self):
self.assertFormfield(models.Event, 'band', widgets.ForeignKeyRawIdWidget,
raw_id_fields=['band'])
def testRadioFieldsForeignKey(self):
ff = self.assertFormfield(models.Event, 'band', widgets.AdminRadioSelect,
radio_fields={'band':admin.VERTICAL})
self.assertEqual(ff.empty_label, None)
def testManyToMany(self):
self.assertFormfield(models.Band, 'members', forms.SelectMultiple)
def testRawIDManyTOMany(self):
self.assertFormfield(models.Band, 'members', widgets.ManyToManyRawIdWidget,
raw_id_fields=['members'])
def testFilteredManyToMany(self):
self.assertFormfield(models.Band, 'members', widgets.FilteredSelectMultiple,
filter_vertical=['members'])
def testFormfieldOverrides(self):
self.assertFormfield(models.Event, 'date', forms.TextInput,
formfield_overrides={'widget': forms.TextInput})
def testFieldWithChoices(self):
self.assertFormfield(models.Member, 'gender', forms.Select)
def testChoicesWithRadioFields(self):
self.assertFormfield(models.Member, 'gender', widgets.AdminRadioSelect,
radio_fields={'gender':admin.VERTICAL})
class AdminFormfieldForDBFieldWithRequestTests(DjangoTestCase):
fixtures = ["admin-widgets-users.xml"]
def testFilterChoicesByRequestUser(self):
"""
Ensure the user can only see their own cars in the foreign key dropdown.
"""
self.client.login(username="super", password="secret")
response = self.client.get("/widget_admin/admin_widgets/cartire/add/")
self.assert_("BMW M3" not in response.content)
self.assert_("Volkswagon Passat" in response.content)
from django.conf.urls.defaults import *
import widgetadmin
urlpatterns = patterns('',
(r'^', include(widgetadmin.site.urls)),
)
"""
"""
from django.contrib import admin
import models
class WidgetAdmin(admin.AdminSite):
pass
class CarTireAdmin(admin.ModelAdmin):
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "car":
kwargs["queryset"] = models.Car.objects.filter(owner=request.user)
return db_field.formfield(**kwargs)
return super(CarTireAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
site = WidgetAdmin()
site.register(models.Car)
site.register(models.CarTire, CarTireAdmin)
......@@ -24,6 +24,9 @@ urlpatterns = patterns('',
# admin view tests
(r'^test_admin/', include('regressiontests.admin_views.urls')),
(r'^generic_inline_admin/', include('regressiontests.generic_inline_admin.urls')),
# admin widget tests
(r'widget_admin/', include('regressiontests.admin_widgets.urls')),
(r'^utils/', include('regressiontests.utils.urls')),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment