1
0
mirror of https://github.com/django/django.git synced 2025-07-04 09:49:12 +00:00

newforms-admin: Fixed #5731 -- Implemented ModelAdmin.radio_fields to match trunk's radio_admin. Removed legacy code and added tests. Thanks Karen Tracey for the initial work.

git-svn-id: http://code.djangoproject.com/svn/django/branches/newforms-admin@7626 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Brian Rosner 2008-06-12 20:13:27 +00:00
parent 4a6965b308
commit f914b0a71b
10 changed files with 157 additions and 47 deletions

View File

@ -1,3 +1,3 @@
from django.contrib.admin.options import ModelAdmin from django.contrib.admin.options import ModelAdmin, HORIZONTAL, VERTICAL
from django.contrib.admin.options import StackedInline, TabularInline from django.contrib.admin.options import StackedInline, TabularInline
from django.contrib.admin.sites import AdminSite, site from django.contrib.admin.sites import AdminSite, site

View File

@ -17,6 +17,10 @@ from django.utils.translation import ugettext as _
from django.utils.encoding import force_unicode from django.utils.encoding import force_unicode
import sets import sets
HORIZONTAL, VERTICAL = 1, 2
# returns the <ul> class for a given radio_admin field
get_ul_class = lambda x: 'radiolist%s' % ((x == HORIZONTAL) and ' inline' or '')
class IncorrectLookupParameters(Exception): class IncorrectLookupParameters(Exception):
pass pass
@ -133,6 +137,7 @@ class BaseModelAdmin(object):
form = forms.ModelForm form = forms.ModelForm
filter_vertical = () filter_vertical = ()
filter_horizontal = () filter_horizontal = ()
radio_fields = {}
prepopulated_fields = {} prepopulated_fields = {}
def __init__(self): def __init__(self):
@ -173,6 +178,11 @@ class BaseModelAdmin(object):
if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)): if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)):
if isinstance(db_field, models.ForeignKey) and db_field.name in self.raw_id_fields: if isinstance(db_field, models.ForeignKey) and db_field.name in self.raw_id_fields:
kwargs['widget'] = widgets.ForeignKeyRawIdWidget(db_field.rel) 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: else:
if isinstance(db_field, models.ManyToManyField): if isinstance(db_field, models.ManyToManyField):
if db_field.name in self.raw_id_fields: if db_field.name in self.raw_id_fields:
@ -188,6 +198,15 @@ class BaseModelAdmin(object):
formfield.widget.render = widgets.RelatedFieldWidgetWrapper(formfield.widget.render, db_field.rel, self.admin_site) formfield.widget.render = widgets.RelatedFieldWidgetWrapper(formfield.widget.render, db_field.rel, self.admin_site)
return formfield return formfield
if db_field.choices and db_field.name in self.radio_fields:
kwargs['widget'] = widgets.AdminRadioSelect(
choices=db_field.get_choices(include_blank=db_field.blank,
blank_choice=[('', _('None'))]),
attrs={
'class': get_ul_class(self.radio_fields[db_field.name]),
}
)
# For any other type of field, just call its formfield() method. # For any other type of field, just call its formfield() method.
return db_field.formfield(**kwargs) return db_field.formfield(**kwargs)

View File

@ -3,6 +3,8 @@ Form Widget classes specific to the Django admin site.
""" """
from django import newforms as forms from django import newforms as forms
from django.newforms.widgets import RadioFieldRenderer
from django.newforms.util import flatatt
from django.utils.datastructures import MultiValueDict from django.utils.datastructures import MultiValueDict
from django.utils.text import capfirst, truncate_words from django.utils.text import capfirst, truncate_words
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
@ -62,6 +64,17 @@ class AdminSplitDateTime(forms.SplitDateTimeWidget):
return mark_safe(u'<p class="datetime">%s %s<br />%s %s</p>' % \ return mark_safe(u'<p class="datetime">%s %s<br />%s %s</p>' % \
(_('Date:'), rendered_widgets[0], _('Time:'), rendered_widgets[1])) (_('Date:'), rendered_widgets[0], _('Time:'), rendered_widgets[1]))
class AdminRadioFieldRenderer(RadioFieldRenderer):
def render(self):
"""Outputs a <ul> for this set of radio fields."""
return mark_safe(u'<ul%s>\n%s\n</ul>' % (
flatatt(self.attrs),
u'\n'.join([u'<li>%s</li>' % force_unicode(w) for w in self]))
)
class AdminRadioSelect(forms.RadioSelect):
renderer = AdminRadioFieldRenderer
class AdminFileWidget(forms.FileInput): class AdminFileWidget(forms.FileInput):
""" """
A FileField Widget that shows its current value if it has one. A FileField Widget that shows its current value if it has one.

View File

