Skip to content

Commit

Permalink
Cleaned up and refactored ModelAdmin.formfield_for_dbfield:
Browse files Browse the repository at this point in the history
  * 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
  • Loading branch information
jacobian committed Jan 16, 2009
1 parent d579e71 commit f212b24
Show file tree
Hide file tree
Showing 8 changed files with 369 additions and 83 deletions.
182 changes: 99 additions & 83 deletions django/contrib/admin/options.py
Expand Up @@ -13,6 +13,7 @@
from django.utils.functional import update_wrapper from django.utils.functional import update_wrapper
from django.utils.html import escape from django.utils.html import escape
from django.utils.safestring import mark_safe 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.text import capfirst, get_text_list
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.encoding import force_unicode from django.utils.encoding import force_unicode
Expand All @@ -28,8 +29,28 @@
class IncorrectLookupParameters(Exception): class IncorrectLookupParameters(Exception):
pass 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): class BaseModelAdmin(object):
"""Functionality common to both ModelAdmin and InlineAdmin.""" """Functionality common to both ModelAdmin and InlineAdmin."""

raw_id_fields = () raw_id_fields = ()
fields = None fields = None
exclude = None exclude = None
Expand All @@ -39,6 +60,10 @@ class BaseModelAdmin(object):
filter_horizontal = () filter_horizontal = ()
radio_fields = {} radio_fields = {}
prepopulated_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): def formfield_for_dbfield(self, db_field, **kwargs):
""" """
Expand All @@ -47,101 +72,92 @@ def formfield_for_dbfield(self, db_field, **kwargs):
If kwargs are given, they're passed to the form Field's constructor. 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 # 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. # admin widgets - we just need to use a select widget of some kind.
if db_field.choices: if db_field.choices:
if db_field.name in self.radio_fields: return self.formfield_for_choice_field(db_field, request, **kwargs)
# If the field is named as a radio_field, use a RadioSelect
# 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={ kwargs['widget'] = widgets.AdminRadioSelect(attrs={
'class': get_ul_class(self.radio_fields[db_field.name]), 'class': get_ul_class(self.radio_fields[db_field.name]),
}) })
if 'choices' not in kwargs:
kwargs['choices'] = db_field.get_choices( kwargs['choices'] = db_field.get_choices(
include_blank = db_field.blank, include_blank = db_field.blank,
blank_choice=[('', _('None'))] blank_choice=[('', _('None'))]
) )
return db_field.formfield(**kwargs) return db_field.formfield(**kwargs)
else:
# Otherwise, use the default select widget. def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
return db_field.formfield(**kwargs) """

Get a form Field for a ForeignKey.
# For DateTimeFields, use a special field and widget. """
if isinstance(db_field, models.DateTimeField): if db_field.name in self.raw_id_fields:
kwargs['form_class'] = forms.SplitDateTimeField kwargs['widget'] = widgets.ForeignKeyRawIdWidget(db_field.rel)
kwargs['widget'] = widgets.AdminSplitDateTime() elif db_field.name in self.radio_fields:
return db_field.formfield(**kwargs) kwargs['widget'] = widgets.AdminRadioSelect(attrs={

'class': get_ul_class(self.radio_fields[db_field.name]),
# For DateFields, add a custom CSS class. })
if isinstance(db_field, models.DateField): kwargs['empty_label'] = db_field.blank and _('None') or None
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)


# For FileFields and ImageFields add a link to the current file. return db_field.formfield(**kwargs)
if isinstance(db_field, models.ImageField) or isinstance(db_field, models.FileField):
kwargs['widget'] = widgets.AdminFileWidget def formfield_for_manytomany(self, db_field, request=None, **kwargs):
return db_field.formfield(**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 db_field.name in self.raw_id_fields:
if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)): kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel)
if isinstance(db_field, models.ForeignKey) and db_field.name in self.raw_id_fields: kwargs['help_text'] = ''
kwargs['widget'] = widgets.ForeignKeyRawIdWidget(db_field.rel) elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)):
elif isinstance(db_field, models.ForeignKey) and db_field.name in self.radio_fields: kwargs['widget'] = widgets.FilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical))
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


# For any other type of field, just call its formfield() method.
return db_field.formfield(**kwargs) return db_field.formfield(**kwargs)


def _declared_fieldsets(self): def _declared_fieldsets(self):
Expand Down Expand Up @@ -292,7 +308,7 @@ def get_form(self, request, obj=None, **kwargs):
"form": self.form, "form": self.form,
"fields": fields, "fields": fields,
"exclude": exclude + kwargs.get("exclude", []), "exclude": exclude + kwargs.get("exclude", []),
"formfield_callback": self.formfield_for_dbfield, "formfield_callback": curry(self.formfield_for_dbfield, request=request),
} }
defaults.update(kwargs) defaults.update(kwargs)
return modelform_factory(self.model, **defaults) return modelform_factory(self.model, **defaults)
Expand Down Expand Up @@ -837,7 +853,7 @@ def get_formset(self, request, obj=None, **kwargs):
"fk_name": self.fk_name, "fk_name": self.fk_name,
"fields": fields, "fields": fields,
"exclude": exclude + kwargs.get("exclude", []), "exclude": exclude + kwargs.get("exclude", []),
"formfield_callback": self.formfield_for_dbfield, "formfield_callback": curry(self.formfield_for_dbfield, request=request),
"extra": self.extra, "extra": self.extra,
"max_num": self.max_num, "max_num": self.max_num,
} }
Expand Down
58 changes: 58 additions & 0 deletions docs/ref/contrib/admin.txt
Expand Up @@ -597,6 +597,47 @@ with an operator:
Performs a full-text match. This is like the default search method but uses Performs a full-text match. This is like the default search method but uses
an index. Currently this is only available for MySQL. 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 ``ModelAdmin`` methods
---------------------- ----------------------


Expand Down Expand Up @@ -675,6 +716,23 @@ Notice the wrapped view in the fifth line above::


This wrapping will protect ``self.my_view`` from unauthorized access. 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 ``ModelAdmin`` media definitions
-------------------------------- --------------------------------


Expand Down
@@ -0,0 +1,43 @@
<?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>
25 changes: 25 additions & 0 deletions tests/regressiontests/admin_widgets/models.py
Expand Up @@ -2,9 +2,12 @@
from django.conf import settings from django.conf import settings
from django.db import models from django.db import models
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
from django.contrib.auth.models import User


class Member(models.Model): class Member(models.Model):
name = models.CharField(max_length=100) 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): def __unicode__(self):
return self.name return self.name
Expand Down Expand Up @@ -40,6 +43,28 @@ class Inventory(models.Model):


def __unicode__(self): def __unicode__(self):
return self.name 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': """ __test__ = {'WIDGETS_TESTS': """
>>> from datetime import datetime >>> from datetime import datetime
Expand Down

0 comments on commit f212b24

Please sign in to comment.