diff options
| author | Brian Rosner <brosner@gmail.com> | 2008-06-12 20:13:27 +0000 |
|---|---|---|
| committer | Brian Rosner <brosner@gmail.com> | 2008-06-12 20:13:27 +0000 |
| commit | f914b0a71b8059166208175f994f61e4984eab1d (patch) | |
| tree | 35b98683199273e79824e378c8803a844dd4080c | |
| parent | 4a6965b3087901b88df813d5696962155311f4dd (diff) | |
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
| -rw-r--r-- | django/contrib/admin/__init__.py | 2 | ||||
| -rw-r--r-- | django/contrib/admin/options.py | 19 | ||||
| -rw-r--r-- | django/contrib/admin/widgets.py | 13 | ||||
| -rw-r--r-- | django/contrib/redirects/models.py | 3 | ||||
| -rw-r--r-- | django/db/models/fields/__init__.py | 21 | ||||
| -rw-r--r-- | django/db/models/fields/related.py | 22 | ||||
| -rw-r--r-- | docs/admin.txt | 17 | ||||
| -rw-r--r-- | docs/custom_model_fields.txt | 1 | ||||
| -rw-r--r-- | docs/model-api.txt | 10 | ||||
| -rw-r--r-- | tests/regressiontests/modeladmin/models.py | 96 |
10 files changed, 157 insertions, 47 deletions
diff --git a/django/contrib/admin/__init__.py b/django/contrib/admin/__init__.py index 9e8b4fb8f4..6a459ec22a 100644 --- a/django/contrib/admin/__init__.py +++ b/django/contrib/admin/__init__.py @@ -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.sites import AdminSite, site diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 8ba543d4a7..0d2af3d524 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -17,6 +17,10 @@ from django.utils.translation import ugettext as _ from django.utils.encoding import force_unicode 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): pass @@ -133,6 +137,7 @@ class BaseModelAdmin(object): form = forms.ModelForm filter_vertical = () filter_horizontal = () + radio_fields = {} prepopulated_fields = {} def __init__(self): @@ -173,6 +178,11 @@ class BaseModelAdmin(object): 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 db_field.name in self.raw_id_fields: @@ -187,6 +197,15 @@ class BaseModelAdmin(object): if not db_field.name in self.raw_id_fields: formfield.widget.render = widgets.RelatedFieldWidgetWrapper(formfield.widget.render, db_field.rel, self.admin_site) 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. return db_field.formfield(**kwargs) diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py index 16b17071d2..e7ea4aa129 100644 --- a/django/contrib/admin/widgets.py +++ b/django/contrib/admin/widgets.py @@ -3,6 +3,8 @@ Form Widget classes specific to the Django admin site. """ 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.text import capfirst, truncate_words 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>' % \ (_('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): """ A FileField Widget that shows its current value if it has one. diff --git a/django/contrib/redirects/models.py b/django/contrib/redirects/models.py index 68f4afeb49..a35c95f1a8 100644 --- a/django/contrib/redirects/models.py +++ b/django/contrib/redirects/models.py @@ -3,7 +3,7 @@ from django.contrib.sites.models import Site from django.utils.translation import ugettext_lazy as _ 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, 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, @@ -28,6 +28,7 @@ from django.contrib import admin class RedirectAdmin(admin.ModelAdmin): list_filter = ('site',) search_fields = ('old_path', 'new_path') + radio_fields = {'site': admin.VERTICAL} admin.site.register(Redirect, RedirectAdmin) diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py index a05f65c443..e71f41cb85 100644 --- a/django/db/models/fields/__init__.py +++ b/django/db/models/fields/__init__.py @@ -26,8 +26,6 @@ from django.utils.maxlength import LegacyMaxlength class NOT_PROVIDED: pass -HORIZONTAL, VERTICAL = 1, 2 - # The values to use for "blank" in SelectFields. Will be appended to the start of most "choices" lists. BLANK_CHOICE_DASH = [("", "---------")] BLANK_CHOICE_NONE = [("", "None")] @@ -35,9 +33,6 @@ BLANK_CHOICE_NONE = [("", "None")] # prepares a value for use in a LIKE query 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): pass @@ -87,8 +82,8 @@ class Field(object): db_index=False, core=False, rel=None, default=NOT_PROVIDED, editable=True, serialize=True, unique_for_date=None, unique_for_month=None, unique_for_year=None, validator_list=None, - choices=None, radio_admin=None, help_text='', db_column=None, - db_tablespace=None, auto_created=False): + choices=None, help_text='', db_column=None, db_tablespace=None, + auto_created=False): self.name = name self.verbose_name = verbose_name 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_year = unique_for_year self._choices = choices or [] - self.radio_admin = radio_admin self.help_text = help_text self.db_column = db_column self.db_tablespace = db_tablespace or settings.DEFAULT_INDEX_TABLESPACE @@ -285,11 +279,7 @@ class Field(object): params['max_length'] = self.max_length 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() else: @@ -377,10 +367,7 @@ class Field(object): return first_choice + lst 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): if obj: diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py index 49d41ff3d9..c82574081c 100644 --- a/django/db/models/fields/related.py +++ b/django/db/models/fields/related.py @@ -1,6 +1,6 @@ from django.db import connection, transaction 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.query_utils import QueryWrapper from django.utils.text import capfirst @@ -628,14 +628,10 @@ class ForeignKey(RelatedField, Field): def prepare_field_objs_and_params(self, manipulator, name_prefix): 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) + if self.null: + field_objs = [oldforms.NullSelectField] else: - if self.null: - field_objs = [oldforms.NullSelectField] - else: - field_objs = [oldforms.SelectField] + field_objs = [oldforms.SelectField] params['choices'] = self.get_choices_default() return field_objs, params @@ -660,15 +656,11 @@ class ForeignKey(RelatedField, Field): if not obj: # In required many-to-one fields with only one available choice, # select that one available choice. Note: For SelectFields - # (radio_admin=False), we have to check that the length of choices - # is *2*, not 1, because SelectFields always have an initial - # "blank" value. Otherwise (radio_admin=True), we check that the - # length is 1. + # we have to check that the length of choices is *2*, not 1, + # because SelectFields always have an initial "blank" value. if not self.blank and self.choices: choice_list = self.get_choices_default() - if self.radio_admin and len(choice_list) == 1: - return {self.attname: choice_list[0][0]} - if not self.radio_admin and len(choice_list) == 2: + if len(choice_list) == 2: return {self.attname: choice_list[1][0]} return Field.flatten_data(self, follow, obj) diff --git a/docs/admin.txt b/docs/admin.txt index 7466d41ee3..0d3cb811ee 100644 --- a/docs/admin.txt +++ b/docs/admin.txt @@ -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. +``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`` ----------- diff --git a/docs/custom_model_fields.txt b/docs/custom_model_fields.txt index 2b344921ef..cbaac873e3 100644 --- a/docs/custom_model_fields.txt +++ b/docs/custom_model_fields.txt @@ -204,7 +204,6 @@ order: * ``unique_for_year`` * ``validator_list`` * ``choices`` - * ``radio_admin`` * ``help_text`` * ``db_column`` * ``db_tablespace``: Currently only used with the Oracle backend and only diff --git a/docs/model-api.txt b/docs/model-api.txt index 5e8d177f97..57e366f959 100644 --- a/docs/model-api.txt +++ b/docs/model-api.txt @@ -663,16 +663,6 @@ unless you want to override the default primary-key behavior. ``primary_key=True`` implies ``blank=False``, ``null=False`` and ``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`` ~~~~~~~~~~ diff --git a/tests/regressiontests/modeladmin/models.py b/tests/regressiontests/modeladmin/models.py index 5e67c873cd..e1690c143d 100644 --- a/tests/regressiontests/modeladmin/models.py +++ b/tests/regressiontests/modeladmin/models.py @@ -1,15 +1,30 @@ # coding: utf-8 from django.db import models +from datetime import date class Band(models.Model): name = models.CharField(max_length=100) bio = models.TextField() 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': """ ->>> from django.contrib.admin.options import ModelAdmin +>>> from django.contrib.admin.options import ModelAdmin, HORIZONTAL, VERTICAL >>> from django.contrib.admin.sites import AdminSite 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 ->>> 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 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) <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() + """ } |