@ -3,7 +3,7 @@ from django.contrib.sites.models import Site
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
class Redirect(models.Model): class Redirect(models.Model):
site = models.ForeignKey(Site, radio_admin=models.VERTICAL) site = models.ForeignKey(Site)
old_path = models.CharField(_('redirect from'), max_length=200, db_index=True, old_path = models.CharField(_('redirect from'), max_length=200, db_index=True,
help_text=_("This should be an absolute path, excluding the domain name. Example: '/events/search/'.")) help_text=_("This should be an absolute path, excluding the domain name. Example: '/events/search/'."))
new_path = models.CharField(_('redirect to'), max_length=200, blank=True, new_path = models.CharField(_('redirect to'), max_length=200, blank=True,
@ -28,6 +28,7 @@ from django.contrib import admin
class RedirectAdmin(admin.ModelAdmin): class RedirectAdmin(admin.ModelAdmin):
list_filter = ('site',) list_filter = ('site',)
search_fields = ('old_path', 'new_path') search_fields = ('old_path', 'new_path')
radio_fields = {'site': admin.VERTICAL}
admin.site.register(Redirect, RedirectAdmin) admin.site.register(Redirect, RedirectAdmin)

View File

@ -26,8 +26,6 @@ from django.utils.maxlength import LegacyMaxlength
class NOT_PROVIDED: class NOT_PROVIDED:
pass pass
HORIZONTAL, VERTICAL = 1, 2
# The values to use for "blank" in SelectFields. Will be appended to the start of most "choices" lists. # The values to use for "blank" in SelectFields. Will be appended to the start of most "choices" lists.
BLANK_CHOICE_DASH = [("", "---------")] BLANK_CHOICE_DASH = [("", "---------")]
BLANK_CHOICE_NONE = [("", "None")] BLANK_CHOICE_NONE = [("", "None")]
@ -35,9 +33,6 @@ BLANK_CHOICE_NONE = [("", "None")]
# prepares a value for use in a LIKE query # prepares a value for use in a LIKE query
prep_for_like_query = lambda x: smart_unicode(x).replace("\\", "\\\\").replace("%", "\%").replace("_", "\_") prep_for_like_query = lambda x: smart_unicode(x).replace("\\", "\\\\").replace("%", "\%").replace("_", "\_")
# returns the <ul> class for a given radio_admin value
get_ul_class = lambda x: 'radiolist%s' % ((x == HORIZONTAL) and ' inline' or '')
class FieldDoesNotExist(Exception): class FieldDoesNotExist(Exception):
pass pass
@ -87,8 +82,8 @@ class Field(object):
db_index=False, core=False, rel=None, default=NOT_PROVIDED, db_index=False, core=False, rel=None, default=NOT_PROVIDED,
editable=True, serialize=True, unique_for_date=None, editable=True, serialize=True, unique_for_date=None,
unique_for_month=None, unique_for_year=None, validator_list=None, unique_for_month=None, unique_for_year=None, validator_list=None,
choices=None, radio_admin=None, help_text='', db_column=None, choices=None, help_text='', db_column=None, db_tablespace=None,
db_tablespace=None, auto_created=False): auto_created=False):
self.name = name self.name = name
self.verbose_name = verbose_name self.verbose_name = verbose_name
self.primary_key = primary_key self.primary_key = primary_key
@ -105,7 +100,6 @@ class Field(object):
self.unique_for_date, self.unique_for_month = unique_for_date, unique_for_month self.unique_for_date, self.unique_for_month = unique_for_date, unique_for_month
self.unique_for_year = unique_for_year self.unique_for_year = unique_for_year
self._choices = choices or [] self._choices = choices or []
self.radio_admin = radio_admin
self.help_text = help_text self.help_text = help_text
self.db_column = db_column self.db_column = db_column
self.db_tablespace = db_tablespace or settings.DEFAULT_INDEX_TABLESPACE self.db_tablespace = db_tablespace or settings.DEFAULT_INDEX_TABLESPACE
@ -285,10 +279,6 @@ class Field(object):
params['max_length'] = self.max_length params['max_length'] = self.max_length
if self.choices: if self.choices:
if self.radio_admin:
field_objs = [oldforms.RadioSelectField]
params['ul_class'] = get_ul_class(self.radio_admin)
else:
field_objs = [oldforms.SelectField] field_objs = [oldforms.SelectField]
params['choices'] = self.get_choices_default() params['choices'] = self.get_choices_default()
@ -377,9 +367,6 @@ class Field(object):
return first_choice + lst return first_choice + lst
def get_choices_default(self): def get_choices_default(self):
if self.radio_admin:
return self.get_choices(include_blank=self.blank, blank_choice=BLANK_CHOICE_NONE)
else:
return self.get_choices() return self.get_choices()
def _get_val_from_obj(self, obj): def _get_val_from_obj(self, obj):

View File

@ -1,6 +1,6 @@
from django.db import connection, transaction from django.db import connection, transaction
from django.db.models import signals, get_model from django.db.models import signals, get_model
from django.db.models.fields import AutoField, Field, IntegerField, PositiveIntegerField, PositiveSmallIntegerField, get_ul_class, FieldDoesNotExist from django.db.models.fields import AutoField, Field, IntegerField, PositiveIntegerField, PositiveSmallIntegerField, FieldDoesNotExist
from django.db.models.related import RelatedObject from django.db.models.related import RelatedObject
from django.db.models.query_utils import QueryWrapper from django.db.models.query_utils import QueryWrapper
from django.utils.text import capfirst from django.utils.text import capfirst
@ -628,10 +628,6 @@ class ForeignKey(RelatedField, Field):
def prepare_field_objs_and_params(self, manipulator, name_prefix): def prepare_field_objs_and_params(self, manipulator, name_prefix):
params = {'validator_list': self.validator_list[:], 'member_name': name_prefix + self.attname} params = {'validator_list': self.validator_list[:], 'member_name': name_prefix + self.attname}
if self.radio_admin:
field_objs = [oldforms.RadioSelectField]
params['ul_class'] = get_ul_class(self.radio_admin)
else:
if self.null: if self.null:
field_objs = [oldforms.NullSelectField] field_objs = [oldforms.NullSelectField]
else: else:
@ -660,15 +656,11 @@ class ForeignKey(RelatedField, Field):
if not obj: if not obj:
# In required many-to-one fields with only one available choice, # In required many-to-one fields with only one available choice,
# select that one available choice. Note: For SelectFields # select that one available choice. Note: For SelectFields
# (radio_admin=False), we have to check that the length of choices # we have to check that the length of choices is *2*, not 1,
# is *2*, not 1, because SelectFields always have an initial # because SelectFields always have an initial "blank" value.
# "blank" value. Otherwise (radio_admin=True), we check that the
# length is 1.
if not self.blank and self.choices: if not self.blank and self.choices:
choice_list = self.get_choices_default() choice_list = self.get_choices_default()
if self.radio_admin and len(choice_list) == 1: if len(choice_list) == 2:
return {self.attname: choice_list[0][0]}
if not self.radio_admin and len(choice_list) == 2:
return {self.attname: choice_list[1][0]} return {self.attname: choice_list[1][0]}
return Field.flatten_data(self, follow, obj) return Field.flatten_data(self, follow, obj)

View File

@ -344,6 +344,23 @@ ordered. This should be a list or tuple in the same format as a model's
If this isn't provided, the Django admin will use the model's default ordering. If this isn't provided, the Django admin will use the model's default ordering.
``radio_fields``
----------------
By default, Django's admin uses a select-box interface (<select>) for
fields that are ``ForeignKey`` or have ``choices`` set. If a field is present
in ``radio_fields``, Django will use a radio-button interface instead.
Assuming ``group`` is a ``ForeignKey`` on the ``Person`` model::
class PersonAdmin(admin.ModelAdmin):
radio_fields = {"group": admin.VERTICAL}
You have the choice of using ``HORIZONTAL`` or ``VERTICAL`` from the
``django.contrib.admin`` module.
Don't include a field in ``radio_fields`` unless it's a ``ForeignKey`` or has
``choices`` set.
``save_as`` ``save_as``
----------- -----------

View File

@ -204,7 +204,6 @@ order:
* ``unique_for_year`` * ``unique_for_year``
* ``validator_list`` * ``validator_list``
* ``choices`` * ``choices``
* ``radio_admin``
* ``help_text`` * ``help_text``
* ``db_column`` * ``db_column``
* ``db_tablespace``: Currently only used with the Oracle backend and only * ``db_tablespace``: Currently only used with the Oracle backend and only

View File

@ -663,16 +663,6 @@ unless you want to override the default primary-key behavior.
``primary_key=True`` implies ``blank=False``, ``null=False`` and ``primary_key=True`` implies ``blank=False``, ``null=False`` and
``unique=True``. Only one primary key is allowed on an object. ``unique=True``. Only one primary key is allowed on an object.
``radio_admin``
~~~~~~~~~~~~~~~
By default, Django's admin uses a select-box interface (<select>) for
fields that are ``ForeignKey`` or have ``choices`` set. If ``radio_admin``
is set to ``True``, Django will use a radio-button interface instead.
Don't use this for a field unless it's a ``ForeignKey`` or has ``choices``
set.
``unique`` ``unique``
~~~~~~~~~~ ~~~~~~~~~~

View File

@ -1,15 +1,30 @@
# coding: utf-8 # coding: utf-8
from django.db import models from django.db import models
from datetime import date
class Band(models.Model): class Band(models.Model):
name = models.CharField(max_length=100) name = models.CharField(max_length=100)
bio = models.TextField() bio = models.TextField()
sign_date = models.DateField() sign_date = models.DateField()
def __unicode__(self):
return self.name
class Concert(models.Model):
main_band = models.ForeignKey(Band, related_name='main_concerts')
opening_band = models.ForeignKey(Band, related_name='opening_concerts',
blank=True)
day = models.CharField(max_length=3, choices=((1, 'Fri'), (2, 'Sat')))
transport = models.CharField(max_length=100, choices=(
(1, 'Plane'),
(2, 'Train'),
(3, 'Bus')
), blank=True)
__test__ = {'API_TESTS': """ __test__ = {'API_TESTS': """
>>> from django.contrib.admin.options import ModelAdmin >>> from django.contrib.admin.options import ModelAdmin, HORIZONTAL, VERTICAL
>>> from django.contrib.admin.sites import AdminSite >>> from django.contrib.admin.sites import AdminSite
None of the following tests really depend on the content of the request, so None of the following tests really depend on the content of the request, so
@ -17,7 +32,9 @@ we'll just pass in None.
>>> request = None >>> request = None
>>> band = Band(name='The Doors', bio='') # the sign_date is not 100 percent accurate ;)
>>> band = Band(name='The Doors', bio='', sign_date=date(1965, 1, 1))
>>> band.save()
Under the covers, the admin system will initialize ModelAdmin with a Model Under the covers, the admin system will initialize ModelAdmin with a Model
class and an AdminSite instance, so let's just go ahead and do that manually class and an AdminSite instance, so let's just go ahead and do that manually
@ -105,5 +122,80 @@ properly. This won't, however, break any of the admin widgets or media.
>>> type(ma.get_form(request).base_fields['sign_date'].widget) >>> type(ma.get_form(request).base_fields['sign_date'].widget)
<class 'django.contrib.admin.widgets.AdminDateWidget'> <class 'django.contrib.admin.widgets.AdminDateWidget'>
# 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.
>>> cma = ModelAdmin(Concert, site)
>>> cmafa = cma.get_form(request)
>>> type(cmafa.base_fields['main_band'].widget)
<class 'django.newforms.widgets.Select'>
>>> list(cmafa.base_fields['main_band'].widget.choices)
[(u'', u'---------'), (1, u'The Doors')]
>>> type(cmafa.base_fields['opening_band'].widget)
<class 'django.newforms.widgets.Select'>
>>> list(cmafa.base_fields['opening_band'].widget.choices)
[(u'', u'---------'), (1, u'The Doors')]
>>> type(cmafa.base_fields['day'].widget)
<class 'django.newforms.widgets.Select'>
>>> list(cmafa.base_fields['day'].widget.choices)
[('', '---------'), (1, 'Fri'), (2, 'Sat')]
>>> type(cmafa.base_fields['transport'].widget)
<class 'django.newforms.widgets.Select'>
>>> list(cmafa.base_fields['transport'].widget.choices)
[('', '---------'), (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
blank=True for the model field. Finally, the widget should have the
'radiolist' attr, and 'inline' as well if the field is specified HORIZONTAL.
>>> class ConcertAdmin(ModelAdmin):
... radio_fields = {
... 'main_band': HORIZONTAL,
... 'opening_band': VERTICAL,
... 'day': VERTICAL,
... 'transport': HORIZONTAL,
... }
>>> cma = ConcertAdmin(Concert, site)
>>> cmafa = cma.get_form(request)
>>> type(cmafa.base_fields['main_band'].widget)
<class 'django.contrib.admin.widgets.AdminRadioSelect'>
>>> 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)
<class 'django.contrib.admin.widgets.AdminRadioSelect'>
>>> cmafa.base_fields['opening_band'].widget.attrs
{'class': 'radiolist'}
>>> list(cmafa.base_fields['opening_band'].widget.choices)
[(u'', u'None'), (1, u'The Doors')]
>>> type(cmafa.base_fields['day'].widget)
<class 'django.contrib.admin.widgets.AdminRadioSelect'>
>>> cmafa.base_fields['day'].widget.attrs
{'class': 'radiolist'}
>>> list(cmafa.base_fields['day'].widget.choices)
[(1, 'Fri'), (2, 'Sat')]
>>> type(cmafa.base_fields['transport'].widget)
<class 'django.contrib.admin.widgets.AdminRadioSelect'>
>>> cmafa.base_fields['transport'].widget.attrs
{'class': 'radiolist inline'}
>>> list(cmafa.base_fields['transport'].widget.choices)
[('', u'None'), (1, 'Plane'), (2, 'Train'), (3, 'Bus')]
>>> band.delete()
""" """
} }